Browse Source

HPCC-25746 React version of Landing Zone

Signed-off-by: Jeremy Clements <jeremy.clements@lexisnexisrisk.com>

Create react components for landing zone in ECL watch. Squashed commit messages below

- adding routes / links to react and "legacy" landing zone pages
- fix issue preventing LZBrowseWidget from rendering
- add TargetDropzoneField component
- preview button click, change url to route landingzone preview
- download and delete button actions
- add target-server field type
- change target-dropzone field type after an earlier rebase
- fix some column defintions
- correct Fields and Filter import paths
- wip, test updating prop dropdown dependant upon another
- wip, try overriding getSelection of ESPUtil to change store
- override DojoGrid getSelected method
- conditionally add getSelected to widget params
- formatQuery return promise
- change loop in filterExists
- add a css grid-based version of form field grouping
- adding spray dialogs
- change GridForm to merge multiple groups of fields for state
- reorganizing some code for spray modals in LandingZone
- fix npm lint issues
- rearrange delimited form fields to make form a bit shorter
- revert changes to Filter component
- add react-hook-form as dependency
- some additional strings for LandingZone.tsx
- add FixedImportForm
- add Delimited, Json & Xml import forms
- add Variable & Blob import forms
- vscode sorted imports?
- additional constant for a generic required validation error
- removed unused import
- change TargetDropzone field to include the actual path as a property of
  each IDropdownOption object
- change TargetServer field to include OS as a property of the returned IDropdownOption objects
- correct types of TargetDfuSprayQueue field
- add TargetFolder field for LZ upload form
- fix lint error for async callback in useConst
- add dropzone for dragging files into LZ pane
- removing some debugging console logs
- add FileListForm to LandingZone
- removing some debug console logs & unused imports
- change how grid store and query are created / updated
- fixing some typescript compiler warnings/errors I noticed in my dev env's build log
- add an uploading... status message during file upload
- finish Add File implementation
- corrected Filter behaviour
- selection might not be an Array (undefined or null)
- fix selection missing properties / methods required by other functions (preview, etc...)
- memoizing some consts for react exhaustive deps
- bump @hpcc-js package versions
- fix merge conflicts
Jeremy Clements 4 years ago
parent
commit
c6249c8910

+ 1 - 0
esp/src/eclwatch/LZBrowseWidget.js

@@ -31,6 +31,7 @@ define([
     "hpcc/TargetComboBoxWidget",
     "hpcc/SelectionGridWidget",
     "hpcc/FilterDropDownWidget",
+    "dijit/Dialog",
     "dijit/layout/BorderContainer",
     "dijit/layout/TabContainer",
     "dijit/layout/ContentPane",

+ 4 - 1
esp/src/src-react/components/DojoGrid.tsx

@@ -28,6 +28,7 @@ interface DojoGridProps {
     query?: any;
     sort?: any;
     columns: any;
+    getSelected?: () => any[];
     setGrid: (_: any) => void;
     setSelection: (_: any[]) => void;
 }
@@ -38,6 +39,7 @@ export const DojoGrid: React.FunctionComponent<DojoGridProps> = ({
     query,
     sort,
     columns,
+    getSelected,
     setGrid,
     setSelection
 }) => {
@@ -57,12 +59,13 @@ export const DojoGrid: React.FunctionComponent<DojoGridProps> = ({
     const params = React.useMemo(() => {
         return {
             deselectOnRefresh: true,
+            getSelected,
             store,
             query,
             sort,
             columns: { ...columns }
         };
-    }, [columns, query, sort, store]);
+    }, [columns, getSelected, query, sort, store]);
 
     const gridSelInit = React.useCallback(grid => {
         grid.onSelectionChanged(() => setSelection(grid.getSelected()));

+ 400 - 0
esp/src/src-react/components/LandingZone.tsx

@@ -0,0 +1,400 @@
+import * as React from "react";
+import { CommandBar, ContextualMenuItemType, ICommandBarItemProps, mergeStyleSets } from "@fluentui/react";
+import { useConst, useOnEvent } from "@fluentui/react-hooks";
+import * as domClass from "dojo/dom-class";
+import * as iframe from "dojo/request/iframe";
+import * as put from "put-selector/put";
+import { TpDropZoneQuery } from "src/WsTopology";
+import * as FileSpray from "src/FileSpray";
+import * as ESPRequest from "src/ESPRequest";
+import * as Utility from "src/Utility";
+import nlsHPCC from "src/nlsHPCC";
+import { HolyGrail } from "../layouts/HolyGrail";
+import { pushParams } from "../util/history";
+import { ShortVerticalDivider } from "./Common";
+import { DojoGrid, selector, tree } from "./DojoGrid";
+import { Fields } from "./forms/Fields";
+import { Filter } from "./forms/Filter";
+import { AddFileForm } from "./forms/landing-zone/AddFileForm";
+import { BlobImportForm } from "./forms/landing-zone/BlobImportForm";
+import { DelimitedImportForm } from "./forms/landing-zone/DelimitedImportForm";
+import { FixedImportForm } from "./forms/landing-zone/FixedImportForm";
+import { JsonImportForm } from "./forms/landing-zone/JsonImportForm";
+import { VariableImportForm } from "./forms/landing-zone/VariableImportForm";
+import { XmlImportForm } from "./forms/landing-zone/XmlImportForm";
+import { FileListForm } from "./forms/landing-zone/FileListForm";
+
+function formatQuery(_filter) {
+    return {
+        DropZoneName: _filter.DropZoneName,
+        Server: _filter.Server,
+        NameFilter: _filter.NameFilter,
+        ECLWatchVisibleOnly: true
+    };
+}
+
+const FilterFields: Fields = {
+    "DropZoneName": { type: "target-dropzone", label: nlsHPCC.DropZone },
+    "Server": { type: "target-server", label: nlsHPCC.Server },
+    "NameFilter": { type: "string", label: nlsHPCC.FileName, placeholder: nlsHPCC.somefile },
+};
+
+interface LandingZoneFilter {
+    DropZoneName?: string;
+    Server?: string;
+    NameFilter?: string;
+    ECLWatchVisibleOnly?: boolean;
+    __dropZone?: any;
+}
+
+const emptyFilter: LandingZoneFilter = {};
+
+interface LandingZoneProps {
+    filter?: LandingZoneFilter;
+    store?: any;
+}
+
+export const LandingZone: React.FunctionComponent<LandingZoneProps> = ({
+    filter = emptyFilter,
+    store
+}) => {
+
+    const [grid, setGrid] = React.useState<any>(undefined);
+    const [showFilter, setShowFilter] = React.useState(false);
+    const [showAddFile, setShowAddFile] = React.useState(false);
+    const [showFixed, setShowFixed] = React.useState(false);
+    const [showDelimited, setShowDelimited] = React.useState(false);
+    const [showXml, setShowXml] = React.useState(false);
+    const [showJson, setShowJson] = React.useState(false);
+    const [showVariable, setShowVariable] = React.useState(false);
+    const [showBlob, setShowBlob] = React.useState(false);
+    const [selection, setSelection] = React.useState([]);
+    const [showDropZone, setShowDropzone] = React.useState(false);
+    const [uploadFiles, setUploadFiles] = React.useState([]);
+    const [showFileUpload, setShowFileUpload] = React.useState(false);
+    const [targetDropzones, setTargetDropzones] = React.useState([]);
+
+    React.useEffect(() => {
+        TpDropZoneQuery({}).then(({ TpDropZoneQueryResponse }) => {
+            setTargetDropzones(TpDropZoneQueryResponse?.TpDropZones?.TpDropZone || []);
+        });
+    }, []);
+
+    //  Grid ---
+    const gridStore = useConst(FileSpray.CreateLandingZonesStore({}));
+    const gridQuery = useConst({});
+    const gridSort = useConst([{ attribute: "modifiedtime", "descending": true }]);
+    const gridColumns = useConst({
+        col1: selector({
+            width: 27,
+            disabled: function (item) {
+                if (item.type) {
+                    switch (item.type) {
+                        case "dropzone":
+                        case "folder":
+                        case "machine":
+                            return true;
+                    }
+                }
+                return false;
+            },
+            selectorType: "checkbox"
+        }),
+        displayName: tree({
+            label: nlsHPCC.Name,
+            sortable: false,
+            formatter: function (_name, row) {
+                let img = "";
+                let name = row.displayName;
+                if (row.isDir === undefined) {
+                    img = Utility.getImageHTML("server.png");
+                    name += " [" + row.Path + "]";
+                } else if (row.isMachine) {
+                    img = Utility.getImageHTML("machine.png");
+                } else if (row.isDir) {
+                    img = Utility.getImageHTML("folder.png");
+                } else {
+                    img = Utility.getImageHTML("file.png");
+                }
+                return img + "&nbsp;" + name;
+            },
+            renderExpando: function (level, hasChildren, expanded, object) {
+                const dir = this.grid.isRTL ? "right" : "left";
+                let cls = ".dgrid-expando-icon";
+                if (hasChildren) {
+                    cls += ".ui-icon.ui-icon-triangle-1-" + (expanded ? "se" : "e");
+                }
+                //@ts-ignore
+                const node = put("div" + cls + "[style=margin-" + dir + ": " + (level * (this.indentWidth || 9)) + "px; float: " + dir + "; margin-top: 3px]");
+                node.innerHTML = "&nbsp;";
+                return node;
+            }
+        }),
+        filesize: {
+            label: nlsHPCC.Size, width: 100,
+            renderCell: function (object, value, node, options) {
+                domClass.add(node, "justify-right");
+                node.innerText = Utility.convertedSize(value);
+            },
+        },
+        modifiedtime: { label: nlsHPCC.Date, width: 162 }
+    });
+
+    const refreshTable = React.useCallback((clearSelection = false) => {
+        const dropzones = targetDropzones.filter(row => row.Name === filter?.DropZoneName);
+        const machines = targetDropzones[0]?.TpMachines?.TpMachine?.filter(row => row.ConfigNetaddress === filter?.Server);
+        const query = {
+            id: "*",
+            filter: (filter?.DropZoneName && dropzones.length && machines.length) ? {
+                ...formatQuery(filter),
+                ECLWatchVisibleOnly: true,
+                __dropZone: {
+                    ...targetDropzones.filter(row => row.Name === filter?.DropZoneName)[0],
+                    machine: machines[0]
+                }
+            } : undefined
+        };
+        grid?.set("query", query);
+        if (clearSelection) {
+            grid?.clearSelection();
+        }
+    }, [filter, grid, targetDropzones]);
+
+    //  Command Bar  ---
+    const buttons = React.useMemo((): ICommandBarItemProps[] => [
+        {
+            key: "refresh", text: nlsHPCC.Refresh, iconProps: { iconName: "Refresh" },
+            onClick: () => refreshTable()
+        },
+        { key: "divider_1", itemType: ContextualMenuItemType.Divider, onRender: () => <ShortVerticalDivider /> },
+        {
+            key: "preview", text: nlsHPCC.Preview, disabled: !selection.length, iconProps: { iconName: "ComplianceAudit" },
+            onClick: () => {
+                if (selection.length === 1) {
+                    window.location.href = `#/landingzone/preview/${selection[0].getLogicalFile()}`;
+                }
+            }
+        },
+        { key: "divider_2", itemType: ContextualMenuItemType.Divider, onRender: () => <ShortVerticalDivider /> },
+        {
+            key: "upload", text: nlsHPCC.Upload, iconProps: { iconName: "Upload" },
+            onClick: () => {
+                document.getElementById("uploaderBtn").click();
+            }
+        },
+        {
+            key: "download", text: nlsHPCC.Download, disabled: !selection.length, iconProps: { iconName: "Download" },
+            onClick: () => {
+                selection.forEach(item => {
+                    const downloadIframeName = "downloadIframe_" + item.calculatedID;
+                    const frame = iframe.create(downloadIframeName);
+                    const url = ESPRequest.getBaseURL("FileSpray") + "/DownloadFile?Name=" + encodeURIComponent(item.name) + "&NetAddress=" + item.NetAddress + "&Path=" + encodeURIComponent(item.fullFolderPath) + "&OS=" + item.OS;
+                    iframe.setSrc(frame, url, true);
+                });
+            }
+        },
+        {
+            key: "delete", text: nlsHPCC.Delete, disabled: !selection.length, iconProps: { iconName: "Delete" },
+            onClick: () => {
+                const list = selection.map(s => s.name);
+                if (confirm(nlsHPCC.DeleteSelectedFiles + "\n" + list)) {
+                    selection.forEach((item, idx) => {
+                        if (item._isUserFile) {
+                            gridStore.removeUserFile(item);
+                            refreshTable(true);
+                        } else {
+                            FileSpray.DeleteDropZoneFile({
+                                request: {
+                                    NetAddress: item.NetAddress,
+                                    Path: item.fullFolderPath,
+                                    OS: item.OS,
+                                    Names: item.name
+                                },
+                                load: function (response) {
+                                    refreshTable(true);
+                                }
+                            });
+                        }
+                    });
+                }
+            }
+        },
+        { key: "divider_3", itemType: ContextualMenuItemType.Divider, onRender: () => <ShortVerticalDivider /> },
+        {
+            key: "filter", text: nlsHPCC.Filter, disabled: !!store, iconProps: { iconName: "Filter" },
+            onClick: () => setShowFilter(true)
+        },
+        { key: "divider_4", itemType: ContextualMenuItemType.Divider, onRender: () => <ShortVerticalDivider /> },
+        {
+            key: "addFile", text: nlsHPCC.AddFile, disabled: !!store,
+            onClick: () => setShowAddFile(true)
+        },
+        { key: "divider_5", itemType: ContextualMenuItemType.Divider, onRender: () => <ShortVerticalDivider /> },
+        {
+            key: "fixed", text: nlsHPCC.Fixed, disabled: !selection.length,
+            onClick: () => setShowFixed(true)
+        },
+        {
+            key: "delimited", text: nlsHPCC.Delimited, disabled: !selection.length,
+            onClick: () => setShowDelimited(true)
+        },
+        {
+            key: "xml", text: nlsHPCC.XML, disabled: !selection.length,
+            onClick: () => setShowXml(true)
+        },
+        {
+            key: "json", text: nlsHPCC.JSON, disabled: !selection.length,
+            onClick: () => setShowJson(true)
+        },
+        {
+            key: "variable", text: nlsHPCC.Variable, disabled: !selection.length,
+            onClick: () => setShowVariable(true)
+        },
+        {
+            key: "blob", text: nlsHPCC.Blob, disabled: !selection.length,
+            onClick: () => setShowBlob(true)
+        },
+        { key: "divider_6", itemType: ContextualMenuItemType.Divider, onRender: () => <ShortVerticalDivider /> }
+    ], [gridStore, refreshTable, selection, store]);
+
+    React.useEffect(() => {
+        //  refreshTable changes when filter changes...
+        refreshTable();
+    }, [refreshTable]);
+
+    //  Filter  ---
+    const filterFields: Fields = {};
+    for (const field in FilterFields) {
+        filterFields[field] = { ...FilterFields[field], value: filter[field] };
+    }
+
+    const dropStyles = mergeStyleSets({
+        dzWrapper: {
+            position: "absolute",
+            top: "118px",
+            bottom: 0,
+            background: "rgba(0, 0, 0, 0.12)",
+            left: "240px",
+            right: 0,
+            display: showDropZone ? "block" : "none",
+            zIndex: 1,
+        },
+        dzInner: {
+            position: "absolute",
+            background: "white",
+            top: "20px",
+            left: "30px",
+            right: "40px",
+            height: "80px",
+            display: "flex",
+            alignItems: "center",
+            justifyContent: "center",
+            border: "1px solid #aaa",
+            selectors: {
+                p: {
+                    fontSize: "1.333rem",
+                    color: "#aaa"
+                }
+            }
+        },
+        displayNone: {
+            display: "none"
+        }
+    });
+
+    const handleFileDragEnter = React.useCallback((evt) => {
+        evt.preventDefault();
+        evt.stopPropagation();
+        setShowDropzone(true);
+    }, [setShowDropzone]);
+    useOnEvent(document, "dragenter", handleFileDragEnter);
+
+    const handleFileDragOver = React.useCallback((evt) => {
+        evt.preventDefault();
+        evt.stopPropagation();
+        evt.dataTransfer.dropEffect = "copy";
+    }, []);
+
+    const handleFileDrop = React.useCallback((evt) => {
+        evt.preventDefault();
+        evt.stopPropagation();
+        setShowDropzone(false);
+        const files = [...evt.dataTransfer.files];
+        setUploadFiles(files);
+        setShowFileUpload(true);
+    }, [setShowDropzone, setShowFileUpload, setUploadFiles]);
+
+    const handleFileSelect = React.useCallback((evt) => {
+        evt.preventDefault();
+        evt.stopPropagation();
+        const files = [...evt.target.files];
+        setUploadFiles(files);
+        setShowFileUpload(true);
+    }, [setShowFileUpload, setUploadFiles]);
+
+    return <HolyGrail
+        header={<CommandBar items={buttons} />}
+        main={
+            <>
+                <input
+                    id="uploaderBtn" type="file" accept="*.txt, *.csv, *.json, *.xml"
+                    className={dropStyles.displayNone} onChange={handleFileSelect} multiple={true}
+                />
+                <div className={dropStyles.dzWrapper} onDragOver={handleFileDragOver} onDrop={handleFileDrop}>
+                    <div className={dropStyles.dzInner}>
+                        <p>Drop file(s) to upload.</p>
+                    </div>
+                </div>
+                <DojoGrid
+                    store={gridStore} columns={gridColumns} query={gridQuery}
+                    getSelected={function () {
+                        if (filter?.__dropZone) {
+                            return this.inherited(arguments, [FileSpray.CreateLandingZonesFilterStore({})]);
+                        }
+                        return this.inherited(arguments, [FileSpray.CreateFileListStore({})]);
+                    }}
+                    sort={gridSort} setGrid={setGrid} setSelection={setSelection}
+                />
+                <Filter
+                    showFilter={showFilter} setShowFilter={setShowFilter}
+                    filterFields={filterFields} onApply={pushParams}
+                />
+                { uploadFiles &&
+                <FileListForm
+                    formMinWidth={360} selection={uploadFiles}
+                    showForm={showFileUpload} setShowForm={setShowFileUpload}
+                    onSubmit={refreshTable}
+                />
+                }
+                <AddFileForm
+                    formMinWidth={620} store={gridStore} refreshGrid={refreshTable}
+                    showForm={showAddFile} setShowForm={setShowAddFile}
+                />
+                <FixedImportForm
+                    formMinWidth={620} selection={selection}
+                    showForm={showFixed} setShowForm={setShowFixed}
+                />
+                <DelimitedImportForm
+                    formMinWidth={620} selection={selection}
+                    showForm={showDelimited} setShowForm={setShowDelimited}
+                />
+                <XmlImportForm
+                    formMinWidth={620} selection={selection}
+                    showForm={showXml} setShowForm={setShowXml}
+                />
+                <JsonImportForm
+                    formMinWidth={620} selection={selection}
+                    showForm={showJson} setShowForm={setShowJson}
+                />
+                <VariableImportForm
+                    formMinWidth={620} selection={selection}
+                    showForm={showVariable} setShowForm={setShowVariable}
+                />
+                <BlobImportForm
+                    formMinWidth={620} selection={selection}
+                    showForm={showBlob} setShowForm={setShowBlob}
+                />
+            </>
+        }
+    />;
+};

+ 1 - 0
esp/src/src-react/components/Menu.tsx

@@ -133,6 +133,7 @@ const subMenuItems: SubMenuItems = {
         { headerText: nlsHPCC.LogicalFiles, itemKey: "/files" },
         { headerText: nlsHPCC.LogicalFiles + " (L)", itemKey: "/files/legacy" },
         { headerText: nlsHPCC.LandingZones, itemKey: "/landingzone" },
+        { headerText: nlsHPCC.LandingZones + " (L)", itemKey: "/landingzone/legacy" },
         { headerText: nlsHPCC.Workunits, itemKey: "/dfuworkunits" },
         { headerText: nlsHPCC.Workunits + " (L)", itemKey: "/dfuworkunits/legacy" },
         { headerText: nlsHPCC.XRef, itemKey: "/xref" },

+ 297 - 27
esp/src/src-react/components/forms/Fields.tsx

@@ -2,9 +2,9 @@ import * as React from "react";
 import { Checkbox, Dropdown as DropdownBase, TextField, IDropdownOption, Link, ProgressIndicator } from "@fluentui/react";
 import { TextField as MaterialUITextField } from "@material-ui/core";
 import { Topology, TpLogicalClusterQuery } from "@hpcc-js/comms";
-import { TpGroupQuery } from "src/WsTopology";
+import { TpDropZoneQuery, TpGroupQuery, TpServiceQuery } from "src/WsTopology";
 import { States } from "src/WsWorkunits";
-import { States as DFUStates } from "src/FileSpray";
+import { FileList, States as DFUStates } from "src/FileSpray";
 import nlsHPCC from "src/nlsHPCC";
 
 interface DropdownProps {
@@ -13,6 +13,8 @@ interface DropdownProps {
     options?: IDropdownOption[];
     selectedKey?: string;
     optional?: boolean;
+    required?: boolean;
+    errorMessage?: string;
     onChange?: (event: React.FormEvent<HTMLDivElement>, option?: IDropdownOption, index?: number) => void;
     placeholder?: string;
     className?: string;
@@ -23,7 +25,9 @@ const Dropdown: React.FunctionComponent<DropdownProps> = ({
     label,
     options = [],
     selectedKey,
+    required = false,
     optional = false,
+    errorMessage,
     onChange,
     placeholder,
     className
@@ -35,14 +39,15 @@ const Dropdown: React.FunctionComponent<DropdownProps> = ({
         setSelOptions(optional ? [{ key: "", text: "" }, ...options] : [...options]);
     }, [optional, options, selectedKey]);
 
-    return <DropdownBase key={key} label={label} className={className} defaultSelectedKey={selectedKey} onChange={onChange} placeholder={placeholder} options={selOptions} />;
+    return <DropdownBase key={key} label={label} errorMessage={errorMessage} required={required} className={className} defaultSelectedKey={selectedKey} onChange={onChange} placeholder={placeholder} options={selOptions} />;
 };
 
-export type FieldType = "string" | "number" | "checkbox" | "datetime" | "link" | "links" | "progress" |
+export type FieldType = "string" | "number" | "checkbox" | "datetime" | "dropdown" | "link" | "links" | "progress" |
     "workunit-state" |
     "file-type" | "file-sortby" |
     "queries-suspend-state" | "queries-active-state" |
-    "target-cluster" | "target-group" |
+    "target-cluster" | "target-dropzone" | "target-server" | "target-group" |
+    "target-dfuqueue" |
     "logicalfile-type" | "dfuworkunit-state";
 
 export type Values = { [name: string]: string | number | boolean | (string | number | boolean)[] };
@@ -53,6 +58,7 @@ interface BaseField {
     disabled?: (params) => boolean;
     placeholder?: string;
     readonly?: boolean;
+    required?: boolean;
 }
 
 interface StringField extends BaseField {
@@ -77,6 +83,12 @@ interface CheckboxField extends BaseField {
     value?: boolean;
 }
 
+interface DropdownField extends BaseField {
+    type: "dropdown";
+    value?: string;
+    options: IDropdownOption[];
+}
+
 interface WorkunitStateField extends BaseField {
     type: "workunit-state";
     value?: string;
@@ -112,6 +124,21 @@ interface TargetGroupField extends BaseField {
     value?: string;
 }
 
+interface TargetServerField extends BaseField {
+    type: "target-server";
+    value?: string;
+}
+
+interface TargetDropzoneField extends BaseField {
+    type: "target-dropzone";
+    value?: string;
+}
+
+interface TargetDfuSprayQueueField extends BaseField {
+    type: "target-dfuqueue";
+    value?: string;
+}
+
 interface LogicalFileType extends BaseField {
     type: "logicalfile-type";
     value?: string;
@@ -140,11 +167,12 @@ interface ProgressField extends BaseField {
     value?: string;
 }
 
-type Field = StringField | NumericField | CheckboxField | DateTimeField | LinkField | LinksField | ProgressField |
+type Field = StringField | NumericField | CheckboxField | DateTimeField | DropdownField | LinkField | LinksField | ProgressField |
     WorkunitStateField |
     FileTypeField | FileSortByField |
     QueriesSuspendStateField | QueriesActiveStateField |
-    TargetClusterField | TargetGroupField |
+    TargetClusterField | TargetDropzoneField | TargetServerField | TargetGroupField |
+    TargetDfuSprayQueueField |
     LogicalFileType | DFUWorkunitStateField;
 
 export type Fields = { [id: string]: Field };
@@ -179,23 +207,94 @@ export const TargetClusterTextField: React.FunctionComponent<TargetClusterTextFi
     return <Dropdown { ...props } options={targetClusters} />;
 };
 
+export interface TargetDropzoneTextFieldProps extends DropdownProps {
+    key: string;
+    label?: string;
+    selectedKey?: string;
+    optional?: boolean;
+    className?: string;
+    onChange?: (event?: React.FormEvent<HTMLDivElement>, option?: IDropdownOption, index?: number) => void;
+    placeholder?: string;
+}
+
+export const TargetDropzoneTextField: React.FunctionComponent<TargetDropzoneTextFieldProps> = (props) => {
+
+    const [targetDropzones, setTargetDropzones] = React.useState<IDropdownOption[]>([]);
+
+    React.useEffect(() => {
+        TpDropZoneQuery({}).then(({ TpDropZoneQueryResponse }) => {
+            let selected: IDropdownOption;
+            let selectedIdx: number;
+            setTargetDropzones(
+                TpDropZoneQueryResponse?.TpDropZones?.TpDropZone?.map((row, idx) => {
+                    const retVal = {
+                        key: row.Name,
+                        text: row.Name,
+                        path: row.Path
+                    };
+                    if (retVal.key === props.selectedKey) {
+                        selected = retVal;
+                        selectedIdx = idx;
+                    }
+                    return retVal;
+                }) || []
+            );
+            if (selected) {
+                props.onChange(undefined, selected, selectedIdx);
+            }
+        });
+    }, [props]);
+
+    return <Dropdown {...props} options={targetDropzones} />;
+};
+
+export interface TargetServerTextFieldProps extends DropdownProps {
+    key: string;
+    label?: string;
+    className?: string;
+    optional?: boolean;
+    onChange?: (event: React.FormEvent<HTMLDivElement>, option?: IDropdownOption, index?: number) => void;
+    placeholder?: string;
+    setSetDropzone?: (setDropzone: (dropzone: string) => void) => void;
+}
+
+export const TargetServerTextField: React.FunctionComponent<TargetServerTextFieldProps> = (props) => {
+
+    const [targetServers, setTargetServers] = React.useState<IDropdownOption[]>([]);
+    const [dropzone, setDropzone] = React.useState("");
+
+    props.setSetDropzone && props.setSetDropzone(setDropzone);
+
+    React.useEffect(() => {
+        TpDropZoneQuery({ Name: "" }).then(({ TpDropZoneQueryResponse }) => {
+            setTargetServers(
+                TpDropZoneQueryResponse?.TpDropZones?.TpDropZone?.filter(row => row.Name === dropzone)[0]?.TpMachines?.TpMachine?.map(n => {
+                    return {
+                        key: n.ConfigNetaddress,
+                        text: n.Netaddress,
+                        OS: n.OS
+                    };
+                }) || []
+            );
+        });
+    }, [props.selectedKey, dropzone]);
+
+    return <Dropdown {...props} options={targetServers} />;
+};
+
 export interface TargetGroupTextFieldProps {
     key: string;
     label?: string;
     selectedKey?: string;
     className?: string;
+    required?: boolean;
+    optional?: boolean;
+    errorMessage?: string;
     onChange?: (event: React.FormEvent<HTMLDivElement>, option?: IDropdownOption, index?: number) => void;
     placeholder?: string;
 }
 
-export const TargetGroupTextField: React.FunctionComponent<TargetGroupTextFieldProps> = ({
-    key,
-    label,
-    selectedKey,
-    className,
-    onChange,
-    placeholder
-}) => {
+export const TargetGroupTextField: React.FunctionComponent<TargetGroupTextFieldProps> = (props) => {
 
     const [targetGroups, setTargetGroups] = React.useState<IDropdownOption[]>([]);
 
@@ -212,7 +311,118 @@ export const TargetGroupTextField: React.FunctionComponent<TargetGroupTextFieldP
         });
     }, []);
 
-    return <Dropdown key={key} label={label} selectedKey={selectedKey} className={className} onChange={onChange} placeholder={placeholder} options={targetGroups} />;
+    return <Dropdown {...props} options={targetGroups} />;
+};
+
+export interface TargetDfuSprayQueueTextFieldProps {
+    key: string;
+    label?: string;
+    selectedKey?: string;
+    className?: string;
+    required?: boolean;
+    optional?: boolean;
+    errorMessage?: string;
+    onChange?: (event: React.FormEvent<HTMLDivElement>, option?: IDropdownOption, index?: number) => void;
+    placeholder?: string;
+}
+
+export const TargetDfuSprayQueueTextField: React.FunctionComponent<TargetDfuSprayQueueTextFieldProps> = (props) => {
+
+    const [dfuSprayQueues, setDfuSprayQueues] = React.useState<IDropdownOption[]>([]);
+
+    React.useEffect(() => {
+        TpServiceQuery({}).then(({ TpServiceQueryResponse }) => {
+            setDfuSprayQueues(
+                TpServiceQueryResponse.ServiceList.TpDfuServers.TpDfuServer.map(n => {
+                    return {
+                        key: n.Queue,
+                        text: n.Queue
+                    };
+                })
+            );
+        });
+    }, []);
+
+    return <Dropdown {...props} options={dfuSprayQueues} />;
+};
+
+export interface TargetFolderTextFieldProps {
+    key: string;
+    label?: string;
+    selectedKey?: string;
+    pathSepChar?: string;
+    machineAddress?: string;
+    machineDirectory?: string;
+    machineOS?: number;
+    className?: string;
+    required?: boolean;
+    optional?: boolean;
+    errorMessage?: string;
+    onChange?: (event?: React.FormEvent<HTMLDivElement>, option?: IDropdownOption, index?: number) => void;
+    placeholder?: string;
+}
+
+export const TargetFolderTextField: React.FunctionComponent<TargetFolderTextFieldProps> = (props) => {
+
+    const [folders, setFolders] = React.useState<IDropdownOption[]>([]);
+
+    const { pathSepChar, machineAddress, machineDirectory, machineOS } = { ...props };
+
+    const fetchFolders = React.useCallback((
+        pathSepChar: string, Netaddr: string, Path: string, OS: number, depth: number
+    ): Promise<IDropdownOption[]> => {
+        depth = depth || 0;
+        let retVal: IDropdownOption[] = [];
+        if (props.optional) {
+            retVal.push({ key: "", text: "" });
+        }
+        let _path = [Path, ""].join(pathSepChar).replace(machineDirectory, "");
+        _path = (_path.length > 1 && _path.substr(-1) === "/") ? _path.substr(0, _path.length - 1) : _path;
+        retVal.push({ key: Path, text: _path });
+        return new Promise((resolve, reject) => {
+            if (depth > 2) {
+                resolve(retVal);
+            } else {
+                FileList({
+                    request: {
+                        Netaddr: Netaddr,
+                        Path: Path,
+                        OS: OS
+                    },
+                    suppressExceptionToaster: true
+                }).then(({ FileListResponse }) => {
+                    const requests = [];
+                    FileListResponse.files.PhysicalFileStruct.forEach(file => {
+                        if (file.isDir) {
+                            if (Path + pathSepChar === "//") {
+                                requests.push(fetchFolders(pathSepChar, Netaddr, Path + file.name, OS, ++depth));
+                            } else {
+                                requests.push(fetchFolders(pathSepChar, Netaddr, [Path, file.name].join(pathSepChar), OS, ++depth));
+                            }
+                        }
+                    });
+                    Promise.all(requests).then(responses => {
+                        responses.forEach(response => {
+                            retVal = retVal.concat(response);
+                        });
+                        resolve(retVal);
+                    });
+                });
+            }
+        });
+    }, [machineDirectory, props.optional]);
+
+    React.useEffect(() => {
+        const _fetchFolders = async () => {
+            const folders = await fetchFolders(pathSepChar, machineAddress, machineDirectory, machineOS, 0);
+            setFolders(folders);
+        };
+        if (machineAddress && machineDirectory && machineOS) {
+            _fetchFolders();
+        }
+    }, [pathSepChar, machineAddress, machineDirectory, machineOS, fetchFolders]);
+
+    return <Dropdown {...props} options={folders} />;
 };
 
 const states = Object.keys(States).map(s => States[s]);
@@ -220,6 +430,7 @@ const dfustates = Object.keys(DFUStates).map(s => DFUStates[s]);
 
 export function createInputs(fields: Fields, onChange?: (id: string, newValue: any) => void) {
     const retVal: { id: string, label: string, field: any }[] = [];
+    let setDropzone = (dropzone: string) => { };
     for (const fieldID in fields) {
         const field = fields[fieldID];
         if (!field.disabled) {
@@ -227,7 +438,7 @@ export function createInputs(fields: Fields, onChange?: (id: string, newValue: a
         }
         switch (field.type) {
             case "string":
-                field.value = field.value || "";
+                field.value = field.value !== undefined ? field.value : "";
                 retVal.push({
                     id: fieldID,
                     label: field.label,
@@ -240,6 +451,7 @@ export function createInputs(fields: Fields, onChange?: (id: string, newValue: a
                         onChange={(evt, newValue) => onChange(fieldID, newValue)}
                         borderless={field.readonly && !field.multiline}
                         readOnly={field.readonly}
+                        required={field.required}
                         multiline={field.multiline}
                     />
                 });
@@ -252,13 +464,28 @@ export function createInputs(fields: Fields, onChange?: (id: string, newValue: a
                     field: <Checkbox
                         key={fieldID}
                         name={fieldID}
+                        disabled={field.disabled("") ? true : false}
                         checked={field.value === true ? true : false}
                         onChange={(evt, newValue) => onChange(fieldID, newValue)}
                     />
                 });
                 break;
+            case "dropdown":
+                field.value = field.value !== undefined ? field.value : "";
+                retVal.push({
+                    id: fieldID,
+                    label: field.label,
+                    field: <Dropdown
+                        key={fieldID}
+                        selectedKey={field.value}
+                        options={field.options}
+                        onChange={(ev, row) => onChange(fieldID, row.key)}
+                        placeholder={field.placeholder}
+                    />
+                });
+                break;
             case "datetime":
-                field.value = field.value || "";
+                field.value = field.value !== undefined ? field.value : "";
                 retVal.push({
                     id: fieldID,
                     label: field.label,
@@ -310,7 +537,7 @@ export function createInputs(fields: Fields, onChange?: (id: string, newValue: a
                 });
                 break;
             case "workunit-state":
-                field.value = field.value || "";
+                field.value = field.value !== undefined ? field.value : "";
                 retVal.push({
                     id: fieldID,
                     label: field.label,
@@ -330,7 +557,7 @@ export function createInputs(fields: Fields, onChange?: (id: string, newValue: a
                 });
                 break;
             case "file-type":
-                field.value = field.value || "";
+                field.value = field.value !== undefined ? field.value : "";
                 retVal.push({
                     id: fieldID,
                     label: field.label,
@@ -349,7 +576,7 @@ export function createInputs(fields: Fields, onChange?: (id: string, newValue: a
                 });
                 break;
             case "file-sortby":
-                field.value = field.value || "";
+                field.value = field.value !== undefined ? field.value : "";
                 retVal.push({
                     id: fieldID,
                     label: field.label,
@@ -369,7 +596,7 @@ export function createInputs(fields: Fields, onChange?: (id: string, newValue: a
                 });
                 break;
             case "queries-suspend-state":
-                field.value = field.value || "";
+                field.value = field.value !== undefined ? field.value : "";
                 retVal.push({
                     id: fieldID,
                     label: field.label,
@@ -390,7 +617,7 @@ export function createInputs(fields: Fields, onChange?: (id: string, newValue: a
                 });
                 break;
             case "queries-active-state":
-                field.value = field.value || "";
+                field.value = field.value !== undefined ? field.value : "";
                 retVal.push({
                     id: fieldID,
                     label: field.label,
@@ -408,7 +635,7 @@ export function createInputs(fields: Fields, onChange?: (id: string, newValue: a
                 });
                 break;
             case "target-cluster":
-                field.value = field.value || "";
+                field.value = field.value !== undefined ? field.value : "";
                 retVal.push({
                     id: fieldID,
                     label: field.label,
@@ -420,8 +647,38 @@ export function createInputs(fields: Fields, onChange?: (id: string, newValue: a
                     />
                 });
                 break;
+            case "target-dropzone":
+                field.value = field.value !== undefined ? field.value : "";
+                retVal.push({
+                    id: fieldID,
+                    label: field.label,
+                    field: <TargetDropzoneTextField
+                        key={fieldID}
+                        selectedKey={field.value}
+                        onChange={(ev, row) => {
+                            onChange(fieldID, row.key);
+                            setDropzone(row.key as string);
+                        }}
+                        placeholder={field.placeholder}
+                    />
+                });
+                break;
+            case "target-server":
+                field.value = field.value !== undefined ? field.value : "";
+                retVal.push({
+                    id: fieldID,
+                    label: field.label,
+                    field: <TargetServerTextField
+                        key={fieldID}
+                        selectedKey={field.value}
+                        onChange={(ev, row) => onChange(fieldID, row.key)}
+                        placeholder={field.placeholder}
+                        setSetDropzone={_ => setDropzone = _}
+                    />
+                });
+                break;
             case "target-group":
-                field.value = field.value || "";
+                field.value = field.value !== undefined ? field.value : "";
                 retVal.push({
                     id: fieldID,
                     label: field.label,
@@ -433,8 +690,21 @@ export function createInputs(fields: Fields, onChange?: (id: string, newValue: a
                     />
                 });
                 break;
+            case "target-dfuqueue":
+                field.value = field.value !== undefined ? field.value : "";
+                retVal.push({
+                    id: fieldID,
+                    label: field.label,
+                    field: <TargetDfuSprayQueueTextField
+                        key={fieldID}
+                        selectedKey={field.value}
+                        onChange={(ev, row) => onChange(fieldID, row.key)}
+                        placeholder={field.placeholder}
+                    />
+                });
+                break;
             case "dfuworkunit-state":
-                field.value = field.value || "";
+                field.value = field.value !== undefined ? field.value : "";
                 retVal.push({
                     id: fieldID,
                     label: field.label,
@@ -453,7 +723,7 @@ export function createInputs(fields: Fields, onChange?: (id: string, newValue: a
                 });
                 break;
             case "logicalfile-type":
-                field.value = field.value || "Created";
+                field.value = field.value !== undefined ? field.value : "Created";
                 retVal.push({
                     id: fieldID,
                     label: field.label,

+ 1 - 6
esp/src/src-react/components/forms/Filter.tsx

@@ -45,7 +45,6 @@ export const Filter: React.FunctionComponent<FilterProps> = ({
             {
                 flex: "1 1 auto",
                 borderTop: `4px solid ${theme.palette.themePrimary}`,
-                color: theme.palette.neutralPrimary,
                 display: "flex",
                 alignItems: "center",
                 fontWeight: FontWeights.semibold,
@@ -67,14 +66,10 @@ export const Filter: React.FunctionComponent<FilterProps> = ({
     const cancelIcon: IIconProps = { iconName: "Cancel" };
     const iconButtonStyles = {
         root: {
-            color: theme.palette.neutralPrimary,
             marginLeft: "auto",
             marginTop: "4px",
             marginRight: "2px",
         },
-        rootHovered: {
-            color: theme.palette.neutralDark,
-        },
     };
     const buttonStackStyles: IStackStyles = {
         root: {
@@ -135,4 +130,4 @@ export const Filter: React.FunctionComponent<FilterProps> = ({
             </Stack>
         </div>
     </Modal>;
-};
+};

+ 1 - 1
esp/src/src-react/components/forms/Groups.tsx

@@ -43,4 +43,4 @@ export const SimpleGroup: React.FunctionComponent<FieldsTableProps> = ({
             })
         }
     </>;
-};
+};

+ 147 - 0
esp/src/src-react/components/forms/landing-zone/AddFileForm.tsx

@@ -0,0 +1,147 @@
+import * as React from "react";
+import { ContextualMenu, IconButton, IDragOptions, mergeStyleSets, Modal, PrimaryButton, Stack, TextField } from "@fluentui/react";
+import { useId } from "@fluentui/react-hooks";
+import { useForm, Controller } from "react-hook-form";
+import nlsHPCC from "src/nlsHPCC";
+import * as FormStyles from "./styles";
+
+interface AddFileFormValues {
+    NetAddress: string;
+    fullPath: string;
+}
+
+const defaultValues: AddFileFormValues = {
+    NetAddress: "",
+    fullPath: ""
+};
+
+interface AddFileFormProps {
+    formMinWidth?: number;
+    showForm: boolean;
+    refreshGrid: (() => void),
+    store: any;
+    setShowForm: (_: boolean) => void;
+}
+
+export const AddFileForm: React.FunctionComponent<AddFileFormProps> = ({
+    formMinWidth = 300,
+    showForm,
+    refreshGrid,
+    store,
+    setShowForm
+}) => {
+
+    const { handleSubmit, control, reset } = useForm<AddFileFormValues>({ defaultValues });
+
+    const closeForm = React.useCallback(() => {
+        setShowForm(false);
+    }, [setShowForm]);
+
+    const onSubmit = React.useCallback(() => {
+        handleSubmit(
+            (data, evt) => {
+                const dropZone = {
+                    ...store.get(data.NetAddress),
+                    NetAddress: data.NetAddress
+                };
+                let fullPathParts = data.fullPath.split("/");
+                if (fullPathParts.length === 1) {
+                    fullPathParts = data.fullPath.split("\\");
+                }
+                const file = {
+                    ...store.get(data.NetAddress + data.fullPath),
+                    name: fullPathParts[fullPathParts.length - 1],
+                    displayName: fullPathParts[fullPathParts.length - 1],
+                    fullPath: data.fullPath,
+                    isDir: false,
+                    DropZone: dropZone
+                };
+                store.addUserFile(file);
+                refreshGrid();
+                closeForm();
+                reset(defaultValues);
+            },
+            err => {
+                console.log(err);
+            }
+        )();
+    }, [closeForm, handleSubmit, refreshGrid, reset, store]);
+
+    const titleId = useId("title");
+
+    const dragOptions: IDragOptions = {
+        moveMenuItemText: nlsHPCC.Move,
+        closeMenuItemText: nlsHPCC.Close,
+        menu: ContextualMenu,
+    };
+
+    const componentStyles = mergeStyleSets(
+        FormStyles.componentStyles,
+        {
+            container: {
+                minWidth: formMinWidth ? formMinWidth : 300,
+            }
+        }
+    );
+
+    return <Modal
+        titleAriaId={titleId}
+        isOpen={showForm}
+        onDismiss={closeForm}
+        isBlocking={false}
+        containerClassName={componentStyles.container}
+        dragOptions={dragOptions}
+    >
+        <div className={componentStyles.header}>
+            <span id={titleId}>{nlsHPCC.AddFile}</span>
+            <IconButton
+                styles={FormStyles.iconButtonStyles}
+                iconProps={FormStyles.cancelIcon}
+                ariaLabel={nlsHPCC.CloseModal}
+                onClick={closeForm}
+            />
+        </div>
+        <div className={componentStyles.body}>
+            <Stack>
+                <Controller
+                    control={control} name="NetAddress"
+                    render={({
+                        field: { onChange, name: fieldName, value },
+                        fieldState: { error }
+                    }) => <TextField
+                        name={fieldName}
+                        onChange={onChange}
+                        label={nlsHPCC.IP}
+                        required={true}
+                        value={value}
+                        errorMessage={ error && error?.message }
+                    /> }
+                    rules={{
+                        required: nlsHPCC.ValidationErrorRequired
+                    }}
+                />
+                <Controller
+                    control={control} name="fullPath"
+                    render={({
+                        field: { onChange, name: fieldName, value },
+                        fieldState: { error }
+                    }) => <TextField
+                        name={fieldName}
+                        onChange={onChange}
+                        label={nlsHPCC.Path}
+                        required={true}
+                        value={value}
+                        placeholder={nlsHPCC.NamePrefixPlaceholder}
+                        errorMessage={ error && error?.message }
+                    /> }
+                    rules={{
+                        required: nlsHPCC.ValidationErrorRequired
+                    }}
+                />
+            </Stack>
+            <Stack horizontal horizontalAlign="space-between" verticalAlign="end" styles={FormStyles.buttonStackStyles}>
+                <PrimaryButton text={nlsHPCC.Add} onClick={handleSubmit(onSubmit)} />
+            </Stack>
+        </div>
+    </Modal>;
+};

+ 339 - 0
esp/src/src-react/components/forms/landing-zone/BlobImportForm.tsx

@@ -0,0 +1,339 @@
+import * as React from "react";
+import { Checkbox, ContextualMenu, IconButton, IDragOptions, mergeStyleSets, Modal, PrimaryButton, Stack, TextField } from "@fluentui/react";
+import { useId } from "@fluentui/react-hooks";
+import { useForm, Controller } from "react-hook-form";
+import * as FileSpray from "src/FileSpray";
+import { TargetDfuSprayQueueTextField, TargetGroupTextField } from "../Fields";
+import nlsHPCC from "src/nlsHPCC";
+import * as FormStyles from "./styles";
+import { pushUrl } from "../../../util/history";
+
+interface BlobImportFormValues {
+    destGroup: string;
+    DFUServerQueue: string;
+    destLogicalName: string;
+    selectedFiles?: {
+        TargetName: string,
+        SourceFile: string,
+        SourceIP: string
+    }[],
+    prefix: string;
+    overwrite: boolean;
+    replicate: boolean;
+    nosplit: boolean;
+    noCommon: boolean;
+    compress: boolean;
+    failIfNoSourceFile: boolean;
+    delayedReplication: boolean;
+    expireDays: string;
+}
+
+const defaultValues: BlobImportFormValues = {
+    destGroup: "",
+    DFUServerQueue: "",
+    destLogicalName: "",
+    prefix: "",
+    overwrite: false,
+    replicate: false,
+    nosplit: false,
+    noCommon: true,
+    compress: false,
+    failIfNoSourceFile: false,
+    delayedReplication: true,
+    expireDays: ""
+};
+
+interface BlobImportFormProps {
+    formMinWidth?: number;
+    showForm: boolean;
+    selection: object[];
+    setShowForm: (_: boolean) => void;
+}
+
+export const BlobImportForm: React.FunctionComponent<BlobImportFormProps> = ({
+    formMinWidth = 300,
+    showForm,
+    selection,
+    setShowForm
+}) => {
+
+    const { handleSubmit, control, reset } = useForm<BlobImportFormValues>({ defaultValues });
+
+    const closeForm = React.useCallback(() => {
+        setShowForm(false);
+        reset(defaultValues);
+    }, [reset, setShowForm]);
+
+    const onSubmit = React.useCallback(() => {
+        handleSubmit(
+            (data, evt) => {
+                let request = {};
+                const files = data.selectedFiles;
+
+                delete data.selectedFiles;
+
+                files.forEach(file => {
+                    request = data;
+                    request["sourceIP"] = file.SourceIP;
+                    request["sourcePath"] = file.SourceFile;
+                    FileSpray.SprayFixed({
+                        request: request
+                    }).then((response) => {
+                        if (response.SprayFixedResponse?.wuid) {
+                            pushUrl(`/dfuworkunits/${response.SprayFixedResponse.wuid}`);
+                        }
+                    });
+                });
+            },
+            err => {
+                console.log(err);
+            }
+        )();
+    }, [handleSubmit]);
+
+    const titleId = useId("title");
+
+    const dragOptions: IDragOptions = {
+        moveMenuItemText: nlsHPCC.Move,
+        closeMenuItemText: nlsHPCC.Close,
+        menu: ContextualMenu,
+    };
+
+    const componentStyles = mergeStyleSets(
+        FormStyles.componentStyles,
+        {
+            container: {
+                minWidth: formMinWidth ? formMinWidth : 300,
+            }
+        }
+    );
+
+    React.useEffect(() => {
+        if (selection) {
+            const newValues = defaultValues;
+            newValues.selectedFiles = [];
+            selection.forEach((file, idx) => {
+                newValues.selectedFiles[idx] = {
+                    TargetName: "",
+                    SourceFile: file["fullPath"],
+                    SourceIP: file["NetAddress"]
+                };
+            });
+            reset(newValues);
+        }
+    }, [selection, reset]);
+
+    return <Modal
+        titleAriaId={titleId}
+        isOpen={showForm}
+        onDismiss={closeForm}
+        isBlocking={false}
+        containerClassName={componentStyles.container}
+        dragOptions={dragOptions}
+    >
+        <div className={componentStyles.header}>
+            <span id={titleId}>{`${nlsHPCC.Import} ${nlsHPCC.Blob}`}</span>
+            <IconButton
+                styles={FormStyles.iconButtonStyles}
+                iconProps={FormStyles.cancelIcon}
+                ariaLabel={nlsHPCC.CloseModal}
+                onClick={closeForm}
+            />
+        </div>
+        <div className={componentStyles.body}>
+            <Stack>
+                <Controller
+                    control={control} name="destGroup"
+                    render={({
+                        field: { onChange, name: fieldName, value },
+                        fieldState: { error }
+                    }) => <TargetGroupTextField
+                        key="destGroup"
+                        label={nlsHPCC.Group}
+                        required={true}
+                        optional={true}
+                        selectedKey={value}
+                        placeholder={nlsHPCC.SelectValue}
+                        onChange={(evt, option) => {
+                            onChange(option.key);
+                        }}
+                        errorMessage={ error && error?.message }
+                    /> }
+                    rules={{
+                        required: `${nlsHPCC.SelectA} ${nlsHPCC.Group}`
+                    }}
+                />
+                <Controller
+                    control={control} name="DFUServerQueue"
+                    render={({
+                        field: { onChange, name: fieldName, value },
+                        fieldState: { error }
+                    }) => <TargetDfuSprayQueueTextField
+                        key="DFUServerQueue"
+                        label={nlsHPCC.Queue}
+                        required={true}
+                        optional={true}
+                        selectedKey={value}
+                        placeholder={nlsHPCC.SelectValue}
+                        onChange={(evt, option) => {
+                            onChange(option.key);
+                        }}
+                        errorMessage={ error && error?.message }
+                    /> }
+                    rules={{
+                        required: `${nlsHPCC.SelectA} ${nlsHPCC.Queue}`
+                    }}
+                />
+                <Controller
+                    control={control} name="destLogicalName"
+                    render={({
+                        field: { onChange, name: fieldName, value },
+                        fieldState: { error }
+                    }) => <TextField
+                        name={fieldName}
+                        onChange={onChange}
+                        label={nlsHPCC.TargetScope}
+                        value={value}
+                        placeholder={nlsHPCC.TargetNamePlaceholder}
+                        errorMessage={ error && error?.message }
+                    /> }
+                    rules={{
+                        pattern: {
+                            value: /^([a-z0-9]+(::)?)+$/i,
+                            message: nlsHPCC.ValidationErrorNamePrefix
+                        }
+                    }}
+                />
+            </Stack>
+            <Stack>
+                <table className={`${componentStyles.twoColumnTable} ${componentStyles.selectionTable}`}>
+                    <thead>
+                        <tr>
+                            <th>{nlsHPCC.SourcePath}</th>
+                        </tr>
+                    </thead>
+                    <tbody>
+                    { selection && selection.map((file, idx) => {
+                        return <tr key={`File-${idx}`}>
+                            <td>
+                                <Controller
+                                    control={control} name={`selectedFiles.${idx}.SourceFile` as const}
+                                    render={({
+                                        field: { onChange, name: fieldName, value },
+                                        fieldState: { error }
+                                    }) => <TextField
+                                        name={fieldName}
+                                        onChange={onChange}
+                                        value={value}
+                                        errorMessage={ error && error?.message }
+                                    /> }
+                                    rules={{
+                                        required: nlsHPCC.ValidationErrorTargetNameRequired,
+                                        pattern: {
+                                            value: /^([a-z0-9]+[-a-z0-9 \._]+)+$/i,
+                                            message: nlsHPCC.ValidationErrorTargetNameInvalid
+                                        }
+                                    }}
+                                />
+                                <input type="hidden" name={`selectedFiles.${idx}.SourceIP` as const} value={file["NetAddress"]} />
+                            </td>
+                        </tr>;
+                    }) }
+                    </tbody>
+                </table>
+            </Stack>
+            <Stack>
+                <Controller
+                    control={control} name="prefix"
+                    render={({
+                        field: { onChange, name: fieldName, value },
+                        fieldState: { error }
+                    }) => <TextField
+                        name={fieldName}
+                        onChange={onChange}
+                        value={value}
+                        label={nlsHPCC.BlobPrefix}
+                        placeholder={nlsHPCC.PrefixPlaceholder}
+                        errorMessage={ error && error?.message }
+                    /> }
+                />
+            </Stack>
+            <Stack>
+                <table className={componentStyles.twoColumnTable}>
+                    <tbody><tr>
+                        <td><Controller
+                            control={control} name="overwrite"
+                            render={({
+                                field : { onChange, name: fieldName, value }
+                            }) => <Checkbox name={fieldName} checked={value} onChange={onChange} label={nlsHPCC.Overwrite} /> }
+                        /></td>
+                        <td><Controller
+                            control={control} name="replicate"
+                            render={({
+                                field : { onChange, name: fieldName, value }
+                            }) => <Checkbox name={fieldName} checked={value} onChange={onChange} label={nlsHPCC.Replicate} /> }
+                        /></td>
+                    </tr>
+                    <tr>
+                        <td><Controller
+                            control={control} name="nosplit"
+                            render={({
+                                field : { onChange, name: fieldName, value }
+                            }) => <Checkbox name={fieldName} checked={value} onChange={onChange} label={nlsHPCC.NoSplit} /> }
+                        /></td>
+                        <td><Controller
+                            control={control} name="noCommon"
+                            render={({
+                                field : { onChange, name: fieldName, value }
+                            }) => <Checkbox name={fieldName} checked={value} onChange={onChange} label={nlsHPCC.NoCommon} /> }
+                        /></td>
+                    </tr>
+                    <tr>
+                        <td><Controller
+                            control={control} name="compress"
+                            render={({
+                                field : { onChange, name: fieldName, value }
+                            }) => <Checkbox name={fieldName} checked={value} onChange={onChange} label={nlsHPCC.Compress} /> }
+                        /></td>
+                        <td><Controller
+                            control={control} name="failIfNoSourceFile"
+                            render={({
+                                field : { onChange, name: fieldName, value }
+                            }) => <Checkbox name={fieldName} checked={value} onChange={onChange} label={nlsHPCC.FailIfNoSourceFile} /> }
+                        /></td>
+                    </tr>
+                    <tr>
+                        <td><Controller
+                            control={control} name="expireDays"
+                            render={({
+                                field: { onChange, name: fieldName, value },
+                                fieldState: { error }
+                            }) => <TextField
+                                name={fieldName}
+                                onChange={onChange}
+                                label={nlsHPCC.ExpireDays}
+                                value={value}
+                                errorMessage={ error && error?.message }
+                            />}
+                            rules={{
+                                min: {
+                                    value: 1,
+                                    message: nlsHPCC.ValidationErrorExpireDaysMinimum
+                                }
+                            }}
+                        /></td>
+                        <td><Controller
+                            control={control} name="delayedReplication"
+                            render={({
+                                field : { onChange, name: fieldName, value }
+                            }) => <Checkbox name={fieldName} checked={value} onChange={onChange} label={nlsHPCC.DelayedReplication} disabled={true} /> }
+                        /></td>
+                    </tr></tbody>
+                </table>
+            </Stack>
+            <Stack horizontal horizontalAlign="space-between" verticalAlign="end" styles={FormStyles.buttonStackStyles}>
+                <PrimaryButton text={nlsHPCC.Import} onClick={handleSubmit(onSubmit)} />
+            </Stack>
+        </div>
+    </Modal>;
+};

+ 452 - 0
esp/src/src-react/components/forms/landing-zone/DelimitedImportForm.tsx

@@ -0,0 +1,452 @@
+import * as React from "react";
+import { Checkbox, ContextualMenu, Dropdown, IconButton, IDragOptions, mergeStyleSets, Modal, PrimaryButton, Stack, TextField } from "@fluentui/react";
+import { useId } from "@fluentui/react-hooks";
+import { useForm, Controller } from "react-hook-form";
+import * as FileSpray from "src/FileSpray";
+import { TargetDfuSprayQueueTextField, TargetGroupTextField } from "../Fields";
+import nlsHPCC from "src/nlsHPCC";
+import * as FormStyles from "./styles";
+import { pushUrl } from "../../../util/history";
+
+interface DelimitedImportFormValues {
+    destGroup: string;
+    DFUServerQueue: string;
+    namePrefix: string;
+    selectedFiles?: {
+        TargetName: string,
+        SourceFile: string,
+        SourceIP: string
+    }[],
+    sourceFormat: string;
+    sourceMaxRecordSize: string;
+    sourceCsvQuote: string;
+    sourceCsvEscape: string;
+    sourceCsvSeparate: string;
+    sourceCsvTerminate: string;
+    NoSourceCsvSeparator: boolean;
+    overwrite: boolean;
+    replicate: boolean;
+    nosplit: boolean;
+    noCommon: boolean;
+    compress: boolean;
+    failIfNoSourceFile: boolean;
+    recordStructurePresent: boolean;
+    quotedTerminator: boolean;
+    delayedReplication: boolean;
+    expireDays: string;
+}
+
+const defaultValues: DelimitedImportFormValues = {
+    destGroup: "",
+    DFUServerQueue: "",
+    namePrefix: "",
+    sourceFormat: "1",
+    sourceMaxRecordSize: "",
+    sourceCsvQuote: "\"",
+    sourceCsvEscape: "",
+    sourceCsvSeparate: "\,",
+    sourceCsvTerminate: "\\n,\\r\\n",
+    NoSourceCsvSeparator: false,
+    overwrite: false,
+    replicate: false,
+    nosplit: false,
+    noCommon: true,
+    compress: false,
+    failIfNoSourceFile: false,
+    recordStructurePresent: false,
+    quotedTerminator: false,
+    delayedReplication: true,
+    expireDays: ""
+};
+
+interface DelimitedImportFormProps {
+    formMinWidth?: number;
+    showForm: boolean;
+    selection: object[];
+    setShowForm: (_: boolean) => void;
+}
+
+export const DelimitedImportForm: React.FunctionComponent<DelimitedImportFormProps> = ({
+    formMinWidth = 300,
+    showForm,
+    selection,
+    setShowForm
+}) => {
+
+    const { handleSubmit, control, reset } = useForm<DelimitedImportFormValues>({ defaultValues });
+
+    const closeForm = React.useCallback(() => {
+        setShowForm(false);
+        reset(defaultValues);
+    }, [reset, setShowForm]);
+
+    const onSubmit = React.useCallback(() => {
+        handleSubmit(
+            (data, evt) => {
+                let request = {};
+                const files = data.selectedFiles;
+
+                delete data.selectedFiles;
+
+                files.forEach(file => {
+                    request = data;
+                    request["sourceIP"] = file.SourceIP;
+                    request["sourcePath"] = file.SourceFile;
+                    request["destLogicalName"] = data.namePrefix + ((
+                            data.namePrefix && data.namePrefix.substr(-2) !== "::" &&
+                            file.TargetName && file.TargetName.substr(0, 2) !== "::"
+                        ) ? "::" : "") + file.TargetName;
+                    FileSpray.SprayVariable({
+                        request: request
+                    }).then((response) => {
+                        if (response.SprayResponse?.wuid) {
+                            pushUrl(`/dfuworkunits/${response.SprayResponse.wuid}`);
+                        }
+                    });
+                });
+            },
+            err => {
+                console.log(err);
+            }
+        )();
+    }, [handleSubmit]);
+
+    const titleId = useId("title");
+
+    const dragOptions: IDragOptions = {
+        moveMenuItemText: nlsHPCC.Move,
+        closeMenuItemText: nlsHPCC.Close,
+        menu: ContextualMenu,
+    };
+
+    const componentStyles = mergeStyleSets(
+        FormStyles.componentStyles,
+        {
+            container: {
+                minWidth: formMinWidth ? formMinWidth : 300,
+            }
+        }
+    );
+
+    React.useEffect(() => {
+        if (selection) {
+            const newValues = defaultValues;
+            newValues.selectedFiles = [];
+            selection.forEach((file, idx) => {
+                newValues.selectedFiles[idx] = {
+                    TargetName: file["name"],
+                    SourceFile: file["fullPath"],
+                    SourceIP: file["NetAddress"]
+                };
+            });
+            reset(newValues);
+        }
+    }, [selection, reset]);
+
+    return <Modal
+        titleAriaId={titleId}
+        isOpen={showForm}
+        onDismiss={closeForm}
+        isBlocking={false}
+        containerClassName={componentStyles.container}
+        dragOptions={dragOptions}
+    >
+        <div className={componentStyles.header}>
+            <span id={titleId}>{`${nlsHPCC.Import} ${nlsHPCC.Delimited}`}</span>
+            <IconButton
+                styles={FormStyles.iconButtonStyles}
+                iconProps={FormStyles.cancelIcon}
+                ariaLabel={nlsHPCC.CloseModal}
+                onClick={closeForm}
+            />
+        </div>
+        <div className={componentStyles.body}>
+            <Stack>
+                <Controller
+                    control={control} name="destGroup"
+                    render={({
+                        field: { onChange, name: fieldName, value },
+                        fieldState: { error }
+                    }) => <TargetGroupTextField
+                        key="destGroup"
+                        label={nlsHPCC.Group}
+                        required={true}
+                        optional={true}
+                        selectedKey={value}
+                        placeholder={nlsHPCC.SelectValue}
+                        onChange={(evt, option) => {
+                            onChange(option.key);
+                        }}
+                        errorMessage={ error && error?.message }
+                    /> }
+                    rules={{
+                        required: `${nlsHPCC.SelectA} ${nlsHPCC.Group}`
+                    }}
+                />
+                <Controller
+                    control={control} name="DFUServerQueue"
+                    render={({
+                        field: { onChange, name: fieldName, value },
+                        fieldState: { error }
+                    }) => <TargetDfuSprayQueueTextField
+                        key="DFUServerQueue"
+                        label={nlsHPCC.Queue}
+                        required={true}
+                        optional={true}
+                        selectedKey={value}
+                        placeholder={nlsHPCC.SelectValue}
+                        onChange={(evt, option) => {
+                            onChange(option.key);
+                        }}
+                        errorMessage={ error && error?.message }
+                    /> }
+                    rules={{
+                        required: `${nlsHPCC.SelectA} ${nlsHPCC.Queue}`
+                    }}
+                />
+                <Controller
+                    control={control} name="namePrefix"
+                    render={({
+                        field: { onChange, name: fieldName, value },
+                        fieldState: { error }
+                    }) => <TextField
+                        name={fieldName}
+                        onChange={onChange}
+                        label={nlsHPCC.TargetScope}
+                        value={value}
+                        placeholder={nlsHPCC.NamePrefixPlaceholder}
+                        errorMessage={ error && error?.message }
+                    /> }
+                    rules={{
+                        pattern: {
+                            value: /^([a-z0-9]+(::)?)+$/i,
+                            message: nlsHPCC.ValidationErrorNamePrefix
+                        }
+                    }}
+                />
+            </Stack>
+            <Stack>
+                <table className={`${componentStyles.twoColumnTable} ${componentStyles.selectionTable}`}>
+                    <thead>
+                        <tr>
+                            <th>{nlsHPCC.TargetName}</th>
+                        </tr>
+                    </thead>
+                    <tbody>
+                    { selection && selection.map((file, idx) => {
+                        return <tr key={`File-${idx}`}>
+                            <td>
+                                <Controller
+                                    control={control} name={`selectedFiles.${idx}.TargetName` as const}
+                                    render={({
+                                        field: { onChange, name: fieldName, value },
+                                        fieldState: { error }
+                                    }) => <TextField
+                                        name={fieldName}
+                                        onChange={onChange}
+                                        value={value}
+                                        errorMessage={ error && error?.message }
+                                    /> }
+                                    rules={{
+                                        required: nlsHPCC.ValidationErrorTargetNameRequired,
+                                        pattern: {
+                                            value: /^([a-z0-9]+[-a-z0-9 \._]+)+$/i,
+                                            message: nlsHPCC.ValidationErrorTargetNameInvalid
+                                        }
+                                    }}
+                                />
+                                <input type="hidden" name={`selectedFiles.${idx}.SourceFile` as const} value={file["fullPath"]} />
+                                <input type="hidden" name={`selectedFiles.${idx}.SourceIP` as const} value={file["NetAddress"]} />
+                            </td>
+                        </tr>;
+                    }) }
+                    </tbody>
+                </table>
+            </Stack>
+            <Stack>
+                <table><tbody>
+                    <tr>
+                        <td><Controller
+                            control={control} name="sourceFormat"
+                            render={({
+                                field: { onChange, name: fieldName, value },
+                                fieldState: { error }
+                            }) => <Dropdown
+                                key="sourceFormat"
+                                label={nlsHPCC.Format}
+                                options={[
+                                    { key: "1", text: "ASCII" },
+                                    { key: "2", text: "UTF-8" },
+                                    { key: "3", text: "UTF-8N" },
+                                    { key: "4", text: "UTF-16" },
+                                    { key: "5", text: "UTF-16LE" },
+                                    { key: "6", text: "UTF-16BE" },
+                                    { key: "7", text: "UTF-32" },
+                                    { key: "8", text: "UTF-32LE" },
+                                    { key: "9", text: "UTF-32BE" }
+                                ]}
+                                defaultSelectedKey="1"
+                                onChange={(evt, option) => {
+                                    onChange(option.key);
+                                }}
+                                errorMessage={ error && error?.message }
+                            /> }
+                            rules={{
+                                required: `${nlsHPCC.SelectA} ${nlsHPCC.Format}`
+                            }}
+                        /></td>
+                        <td><Controller
+                            control={control} name="sourceMaxRecordSize"
+                            render={({
+                                field: { onChange, name: fieldName, value },
+                                fieldState: { error }
+                            }) => <TextField
+                                name={fieldName}
+                                onChange={onChange}
+                                label={nlsHPCC.MaxRecordLength}
+                                value={value}
+                                placeholder="8192"
+                                errorMessage={ error && error?.message }
+                            /> }
+                        /></td>
+                    </tr>
+                    <tr>
+                        <td><Controller
+                            control={control} name="sourceCsvQuote"
+                            render={({
+                                field: { onChange, name: fieldName, value },
+                                fieldState: { error }
+                            }) => <TextField
+                                name={fieldName}
+                                onChange={onChange}
+                                label={nlsHPCC.Quote}
+                                value={value}
+                                placeholder="'"
+                                errorMessage={ error && error?.message }
+                            /> }
+                        /></td>
+                        <td><Controller
+                            control={control} name="sourceCsvEscape"
+                            render={({
+                                field: { onChange, name: fieldName, value },
+                                fieldState: { error }
+                            }) => <TextField
+                                name={fieldName}
+                                onChange={onChange}
+                                label={nlsHPCC.Escape}
+                                value={value}
+                                errorMessage={ error && error?.message }
+                            /> }
+                        /></td>
+                    </tr>
+                    <tr>
+                        <td><Controller
+                            control={control} name="sourceCsvSeparate" defaultValue="\\,"
+                            render={({
+                                field: { onChange, name: fieldName, value },
+                                fieldState: { error }
+                            }) => <TextField
+                                name={fieldName}
+                                onChange={onChange}
+                                label={nlsHPCC.Separators}
+                                value={value}
+                                placeholder={nlsHPCC.NamePrefixPlaceholder}
+                                errorMessage={ error && error?.message }
+                            /> }
+                        /></td>
+                        <td><Controller
+                            control={control} name="sourceCsvTerminate" defaultValue="\\n,\\r\\n"
+                            render={({
+                                field: { onChange, name: fieldName, value },
+                                fieldState: { error }
+                            }) => <TextField
+                                name={fieldName}
+                                onChange={onChange}
+                                label={nlsHPCC.LineTerminators}
+                                value={value}
+                                placeholder="\\n,\\r\\n"
+                                errorMessage={ error && error?.message }
+                            /> }
+                        /></td>
+                    </tr>
+                </tbody></table>
+            </Stack>
+            <Stack>
+                <table className={componentStyles.twoColumnTable}>
+                    <tbody><tr>
+                        <td><Controller
+                            control={control} name="overwrite"
+                            render={({
+                                field : { onChange, name: fieldName, value }
+                            }) => <Checkbox name={fieldName} checked={value} onChange={onChange} label={nlsHPCC.Overwrite} /> }
+                        /></td>
+                        <td><Controller
+                            control={control} name="replicate"
+                            render={({
+                                field : { onChange, name: fieldName, value }
+                            }) => <Checkbox name={fieldName} checked={value} onChange={onChange} label={nlsHPCC.Replicate} /> }
+                        /></td>
+                    </tr>
+                    <tr>
+                        <td><Controller
+                            control={control} name="nosplit"
+                            render={({
+                                field : { onChange, name: fieldName, value }
+                            }) => <Checkbox name={fieldName} checked={value} onChange={onChange} label={nlsHPCC.NoSplit} /> }
+                        /></td>
+                        <td><Controller
+                            control={control} name="noCommon"
+                            render={({
+                                field : { onChange, name: fieldName, value }
+                            }) => <Checkbox name={fieldName} checked={value} onChange={onChange} label={nlsHPCC.NoCommon} /> }
+                        /></td>
+                    </tr>
+                    <tr>
+                        <td><Controller
+                            control={control} name="compress"
+                            render={({
+                                field : { onChange, name: fieldName, value }
+                            }) => <Checkbox name={fieldName} checked={value} onChange={onChange} label={nlsHPCC.Compress} /> }
+                        /></td>
+                        <td><Controller
+                            control={control} name="failIfNoSourceFile"
+                            render={({
+                                field : { onChange, name: fieldName, value }
+                            }) => <Checkbox name={fieldName} checked={value} onChange={onChange} label={nlsHPCC.FailIfNoSourceFile} /> }
+                        /></td>
+                    </tr>
+                    <tr>
+                        <td><Controller
+                            control={control} name="expireDays"
+                            render={({
+                                field: { onChange, name: fieldName, value },
+                                fieldState: { error }
+                            }) => <TextField
+                                name={fieldName}
+                                onChange={onChange}
+                                label={nlsHPCC.ExpireDays}
+                                value={value}
+                                errorMessage={ error && error?.message }
+                            />}
+                            rules={{
+                                min: {
+                                    value: 1,
+                                    message: nlsHPCC.ValidationErrorExpireDaysMinimum
+                                }
+                            }}
+                        /></td>
+                        <td><Controller
+                            control={control} name="delayedReplication"
+                            render={({
+                                field : { onChange, name: fieldName, value }
+                            }) => <Checkbox name={fieldName} checked={value} onChange={onChange} label={nlsHPCC.DelayedReplication} disabled={true} /> }
+                        /></td>
+                    </tr></tbody>
+                </table>
+            </Stack>
+            <Stack horizontal horizontalAlign="space-between" verticalAlign="end" styles={FormStyles.buttonStackStyles}>
+                <PrimaryButton text={nlsHPCC.Import} onClick={handleSubmit(onSubmit)} />
+            </Stack>
+        </div>
+    </Modal>;
+};

+ 292 - 0
esp/src/src-react/components/forms/landing-zone/FileListForm.tsx

@@ -0,0 +1,292 @@
+import * as React from "react";
+import { Checkbox, ContextualMenu, IconButton, IDragOptions, keyframes, mergeStyleSets, Modal, PrimaryButton, Stack } from "@fluentui/react";
+import { useId } from "@fluentui/react-hooks";
+import { useForm, Controller } from "react-hook-form";
+import { TargetDropzoneTextField, TargetFolderTextField, TargetServerTextField } from "../Fields";
+import * as FileSpray from "src/FileSpray";
+import nlsHPCC from "src/nlsHPCC";
+import * as FormStyles from "./styles";
+import { ProgressRingDotsIcon } from "@fluentui/react-icons-mdl2";
+
+interface FileListFormValues {
+    dropzone: string;
+    machines: string;
+    path: string;
+    selectedFiles?: {
+        TargetName: string,
+        RecordSize: string,
+        SourceFile: string,
+        SourceIP: string,
+    }[],
+    overwrite: boolean,
+}
+
+const defaultValues: FileListFormValues = {
+    dropzone: "",
+    machines: "",
+    path: "",
+    overwrite: false,
+};
+interface FileListFormProps {
+    formMinWidth?: number;
+    showForm: boolean;
+    selection: object[];
+    setShowForm: (_: boolean) => void;
+    onSubmit?: (_: void) => void;
+}
+
+export const FileListForm: React.FunctionComponent<FileListFormProps> = ({
+    formMinWidth = 300,
+    showForm,
+    selection,
+    setShowForm,
+    onSubmit
+}) => {
+
+    const [machine, setMachine] = React.useState<string>("");
+    const [directory, setDirectory] = React.useState<string>("/");
+    const [pathSep, setPathSep] = React.useState<string>("/");
+    const [os, setOs] = React.useState<number>();
+
+    const [submitDisabled, setSubmitDisabled] = React.useState(false);
+
+    const { handleSubmit, control, reset } = useForm<FileListFormValues>({ defaultValues });
+
+    const closeForm = React.useCallback(() => {
+        setShowForm(false);
+    }, [setShowForm]);
+
+    const doSubmit = React.useCallback(() => {
+        const uploadFiles = (data, selection) => {
+            const formData = new FormData();
+            selection.forEach(file => {
+                formData.append("uploadedfiles[]", file);
+            });
+            const uploadUrl = "/FileSpray/UploadFile.json?" +
+                "upload_&rawxml_=1&NetAddress=" + machine + "&OS=" + os + "&Path=" + data.path;
+
+            setSubmitDisabled(true);
+
+            fetch(uploadUrl, {
+                method: "POST",
+                body: formData,
+            })
+            .then(response => response.json())
+            .then(response => {
+                setSubmitDisabled(false);
+                const DFUActionResult = response?.UploadFilesResponse?.UploadFileResults?.DFUActionResult;
+                if (DFUActionResult.filter(result => result.Result !== "Success").length > 0) {
+                    console.log("upload failed");
+                } else {
+                    closeForm();
+                    if (typeof onSubmit === "function") {
+                        onSubmit();
+                    }
+                    reset(defaultValues);
+                }
+            });
+        };
+
+        handleSubmit(
+            (data, evt) => {
+                if (data.overwrite) {
+                    uploadFiles(data, selection);
+                } else {
+                    const fileNames = selection.map(file => file["name"]);
+                    FileSpray.FileList({
+                        request: {
+                            Netaddr: machine,
+                            Path: data.path
+                        }
+                    }).then(({FileListResponse}) => {
+                        let fileName = "";
+                        FileListResponse?.files?.PhysicalFileStruct.forEach(file => {
+                            if (fileNames.indexOf(file.name) > -1) {
+                                fileName = file.name;
+                                return;
+                            }
+                        });
+                        if (fileName === "") {
+                            uploadFiles(data, selection);
+                        } else {
+                            alert(nlsHPCC.OverwriteMessage + "\n" + fileNames.join("\n"));
+                        }
+                    });
+                }
+
+            },
+            err => {
+                console.log(err);
+            }
+        )();
+    }, [closeForm, handleSubmit, machine, onSubmit, os, reset, selection]);
+
+    const titleId = useId("title");
+
+    const dragOptions: IDragOptions = {
+        moveMenuItemText: nlsHPCC.Move,
+        closeMenuItemText: nlsHPCC.Close,
+        menu: ContextualMenu,
+    };
+
+    const progressIconSpin = keyframes({
+        "0%": {
+            transform: "rotate(0deg)"
+        },
+        "50%": {
+            transform: "rotate(180deg)"
+        },
+        "100%": {
+            transform: "rotate(360deg)"
+        }
+    });
+
+    const componentStyles = mergeStyleSets(
+        FormStyles.componentStyles,
+        {
+            container: {
+                minWidth: formMinWidth ? formMinWidth : 300,
+            },
+            progressMessage: {
+                margin: "10px 10px 8px 0",
+            },
+            progressIcon: {
+                animation: `${progressIconSpin} 1.55s infinite linear`
+            },
+        }
+    );
+
+    let setDropzone = React.useCallback((dropzone: string) => { }, []);
+
+    return <Modal
+        titleAriaId={titleId}
+        isOpen={showForm}
+        onDismiss={closeForm}
+        isBlocking={false}
+        containerClassName={componentStyles.container}
+        dragOptions={dragOptions}
+    >
+        <div className={componentStyles.header}>
+            <span id={titleId}>{`${nlsHPCC.FileUploader}`}</span>
+            <IconButton
+                styles={FormStyles.iconButtonStyles}
+                iconProps={FormStyles.cancelIcon}
+                ariaLabel={nlsHPCC.CloseModal}
+                onClick={closeForm}
+            />
+        </div>
+        <div className={componentStyles.body}>
+            <Stack>
+                <Controller
+                    control={control} name="dropzone"
+                    render={({
+                        field: { onChange, name: fieldName, value },
+                        fieldState: { error }
+                    }) => <TargetDropzoneTextField
+                        key="dropzone"
+                        label={nlsHPCC.LandingZone}
+                        required={true}
+                        placeholder={nlsHPCC.SelectValue}
+                        onChange={(evt, option) => {
+                            setDirectory(option["path"] as string);
+                            if (option["path"].indexOf("\\") > -1) {
+                                setPathSep("\\");
+                            }
+                            setDropzone(option.key as string);
+                            onChange(option.key);
+                        }}
+                        errorMessage={ error && error?.message }
+                    /> }
+                    rules={{
+                        required: nlsHPCC.ValidationErrorRequired
+                    }}
+                />
+                <Controller
+                    control={control} name="machines"
+                    render={({
+                        field: { onChange, name: fieldName, value },
+                        fieldState: { error }
+                    }) => <TargetServerTextField
+                        key="machines"
+                        label={nlsHPCC.Machines}
+                        required={true}
+                        placeholder={nlsHPCC.SelectValue}
+                        onChange={(evt, option) => {
+                            setMachine(option.key as string);
+                            setOs(option["OS"] as number);
+                            onChange(option.key);
+                        }}
+                        setSetDropzone={_ => setDropzone = _}
+                        errorMessage={ error && error?.message }
+                    /> }
+                    rules={{
+                        required: nlsHPCC.ValidationErrorRequired
+                    }}
+                />
+                <Controller
+                    control={control} name="path"
+                    render={({
+                        field: { onChange, name: fieldName, value },
+                        fieldState: { error }
+                    }) => <TargetFolderTextField
+                        key="path"
+                        label={nlsHPCC.Folder}
+                        pathSepChar={pathSep}
+                        machineAddress={machine}
+                        machineDirectory={directory}
+                        machineOS={os}
+                        required={true}
+                        placeholder={nlsHPCC.SelectValue}
+                        onChange={(evt, option) => {
+                            onChange(option.key);
+                        }}
+                        errorMessage={ error && error?.message }
+                    /> }
+                    rules={{
+                        required: nlsHPCC.ValidationErrorRequired,
+                        pattern: {
+                            value: /^(\/[a-z0-9]*)+$/i,
+                            message: nlsHPCC.ValidationErrorTargetNameRequired
+                        }
+                    }}
+                />
+            </Stack>
+            <Stack>
+                <table className={`${componentStyles.twoColumnTable} ${componentStyles.selectionTable}`}>
+                    <thead>
+                        <tr>
+                            <th>#</th>
+                            <th>{nlsHPCC.Type}</th>
+                            <th>{nlsHPCC.FileName}</th>
+                            <th>{nlsHPCC.Size}</th>
+                        </tr>
+                    </thead>
+                    <tbody>
+                    { selection && selection.map((file, idx) => {
+                        return <tr key={`File-${idx}`}>
+                            <td>{idx + 1}</td>
+                            <td>{file["name"].substr(file["name"].lastIndexOf(".") + 1).toUpperCase()}</td>
+                            <td>{file["name"]}</td>
+                            <td>{`${(parseInt(file["size"], 10) / 1024).toFixed(2)} kb`}</td>
+                        </tr>;
+                    }) }
+                    </tbody>
+                </table>
+                <Controller
+                    control={control} name="overwrite"
+                    render={({
+                        field : { onChange, name: fieldName, value }
+                    }) => <Checkbox name={fieldName} checked={value} onChange={onChange} label={nlsHPCC.Overwrite} /> }
+                />
+            </Stack>
+            <Stack horizontal horizontalAlign="space-between" verticalAlign="end" styles={FormStyles.buttonStackStyles}>
+                { submitDisabled &&
+                <span className={componentStyles.progressMessage}>
+                    {nlsHPCC.Uploading}... <ProgressRingDotsIcon className={componentStyles.progressIcon} />
+                </span>
+                }
+                <PrimaryButton text={nlsHPCC.Upload} onClick={handleSubmit(doSubmit)} disabled={submitDisabled}/>
+            </Stack>
+        </div>
+    </Modal>;
+};

+ 351 - 0
esp/src/src-react/components/forms/landing-zone/FixedImportForm.tsx

@@ -0,0 +1,351 @@
+import * as React from "react";
+import { Checkbox, ContextualMenu, IconButton, IDragOptions, mergeStyleSets, Modal, PrimaryButton, Stack, TextField } from "@fluentui/react";
+import { useId } from "@fluentui/react-hooks";
+import { useForm, Controller } from "react-hook-form";
+import * as FileSpray from "src/FileSpray";
+import { TargetDfuSprayQueueTextField, TargetGroupTextField } from "../Fields";
+import nlsHPCC from "src/nlsHPCC";
+import * as FormStyles from "./styles";
+import { pushUrl } from "../../../util/history";
+
+interface FixedImportFormValues {
+    destGroup: string;
+    DFUServerQueue: string;
+    namePrefix: string;
+    selectedFiles?: {
+        TargetName: string,
+        RecordSize: string,
+        SourceFile: string,
+        SourceIP: string,
+    }[],
+    overwrite: boolean,
+    replicate: boolean,
+    nosplit: boolean,
+    noCommon: boolean,
+    compress: boolean,
+    failIfNoSourceFile: boolean,
+    delayedReplication: boolean,
+    expireDays: string;
+}
+
+const defaultValues: FixedImportFormValues = {
+    destGroup: "",
+    DFUServerQueue: "",
+    namePrefix: "",
+    overwrite: false,
+    replicate: false,
+    nosplit: false,
+    noCommon: true,
+    compress: false,
+    failIfNoSourceFile: false,
+    delayedReplication: true,
+    expireDays: "",
+};
+
+interface FixedImportFormProps {
+    formMinWidth?: number;
+    showForm: boolean;
+    selection: object[];
+    setShowForm: (_: boolean) => void;
+}
+
+export const FixedImportForm: React.FunctionComponent<FixedImportFormProps> = ({
+    formMinWidth = 300,
+    showForm,
+    selection,
+    setShowForm
+}) => {
+
+    const { handleSubmit, control, reset } = useForm<FixedImportFormValues>({ defaultValues });
+
+    const closeForm = React.useCallback(() => {
+        setShowForm(false);
+        reset(defaultValues);
+    }, [reset, setShowForm]);
+
+    const onSubmit = React.useCallback(() => {
+        handleSubmit(
+            (data, evt) => {
+                let request = {};
+                const files = data.selectedFiles;
+
+                delete data.selectedFiles;
+
+                files.forEach(file => {
+                    request = data;
+                    request["sourceIP"] = file.SourceIP;
+                    request["sourcePath"] = file.SourceFile;
+                    request["sourceRecordSize"] = file.RecordSize;
+                    request["destLogicalName"] = data.namePrefix + ((
+                            data.namePrefix && data.namePrefix.substr(-2) !== "::" &&
+                            file.TargetName && file.TargetName.substr(0, 2) !== "::"
+                        ) ? "::" : "") + file.TargetName;
+                    FileSpray.SprayFixed({
+                        request: request
+                    }).then((response) => {
+                        if (response.SprayFixedResponse?.wuid) {
+                            pushUrl(`/dfuworkunits/${response.SprayFixedResponse.wuid}`);
+                        }
+                    });
+                });
+            },
+            err => {
+                console.log(err);
+            }
+        )();
+    }, [handleSubmit]);
+
+    const titleId = useId("title");
+
+    const dragOptions: IDragOptions = {
+        moveMenuItemText: nlsHPCC.Move,
+        closeMenuItemText: nlsHPCC.Close,
+        menu: ContextualMenu,
+    };
+
+    const componentStyles = mergeStyleSets(
+        FormStyles.componentStyles,
+        {
+            container: {
+                minWidth: formMinWidth ? formMinWidth : 300,
+            },
+        }
+    );
+
+    React.useEffect(() => {
+        if (selection) {
+            const newValues = defaultValues;
+            newValues.selectedFiles = [];
+            selection.forEach((file, idx) => {
+                newValues.selectedFiles[idx] = {
+                    TargetName: file["name"],
+                    RecordSize: "",
+                    SourceFile: file["fullPath"],
+                    SourceIP: file["NetAddress"]
+                };
+            });
+            reset(newValues);
+        }
+    }, [selection, reset]);
+
+    return <Modal
+        titleAriaId={titleId}
+        isOpen={showForm}
+        onDismiss={closeForm}
+        isBlocking={false}
+        containerClassName={componentStyles.container}
+        dragOptions={dragOptions}
+    >
+        <div className={componentStyles.header}>
+            <span id={titleId}>{`${nlsHPCC.Import} ${nlsHPCC.Fixed} ${nlsHPCC.Length}`}</span>
+            <IconButton
+                styles={FormStyles.iconButtonStyles}
+                iconProps={FormStyles.cancelIcon}
+                ariaLabel={nlsHPCC.CloseModal}
+                onClick={closeForm}
+            />
+        </div>
+        <div className={componentStyles.body}>
+            <Stack>
+                <Controller
+                    control={control} name="destGroup"
+                    render={({
+                        field: { onChange, name: fieldName, value },
+                        fieldState: { error }
+                    }) => <TargetGroupTextField
+                        key="destGroup"
+                        label={nlsHPCC.Group}
+                        required={true}
+                        optional={true}
+                        selectedKey={value}
+                        placeholder={nlsHPCC.SelectValue}
+                        onChange={(evt, option) => {
+                            onChange(option.key);
+                        }}
+                        errorMessage={ error && error?.message }
+                    /> }
+                    rules={{
+                        required: `${nlsHPCC.SelectA} ${nlsHPCC.Group}`
+                    }}
+                />
+                <Controller
+                    control={control} name="DFUServerQueue"
+                    render={({
+                        field: { onChange, name: fieldName, value },
+                        fieldState: { error }
+                    }) => <TargetDfuSprayQueueTextField
+                        key="DFUServerQueue"
+                        label={nlsHPCC.Queue}
+                        required={true}
+                        optional={true}
+                        selectedKey={value}
+                        placeholder={nlsHPCC.SelectValue}
+                        onChange={(evt, option) => {
+                            onChange(option.key);
+                        }}
+                        errorMessage={ error && error?.message }
+                    /> }
+                    rules={{
+                        required: `${nlsHPCC.SelectA} ${nlsHPCC.Queue}`
+                    }}
+                />
+                <Controller
+                    control={control} name="namePrefix"
+                    render={({
+                        field: { onChange, name: fieldName, value },
+                        fieldState: { error }
+                    }) => <TextField
+                        name={fieldName}
+                        onChange={onChange}
+                        label={nlsHPCC.TargetScope}
+                        value={value}
+                        placeholder={nlsHPCC.NamePrefixPlaceholder}
+                        errorMessage={ error && error?.message }
+                    /> }
+                    rules={{
+                        pattern: {
+                            value: /^([a-z0-9]+(::)?)+$/i,
+                            message: nlsHPCC.ValidationErrorNamePrefix
+                        }
+                    }}
+                />
+            </Stack>
+            <Stack>
+                <table className={`${componentStyles.twoColumnTable} ${componentStyles.selectionTable}`}>
+                    <thead>
+                        <tr>
+                            <th>{nlsHPCC.TargetName}</th>
+                            <th>{nlsHPCC.RecordLength}</th>
+                        </tr>
+                    </thead>
+                    <tbody>
+                    { selection && selection.map((file, idx) => {
+                        return <tr key={`File-${idx}`}>
+                            <td><Controller
+                                control={control} name={`selectedFiles.${idx}.TargetName` as const}
+                                render={({
+                                    field: { onChange, name: fieldName, value },
+                                    fieldState: { error }
+                                }) => <TextField
+                                    name={fieldName}
+                                    onChange={onChange}
+                                    required
+                                    value={value}
+                                    errorMessage={ error && error?.message }
+                                /> }
+                                rules={{
+                                    required: nlsHPCC.ValidationErrorTargetNameRequired,
+                                    pattern: {
+                                        value: /^([a-z0-9]+[-a-z0-9 \._]+)+$/i,
+                                        message: nlsHPCC.ValidationErrorTargetNameInvalid
+                                    }
+                                }}
+                            /></td>
+                            <td>
+                                <Controller
+                                    control={control} name={`selectedFiles.${idx}.RecordSize` as const}
+                                    render={({
+                                        field: { onChange, name: fieldName, value },
+                                        fieldState: { error }
+                                    }) => <TextField
+                                        name={fieldName}
+                                        onChange={onChange}
+                                        value={value}
+                                        placeholder={nlsHPCC.RequiredForFixedSpray}
+                                        errorMessage={ error && error?.message }
+                                    /> }
+                                    rules={{
+                                        required: nlsHPCC.ValidationErrorRecordSizeRequired,
+                                        pattern: {
+                                            value: /^[0-9]+$/i,
+                                            message: nlsHPCC.ValidationErrorRecordSizeNumeric
+                                        }
+                                    }}
+                                />
+                                <input type="hidden" name={`selectedFiles.${idx}.SourceFile` as const} value={file["fullPath"]} />
+                                <input type="hidden" name={`selectedFiles.${idx}.SourceIP` as const} value={file["NetAddress"]} />
+                            </td>
+                        </tr>;
+                    }) }
+                    </tbody>
+                </table>
+            </Stack>
+            <Stack>
+                <table className={componentStyles.twoColumnTable}>
+                    <tbody><tr>
+                        <td><Controller
+                            control={control} name="overwrite"
+                            render={({
+                                field : { onChange, name: fieldName, value }
+                            }) => <Checkbox name={fieldName} checked={value} onChange={onChange} label={nlsHPCC.Overwrite} /> }
+                        /></td>
+                        <td><Controller
+                            control={control} name="replicate"
+                            render={({
+                                field : { onChange, name: fieldName, value }
+                            }) => <Checkbox name={fieldName} checked={value} onChange={onChange} label={nlsHPCC.Replicate} /> }
+                        /></td>
+                    </tr>
+                    <tr>
+                        <td><Controller
+                            control={control} name="nosplit"
+                            render={({
+                                field : { onChange, name: fieldName, value }
+                            }) => <Checkbox name={fieldName} checked={value} onChange={onChange} label={nlsHPCC.NoSplit} /> }
+                        /></td>
+                        <td><Controller
+                            control={control} name="noCommon"
+                            render={({
+                                field : { onChange, name: fieldName, value }
+                            }) => <Checkbox name={fieldName} checked={value} onChange={onChange} label={nlsHPCC.NoCommon} /> }
+                        /></td>
+                    </tr>
+                    <tr>
+                        <td><Controller
+                            control={control} name="compress"
+                            render={({
+                                field : { onChange, name: fieldName, value }
+                            }) => <Checkbox name={fieldName} checked={value} onChange={onChange} label={nlsHPCC.Compress} /> }
+                        /></td>
+                        <td><Controller
+                            control={control} name="failIfNoSourceFile"
+                            render={({
+                                field : { onChange, name: fieldName, value }
+                            }) => <Checkbox name={fieldName} checked={value} onChange={onChange} label={nlsHPCC.FailIfNoSourceFile} /> }
+                        /></td>
+                    </tr>
+                    <tr>
+                        <td><Controller
+                            control={control} name="expireDays"
+                            render={({
+                                field: { onChange, name: fieldName, value },
+                                fieldState: { error }
+                            }) => <TextField
+                                name={fieldName}
+                                onChange={onChange}
+                                label={nlsHPCC.ExpireDays}
+                                value={value}
+                                errorMessage={ error && error?.message }
+                            />}
+                            rules={{
+                                min: {
+                                    value: 1,
+                                    message: nlsHPCC.ValidationErrorExpireDaysMinimum
+                                }
+                            }}
+                        /></td>
+                        <td><Controller
+                            control={control} name="delayedReplication"
+                            render={({
+                                field : { onChange, name: fieldName, value }
+                            }) => <Checkbox name={fieldName} checked={value} onChange={onChange} label={nlsHPCC.DelayedReplication} disabled={true} /> }
+                        /></td>
+                    </tr></tbody>
+                </table>
+            </Stack>
+            <Stack horizontal horizontalAlign="space-between" verticalAlign="end" styles={FormStyles.buttonStackStyles}>
+                <PrimaryButton text={nlsHPCC.Import} onClick={handleSubmit(onSubmit)} />
+            </Stack>
+        </div>
+    </Modal>;
+};

+ 395 - 0
esp/src/src-react/components/forms/landing-zone/JsonImportForm.tsx

@@ -0,0 +1,395 @@
+import * as React from "react";
+import { Checkbox, ContextualMenu, Dropdown, IconButton, IDragOptions, mergeStyleSets, Modal, PrimaryButton, Stack, TextField } from "@fluentui/react";
+import { useId } from "@fluentui/react-hooks";
+import { useForm, Controller } from "react-hook-form";
+import * as FileSpray from "src/FileSpray";
+import { TargetDfuSprayQueueTextField, TargetGroupTextField } from "../Fields";
+import nlsHPCC from "src/nlsHPCC";
+import * as FormStyles from "./styles";
+import { pushUrl } from "../../../util/history";
+
+interface JsonImportFormValues {
+    destGroup: string;
+    DFUServerQueue: string;
+    namePrefix: string;
+    selectedFiles?: {
+        TargetName: string,
+        TargetRowPath: string,
+        SourceFile: string,
+        SourceIP: string
+    }[],
+    sourceFormat: string;
+    sourceMaxRecordSize: string;
+    overwrite: boolean;
+    replicate: boolean;
+    nosplit: boolean;
+    noCommon: boolean;
+    compress: boolean;
+    failIfNoSourceFile: boolean;
+    delayedReplication: boolean;
+    expireDays: string;
+}
+
+const defaultValues: JsonImportFormValues = {
+    destGroup: "",
+    DFUServerQueue: "",
+    namePrefix: "",
+    sourceFormat: "1",
+    sourceMaxRecordSize: "",
+    overwrite: false,
+    replicate: false,
+    nosplit: false,
+    noCommon: true,
+    compress: false,
+    failIfNoSourceFile: false,
+    delayedReplication: true,
+    expireDays: ""
+};
+
+interface JsonImportFormProps {
+    formMinWidth?: number;
+    showForm: boolean;
+    selection: object[];
+    setShowForm: (_: boolean) => void;
+}
+
+export const JsonImportForm: React.FunctionComponent<JsonImportFormProps> = ({
+    formMinWidth = 300,
+    showForm,
+    selection,
+    setShowForm
+}) => {
+
+    const { handleSubmit, control, reset } = useForm<JsonImportFormValues>({ defaultValues });
+
+    const closeForm = React.useCallback(() => {
+        setShowForm(false);
+    }, [setShowForm]);
+
+    const onSubmit = React.useCallback(() => {
+        handleSubmit(
+            (data, evt) => {
+                let request = {};
+                const files = data.selectedFiles;
+
+                delete data.selectedFiles;
+
+                files.forEach(file => {
+                    request = data;
+                    request["sourceIP"] = file.SourceIP;
+                    request["sourcePath"] = file.SourceFile;
+                    request["isJSON"] = true;
+                    request["destLogicalName"] = data.namePrefix + ((
+                            data.namePrefix && data.namePrefix.substr(-2) !== "::" &&
+                            file.TargetName && file.TargetName.substr(0, 2) !== "::"
+                        ) ? "::" : "") + file.TargetName;
+                    request["sourceRowTag"] = file.TargetRowPath;
+                    FileSpray.SprayVariable({
+                        request: request
+                    }).then((response) => {
+                        if (response.SprayResponse?.wuid) {
+                            pushUrl(`/dfuworkunits/${response.SprayResponse.wuid}`);
+                        }
+                    });
+                });
+            },
+            err => {
+                console.log(err);
+            }
+        )();
+    }, [handleSubmit]);
+
+    const titleId = useId("title");
+
+    const dragOptions: IDragOptions = {
+        moveMenuItemText: nlsHPCC.Move,
+        closeMenuItemText: nlsHPCC.Close,
+        menu: ContextualMenu,
+    };
+
+    const componentStyles = mergeStyleSets(
+        FormStyles.componentStyles,
+        {
+            container: {
+                minWidth: formMinWidth ? formMinWidth : 300,
+            }
+        }
+    );
+
+    React.useEffect(() => {
+        if (selection) {
+            const newValues = defaultValues;
+            newValues.selectedFiles = [];
+            selection.forEach((file, idx) => {
+                newValues.selectedFiles[idx] = {
+                    TargetName: file["name"],
+                    TargetRowPath: "/",
+                    SourceFile: file["fullPath"],
+                    SourceIP: file["NetAddress"]
+                };
+            });
+            reset(newValues);
+        }
+    }, [selection, reset]);
+
+    return <Modal
+        titleAriaId={titleId}
+        isOpen={showForm}
+        onDismiss={closeForm}
+        isBlocking={false}
+        containerClassName={componentStyles.container}
+        dragOptions={dragOptions}
+    >
+        <div className={componentStyles.header}>
+            <span id={titleId}>{`${nlsHPCC.Import} ${nlsHPCC.JSON}`}</span>
+            <IconButton
+                styles={FormStyles.iconButtonStyles}
+                iconProps={FormStyles.cancelIcon}
+                ariaLabel={nlsHPCC.CloseModal}
+                onClick={closeForm}
+            />
+        </div>
+        <div className={componentStyles.body}>
+            <Stack>
+                <Controller
+                    control={control} name="destGroup"
+                    render={({
+                        field: { onChange, name: fieldName, value },
+                        fieldState: { error }
+                    }) => <TargetGroupTextField
+                        key="destGroup"
+                        label={nlsHPCC.Group}
+                        required={true}
+                        optional={true}
+                        selectedKey={value}
+                        placeholder={nlsHPCC.SelectValue}
+                        onChange={(evt, option) => {
+                            onChange(option.key);
+                        }}
+                        errorMessage={ error && error?.message }
+                    /> }
+                    rules={{
+                        required: `${nlsHPCC.SelectA} ${nlsHPCC.Group}`
+                    }}
+                />
+                <Controller
+                    control={control} name="DFUServerQueue"
+                    render={({
+                        field: { onChange, name: fieldName, value },
+                        fieldState: { error }
+                    }) => <TargetDfuSprayQueueTextField
+                        key="DFUServerQueue"
+                        label={nlsHPCC.Queue}
+                        required={true}
+                        optional={true}
+                        selectedKey={value}
+                        placeholder={nlsHPCC.SelectValue}
+                        onChange={(evt, option) => {
+                            onChange(option.key);
+                        }}
+                        errorMessage={ error && error?.message }
+                    /> }
+                    rules={{
+                        required: `${nlsHPCC.SelectA} ${nlsHPCC.Queue}`
+                    }}
+                />
+                <Controller
+                    control={control} name="namePrefix"
+                    render={({
+                        field: { onChange, name: fieldName, value },
+                        fieldState: { error }
+                    }) => <TextField
+                        name={fieldName}
+                        onChange={onChange}
+                        label={nlsHPCC.TargetScope}
+                        value={value}
+                        placeholder={nlsHPCC.NamePrefixPlaceholder}
+                        errorMessage={ error && error?.message }
+                    /> }
+                    rules={{
+                        pattern: {
+                            value: /^([a-z0-9]+(::)?)+$/i,
+                            message: nlsHPCC.ValidationErrorNamePrefix
+                        }
+                    }}
+                />
+            </Stack>
+            <Stack>
+                <table className={`${componentStyles.twoColumnTable} ${componentStyles.selectionTable}`}>
+                    <thead>
+                        <tr>
+                            <th>{nlsHPCC.TargetName}</th>
+                            <th>{nlsHPCC.RowPath}</th>
+                        </tr>
+                    </thead>
+                    <tbody>
+                    { selection && selection.map((file, idx) => {
+                        return <tr key={`File-${idx}`}>
+                            <td><Controller
+                                control={control} name={`selectedFiles.${idx}.TargetName` as const}
+                                render={({
+                                    field: { onChange, name: fieldName, value },
+                                    fieldState: { error }
+                                }) => <TextField
+                                    name={fieldName}
+                                    onChange={onChange}
+                                    value={value}
+                                    errorMessage={ error && error?.message }
+                                /> }
+                                rules={{
+                                    required: nlsHPCC.ValidationErrorTargetNameRequired,
+                                    pattern: {
+                                        value: /^([a-z0-9]+[-a-z0-9 \._]+)+$/i,
+                                        message: nlsHPCC.ValidationErrorTargetNameInvalid
+                                    }
+                                }}
+                            /></td>
+                            <td>
+                                <Controller
+                                    control={control} name={`selectedFiles.${idx}.TargetRowPath` as const}
+                                    render={({
+                                        field: { onChange, name: fieldName, value },
+                                        fieldState: { error }
+                                    }) => <TextField
+                                        name={fieldName}
+                                        onChange={onChange}
+                                        value={value}
+                                        errorMessage={ error && error?.message }
+                                    /> }
+                                />
+                                <input type="hidden" name={`selectedFiles.${idx}.SourceFile` as const} value={file["fullPath"]} />
+                                <input type="hidden" name={`selectedFiles.${idx}.SourceIP` as const} value={file["NetAddress"]} />
+                            </td>
+                        </tr>;
+                    }) }
+                    </tbody>
+                </table>
+            </Stack>
+            <Stack>
+                <table><tbody>
+                    <tr>
+                        <td><Controller
+                            control={control} name="sourceFormat"
+                            render={({
+                                field: { onChange, name: fieldName, value },
+                                fieldState: { error }
+                            }) => <Dropdown
+                                key="sourceFormat"
+                                label={nlsHPCC.Format}
+                                options={[
+                                    { key: "1", text: "ASCII" },
+                                    { key: "2", text: "UTF-8" },
+                                    { key: "3", text: "UTF-8N" },
+                                    { key: "4", text: "UTF-16" },
+                                    { key: "5", text: "UTF-16LE" },
+                                    { key: "6", text: "UTF-16BE" },
+                                    { key: "7", text: "UTF-32" },
+                                    { key: "8", text: "UTF-32LE" },
+                                    { key: "9", text: "UTF-32BE" }
+                                ]}
+                                defaultSelectedKey="1"
+                                onChange={(evt, option) => {
+                                    onChange(option.key);
+                                }}
+                                errorMessage={ error && error?.message }
+                            /> }
+                            rules={{
+                                required: `${nlsHPCC.SelectA} ${nlsHPCC.Format}`
+                            }}
+                        /></td>
+                        <td><Controller
+                            control={control} name="sourceMaxRecordSize"
+                            render={({
+                                field: { onChange, name: fieldName, value },
+                                fieldState: { error }
+                            }) => <TextField
+                                name={fieldName}
+                                onChange={onChange}
+                                label={nlsHPCC.MaxRecordLength}
+                                value={value}
+                                placeholder="8192"
+                                errorMessage={ error && error?.message }
+                            /> }
+                        /></td>
+                    </tr>
+                </tbody></table>
+            </Stack>
+            <Stack>
+                <table className={componentStyles.twoColumnTable}>
+                    <tbody><tr>
+                        <td><Controller
+                            control={control} name="overwrite"
+                            render={({
+                                field : { onChange, name: fieldName, value }
+                            }) => <Checkbox name={fieldName} checked={value} onChange={onChange} label={nlsHPCC.Overwrite} /> }
+                        /></td>
+                        <td><Controller
+                            control={control} name="replicate"
+                            render={({
+                                field : { onChange, name: fieldName, value }
+                            }) => <Checkbox name={fieldName} checked={value} onChange={onChange} label={nlsHPCC.Replicate} /> }
+                        /></td>
+                    </tr>
+                    <tr>
+                        <td><Controller
+                            control={control} name="nosplit"
+                            render={({
+                                field : { onChange, name: fieldName, value }
+                            }) => <Checkbox name={fieldName} checked={value} onChange={onChange} label={nlsHPCC.NoSplit} /> }
+                        /></td>
+                        <td><Controller
+                            control={control} name="noCommon"
+                            render={({
+                                field : { onChange, name: fieldName, value }
+                            }) => <Checkbox name={fieldName} checked={value} onChange={onChange} label={nlsHPCC.NoCommon} /> }
+                        /></td>
+                    </tr>
+                    <tr>
+                        <td><Controller
+                            control={control} name="compress"
+                            render={({
+                                field : { onChange, name: fieldName, value }
+                            }) => <Checkbox name={fieldName} checked={value} onChange={onChange} label={nlsHPCC.Compress} /> }
+                        /></td>
+                        <td><Controller
+                            control={control} name="failIfNoSourceFile"
+                            render={({
+                                field : { onChange, name: fieldName, value }
+                            }) => <Checkbox name={fieldName} checked={value} onChange={onChange} label={nlsHPCC.FailIfNoSourceFile} /> }
+                        /></td>
+                    </tr>
+                    <tr>
+                        <td><Controller
+                            control={control} name="expireDays"
+                            render={({
+                                field: { onChange, name: fieldName, value },
+                                fieldState: { error }
+                            }) => <TextField
+                                name={fieldName}
+                                onChange={onChange}
+                                label={nlsHPCC.ExpireDays}
+                                value={value}
+                                errorMessage={ error && error?.message }
+                            />}
+                            rules={{
+                                min: {
+                                    value: 1,
+                                    message: nlsHPCC.ValidationErrorExpireDaysMinimum
+                                }
+                            }}
+                        /></td>
+                        <td><Controller
+                            control={control} name="delayedReplication"
+                            render={({
+                                field : { onChange, name: fieldName, value }
+                            }) => <Checkbox name={fieldName} checked={value} onChange={onChange} label={nlsHPCC.DelayedReplication} disabled={true} /> }
+                        /></td>
+                    </tr></tbody>
+                </table>
+            </Stack>
+            <Stack horizontal horizontalAlign="space-between" verticalAlign="end" styles={FormStyles.buttonStackStyles}>
+                <PrimaryButton text={nlsHPCC.Import} onClick={handleSubmit(onSubmit)} />
+            </Stack>
+        </div>
+    </Modal>;
+};

+ 353 - 0
esp/src/src-react/components/forms/landing-zone/VariableImportForm.tsx

@@ -0,0 +1,353 @@
+import * as React from "react";
+import { Checkbox, ContextualMenu, Dropdown, IconButton, IDragOptions, mergeStyleSets, Modal, PrimaryButton, Stack, TextField } from "@fluentui/react";
+import { useId } from "@fluentui/react-hooks";
+import { useForm, Controller } from "react-hook-form";
+import * as FileSpray from "src/FileSpray";
+import { TargetDfuSprayQueueTextField, TargetGroupTextField } from "../Fields";
+import nlsHPCC from "src/nlsHPCC";
+import * as FormStyles from "./styles";
+import { pushUrl } from "../../../util/history";
+
+interface VariableImportFormValues {
+    destGroup: string;
+    DFUServerQueue: string;
+    namePrefix: string;
+    selectedFiles?: {
+        TargetName: string,
+        SourceFile: string,
+        SourceIP: string
+    }[],
+    sourceFormat: string;
+    overwrite: boolean;
+    replicate: boolean;
+    nosplit: boolean;
+    noCommon: boolean;
+    compress: boolean;
+    failIfNoSourceFile: boolean;
+    delayedReplication: boolean;
+    expireDays: string;
+}
+
+const defaultValues: VariableImportFormValues = {
+    destGroup: "",
+    DFUServerQueue: "",
+    namePrefix: "",
+    sourceFormat: "recfmv",
+    overwrite: false,
+    replicate: false,
+    nosplit: false,
+    noCommon: true,
+    compress: false,
+    failIfNoSourceFile: false,
+    delayedReplication: true,
+    expireDays: ""
+};
+
+interface VariableImportFormProps {
+    formMinWidth?: number;
+    showForm: boolean;
+    selection: object[];
+    setShowForm: (_: boolean) => void;
+}
+
+export const VariableImportForm: React.FunctionComponent<VariableImportFormProps> = ({
+    formMinWidth = 300,
+    showForm,
+    selection,
+    setShowForm
+}) => {
+
+    const { handleSubmit, control, reset } = useForm<VariableImportFormValues>({ defaultValues });
+
+    const closeForm = React.useCallback(() => {
+        setShowForm(false);
+    }, [setShowForm]);
+
+    const onSubmit = React.useCallback(() => {
+        handleSubmit(
+            (data, evt) => {
+                let request = {};
+                const files = data.selectedFiles;
+
+                delete data.selectedFiles;
+
+                files.forEach(file => {
+                    request = data;
+                    request["sourceIP"] = file.SourceIP;
+                    request["sourcePath"] = file.SourceFile;
+                    request["destLogicalName"] = data.namePrefix + ((
+                            data.namePrefix && data.namePrefix.substr(-2) !== "::" &&
+                            file.TargetName && file.TargetName.substr(0, 2) !== "::"
+                        ) ? "::" : "") + file.TargetName;
+                    FileSpray.SprayFixed({
+                        request: request
+                    }).then((response) => {
+                        if (response.SprayFixedResponse?.wuid) {
+                            pushUrl(`/dfuworkunits/${response.SprayFixedResponse.wuid}`);
+                        }
+                    });
+                });
+            },
+            err => {
+                console.log(err);
+            }
+        )();
+    }, [handleSubmit]);
+
+    const titleId = useId("title");
+
+    const dragOptions: IDragOptions = {
+        moveMenuItemText: nlsHPCC.Move,
+        closeMenuItemText: nlsHPCC.Close,
+        menu: ContextualMenu,
+    };
+
+    const componentStyles = mergeStyleSets(
+        FormStyles.componentStyles,
+        {
+            container: {
+                minWidth: formMinWidth ? formMinWidth : 300,
+            }
+        }
+    );
+
+    React.useEffect(() => {
+        if (selection) {
+            const newValues = defaultValues;
+            newValues.selectedFiles = [];
+            selection.forEach((file, idx) => {
+                newValues.selectedFiles[idx] = {
+                    TargetName: file["name"],
+                    SourceFile: file["fullPath"],
+                    SourceIP: file["NetAddress"]
+                };
+            });
+            reset(newValues);
+        }
+    }, [selection, reset]);
+
+    return <Modal
+        titleAriaId={titleId}
+        isOpen={showForm}
+        onDismiss={closeForm}
+        isBlocking={false}
+        containerClassName={componentStyles.container}
+        dragOptions={dragOptions}
+    >
+        <div className={componentStyles.header}>
+            <span id={titleId}>{`${nlsHPCC.Import} ${nlsHPCC.Variable}`}</span>
+            <IconButton
+                styles={FormStyles.iconButtonStyles}
+                iconProps={FormStyles.cancelIcon}
+                ariaLabel={nlsHPCC.CloseModal}
+                onClick={closeForm}
+            />
+        </div>
+        <div className={componentStyles.body}>
+            <Stack>
+                <Controller
+                    control={control} name="destGroup"
+                    render={({
+                        field: { onChange, name: fieldName, value },
+                        fieldState: { error }
+                    }) => <TargetGroupTextField
+                        key="destGroup"
+                        label={nlsHPCC.Group}
+                        required={true}
+                        optional={true}
+                        selectedKey={value}
+                        placeholder={nlsHPCC.SelectValue}
+                        onChange={(evt, option) => {
+                            onChange(option.key);
+                        }}
+                        errorMessage={ error && error?.message }
+                    /> }
+                    rules={{
+                        required: `${nlsHPCC.SelectA} ${nlsHPCC.Group}`
+                    }}
+                />
+                <Controller
+                    control={control} name="DFUServerQueue"
+                    render={({
+                        field: { onChange, name: fieldName, value },
+                        fieldState: { error }
+                    }) => <TargetDfuSprayQueueTextField
+                        key="DFUServerQueue"
+                        label={nlsHPCC.Queue}
+                        required={true}
+                        optional={true}
+                        selectedKey={value}
+                        placeholder={nlsHPCC.SelectValue}
+                        onChange={(evt, option) => {
+                            onChange(option.key);
+                        }}
+                        errorMessage={ error && error?.message }
+                    /> }
+                    rules={{
+                        required: `${nlsHPCC.SelectA} ${nlsHPCC.Queue}`
+                    }}
+                />
+                <Controller
+                    control={control} name="namePrefix"
+                    render={({
+                        field: { onChange, name: fieldName, value },
+                        fieldState: { error }
+                    }) => <TextField
+                        name={fieldName}
+                        onChange={onChange}
+                        label={nlsHPCC.TargetScope}
+                        value={value}
+                        placeholder={nlsHPCC.NamePrefixPlaceholder}
+                        errorMessage={ error && error?.message }
+                    /> }
+                    rules={{
+                        pattern: {
+                            value: /^([a-z0-9]+(::)?)+$/i,
+                            message: nlsHPCC.ValidationErrorNamePrefix
+                        }
+                    }}
+                />
+            </Stack>
+            <Stack>
+                <table className={`${componentStyles.twoColumnTable} ${componentStyles.selectionTable}`}>
+                    <thead>
+                        <tr>
+                            <th>{nlsHPCC.TargetName}</th>
+                        </tr>
+                    </thead>
+                    <tbody>
+                    { selection && selection.map((file, idx) => {
+                        return <tr key={`File-${idx}`}>
+                            <td>
+                                <Controller
+                                    control={control} name={`selectedFiles.${idx}.TargetName` as const}
+                                    render={({
+                                        field: { onChange, name: fieldName, value },
+                                        fieldState: { error }
+                                    }) => <TextField
+                                        name={fieldName}
+                                        onChange={onChange}
+                                        value={value}
+                                        errorMessage={ error && error?.message }
+                                    /> }
+                                    rules={{
+                                        required: nlsHPCC.ValidationErrorTargetNameRequired,
+                                        pattern: {
+                                            value: /^([a-z0-9]+[-a-z0-9 \._]+)+$/i,
+                                            message: nlsHPCC.ValidationErrorTargetNameInvalid
+                                        }
+                                    }}
+                                />
+                                <input type="hidden" name={`selectedFiles.${idx}.SourceFile` as const} value={file["fullPath"]} />
+                                <input type="hidden" name={`selectedFiles.${idx}.SourceIP` as const} value={file["NetAddress"]} />
+                            </td>
+                        </tr>;
+                    }) }
+                    </tbody>
+                </table>
+            </Stack>
+            <Stack>
+                <Controller
+                    control={control} name="sourceFormat"
+                    render={({
+                        field: { onChange, name: fieldName, value },
+                        fieldState: { error }
+                    }) => <Dropdown
+                        key="sourceFormat"
+                        label={nlsHPCC.Format}
+                        options={[
+                            { key: "recfmv", text: "recfmv" },
+                            { key: "recfmvb", text: "recfmvb" },
+                            { key: "variable", text: nlsHPCC.Variable },
+                            { key: "variablebigendian", text: nlsHPCC.VariableBigendian },
+                        ]}
+                        defaultSelectedKey="recfmv"
+                        onChange={(evt, option) => {
+                            onChange(option.key);
+                        }}
+                        errorMessage={ error && error?.message }
+                    /> }
+                    rules={{
+                        required: `${nlsHPCC.SelectA} ${nlsHPCC.Format}`
+                    }}
+                />
+            </Stack>
+            <Stack>
+                <table className={componentStyles.twoColumnTable}>
+                    <tbody><tr>
+                        <td><Controller
+                            control={control} name="overwrite"
+                            render={({
+                                field : { onChange, name: fieldName, value }
+                            }) => <Checkbox name={fieldName} checked={value} onChange={onChange} label={nlsHPCC.Overwrite} /> }
+                        /></td>
+                        <td><Controller
+                            control={control} name="replicate"
+                            render={({
+                                field : { onChange, name: fieldName, value }
+                            }) => <Checkbox name={fieldName} checked={value} onChange={onChange} label={nlsHPCC.Replicate} /> }
+                        /></td>
+                    </tr>
+                    <tr>
+                        <td><Controller
+                            control={control} name="nosplit"
+                            render={({
+                                field : { onChange, name: fieldName, value }
+                            }) => <Checkbox name={fieldName} checked={value} onChange={onChange} label={nlsHPCC.NoSplit} /> }
+                        /></td>
+                        <td><Controller
+                            control={control} name="noCommon"
+                            render={({
+                                field : { onChange, name: fieldName, value }
+                            }) => <Checkbox name={fieldName} checked={value} onChange={onChange} label={nlsHPCC.NoCommon} /> }
+                        /></td>
+                    </tr>
+                    <tr>
+                        <td><Controller
+                            control={control} name="compress"
+                            render={({
+                                field : { onChange, name: fieldName, value }
+                            }) => <Checkbox name={fieldName} checked={value} onChange={onChange} label={nlsHPCC.Compress} /> }
+                        /></td>
+                        <td><Controller
+                            control={control} name="failIfNoSourceFile"
+                            render={({
+                                field : { onChange, name: fieldName, value }
+                            }) => <Checkbox name={fieldName} checked={value} onChange={onChange} label={nlsHPCC.FailIfNoSourceFile} /> }
+                        /></td>
+                    </tr>
+                    <tr>
+                        <td><Controller
+                            control={control} name="expireDays"
+                            render={({
+                                field: { onChange, name: fieldName, value },
+                                fieldState: { error }
+                            }) => <TextField
+                                name={fieldName}
+                                onChange={onChange}
+                                label={nlsHPCC.ExpireDays}
+                                value={value}
+                                errorMessage={ error && error?.message }
+                            />}
+                            rules={{
+                                min: {
+                                    value: 1,
+                                    message: nlsHPCC.ValidationErrorExpireDaysMinimum
+                                }
+                            }}
+                        /></td>
+                        <td><Controller
+                            control={control} name="delayedReplication"
+                            render={({
+                                field : { onChange, name: fieldName, value }
+                            }) => <Checkbox name={fieldName} checked={value} onChange={onChange} label={nlsHPCC.DelayedReplication} disabled={true} /> }
+                        /></td>
+                    </tr></tbody>
+                </table>
+            </Stack>
+            <Stack horizontal horizontalAlign="space-between" verticalAlign="end" styles={FormStyles.buttonStackStyles}>
+                <PrimaryButton text={nlsHPCC.Import} onClick={handleSubmit(onSubmit)} />
+            </Stack>
+        </div>
+    </Modal>;
+};

+ 394 - 0
esp/src/src-react/components/forms/landing-zone/XmlImportForm.tsx

@@ -0,0 +1,394 @@
+import * as React from "react";
+import { Checkbox, ContextualMenu, Dropdown, IconButton, IDragOptions, mergeStyleSets, Modal, PrimaryButton, Stack, TextField } from "@fluentui/react";
+import { useId } from "@fluentui/react-hooks";
+import { useForm, Controller } from "react-hook-form";
+import * as FileSpray from "src/FileSpray";
+import { TargetDfuSprayQueueTextField, TargetGroupTextField } from "../Fields";
+import nlsHPCC from "src/nlsHPCC";
+import * as FormStyles from "./styles";
+import { pushUrl } from "../../../util/history";
+
+interface XmlImportFormValues {
+    destGroup: string;
+    DFUServerQueue: string;
+    namePrefix: string;
+    selectedFiles?: {
+        TargetName: string,
+        TargetRowTag: string,
+        SourceFile: string,
+        SourceIP: string
+    }[],
+    sourceFormat: string;
+    sourceMaxRecordSize: string;
+    overwrite: boolean;
+    replicate: boolean;
+    nosplit: boolean;
+    noCommon: boolean;
+    compress: boolean;
+    failIfNoSourceFile: boolean;
+    delayedReplication: boolean;
+    expireDays: string;
+}
+
+const defaultValues: XmlImportFormValues = {
+    destGroup: "",
+    DFUServerQueue: "",
+    namePrefix: "",
+    sourceFormat: "1",
+    sourceMaxRecordSize: "",
+    overwrite: false,
+    replicate: false,
+    nosplit: false,
+    noCommon: true,
+    compress: false,
+    failIfNoSourceFile: false,
+    delayedReplication: true,
+    expireDays: ""
+};
+
+interface XmlImportFormProps {
+    formMinWidth?: number;
+    showForm: boolean;
+    selection: object[];
+    setShowForm: (_: boolean) => void;
+}
+
+export const XmlImportForm: React.FunctionComponent<XmlImportFormProps> = ({
+    formMinWidth = 300,
+    showForm,
+    selection,
+    setShowForm
+}) => {
+
+    const { handleSubmit, control, reset } = useForm<XmlImportFormValues>({ defaultValues });
+
+    const closeForm = React.useCallback(() => {
+        setShowForm(false);
+    }, [setShowForm]);
+
+    const onSubmit = React.useCallback(() => {
+        handleSubmit(
+            (data, evt) => {
+                let request = {};
+                const files = data.selectedFiles;
+
+                delete data.selectedFiles;
+
+                files.forEach(file => {
+                    request = data;
+                    request["sourceIP"] = file.SourceIP;
+                    request["sourcePath"] = file.SourceFile;
+                    request["destLogicalName"] = data.namePrefix + ((
+                            data.namePrefix && data.namePrefix.substr(-2) !== "::" &&
+                            file.TargetName && file.TargetName.substr(0, 2) !== "::"
+                        ) ? "::" : "") + file.TargetName;
+                    request["sourceRowTag"] = file.TargetRowTag;
+                    FileSpray.SprayVariable({
+                        request: request
+                    }).then((response) => {
+                        if (response.SprayResponse?.wuid) {
+                            pushUrl(`/dfuworkunits/${response.SprayResponse.wuid}`);
+                        }
+                    });
+                });
+            },
+            err => {
+                console.log(err);
+            }
+        )();
+    }, [handleSubmit]);
+
+    const titleId = useId("title");
+
+    const dragOptions: IDragOptions = {
+        moveMenuItemText: nlsHPCC.Move,
+        closeMenuItemText: nlsHPCC.Close,
+        menu: ContextualMenu,
+    };
+
+    const componentStyles = mergeStyleSets(
+        FormStyles.componentStyles,
+        {
+            container: {
+                minWidth: formMinWidth ? formMinWidth : 300,
+            }
+        }
+    );
+
+    React.useEffect(() => {
+        if (selection) {
+            const newValues = defaultValues;
+            newValues.selectedFiles = [];
+            selection.forEach((file, idx) => {
+                newValues.selectedFiles[idx] = {
+                    TargetName: file["name"],
+                    TargetRowTag: "Row",
+                    SourceFile: file["fullPath"],
+                    SourceIP: file["NetAddress"]
+                };
+            });
+            reset(newValues);
+        }
+    }, [selection, reset]);
+
+    return <Modal
+        titleAriaId={titleId}
+        isOpen={showForm}
+        onDismiss={closeForm}
+        isBlocking={false}
+        containerClassName={componentStyles.container}
+        dragOptions={dragOptions}
+    >
+        <div className={componentStyles.header}>
+            <span id={titleId}>{`${nlsHPCC.Import} ${nlsHPCC.XML}`}</span>
+            <IconButton
+                styles={FormStyles.iconButtonStyles}
+                iconProps={FormStyles.cancelIcon}
+                ariaLabel={nlsHPCC.CloseModal}
+                onClick={closeForm}
+            />
+        </div>
+        <div className={componentStyles.body}>
+            <Stack>
+                <Controller
+                    control={control} name="destGroup"
+                    render={({
+                        field: { onChange, name: fieldName, value },
+                        fieldState: { error }
+                    }) => <TargetGroupTextField
+                        key="destGroup"
+                        label={nlsHPCC.Group}
+                        required={true}
+                        optional={true}
+                        selectedKey={value}
+                        placeholder={nlsHPCC.SelectValue}
+                        onChange={(evt, option) => {
+                            onChange(option.key);
+                        }}
+                        errorMessage={ error && error?.message }
+                    /> }
+                    rules={{
+                        required: `${nlsHPCC.SelectA} ${nlsHPCC.Group}`
+                    }}
+                />
+                <Controller
+                    control={control} name="DFUServerQueue"
+                    render={({
+                        field: { onChange, name: fieldName, value },
+                        fieldState: { error }
+                    }) => <TargetDfuSprayQueueTextField
+                        key="DFUServerQueue"
+                        label={nlsHPCC.Queue}
+                        required={true}
+                        optional={true}
+                        selectedKey={value}
+                        placeholder={nlsHPCC.SelectValue}
+                        onChange={(evt, option) => {
+                            onChange(option.key);
+                        }}
+                        errorMessage={ error && error?.message }
+                    /> }
+                    rules={{
+                        required: `${nlsHPCC.SelectA} ${nlsHPCC.Queue}`
+                    }}
+                />
+                <Controller
+                    control={control} name="namePrefix"
+                    render={({
+                        field: { onChange, name: fieldName, value },
+                        fieldState: { error }
+                    }) => <TextField
+                        name={fieldName}
+                        onChange={onChange}
+                        label={nlsHPCC.TargetScope}
+                        value={value}
+                        placeholder={nlsHPCC.NamePrefixPlaceholder}
+                        errorMessage={ error && error?.message }
+                    /> }
+                    rules={{
+                        pattern: {
+                            value: /^([a-z0-9]+(::)?)+$/i,
+                            message: nlsHPCC.ValidationErrorNamePrefix
+                        }
+                    }}
+                />
+            </Stack>
+            <Stack>
+                <table className={`${componentStyles.twoColumnTable} ${componentStyles.selectionTable}`}>
+                    <thead>
+                        <tr>
+                            <th>{nlsHPCC.TargetName}</th>
+                        </tr>
+                    </thead>
+                    <tbody>
+                    { selection && selection.map((file, idx) => {
+                        return <tr key={`File-${idx}`}>
+                            <td><Controller
+                                control={control} name={`selectedFiles.${idx}.TargetName` as const}
+                                render={({
+                                    field: { onChange, name: fieldName, value },
+                                    fieldState: { error }
+                                }) => <TextField
+                                    name={fieldName}
+                                    onChange={onChange}
+                                    value={value}
+                                    errorMessage={ error && error?.message }
+                                /> }
+                                rules={{
+                                    required: nlsHPCC.ValidationErrorTargetNameRequired,
+                                    pattern: {
+                                        value: /^([a-z0-9]+[-a-z0-9 \._]+)+$/i,
+                                        message: nlsHPCC.ValidationErrorTargetNameInvalid
+                                    }
+                                }}
+                            /></td>
+                            <td>
+                                <Controller
+                                    control={control} name={`selectedFiles.${idx}.TargetRowTag` as const}
+                                    render={({
+                                        field: { onChange, name: fieldName, value },
+                                        fieldState: { error }
+                                    }) => <TextField
+                                        name={fieldName}
+                                        onChange={onChange}
+                                        value={value}
+                                        placeholder={nlsHPCC.RequiredForFixedSpray}
+                                        errorMessage={ error && error?.message }
+                                    /> }
+                                />
+                                <input type="hidden" name={`selectedFiles.${idx}.SourceFile` as const} value={file["fullPath"]} />
+                                <input type="hidden" name={`selectedFiles.${idx}.SourceIP` as const} value={file["NetAddress"]} />
+                            </td>
+                        </tr>;
+                    }) }
+                    </tbody>
+                </table>
+            </Stack>
+            <Stack>
+                <table><tbody>
+                    <tr>
+                        <td><Controller
+                            control={control} name="sourceFormat"
+                            render={({
+                                field: { onChange, name: fieldName, value },
+                                fieldState: { error }
+                            }) => <Dropdown
+                                key="sourceFormat"
+                                label={nlsHPCC.Format}
+                                options={[
+                                    { key: "1", text: "ASCII" },
+                                    { key: "2", text: "UTF-8" },
+                                    { key: "3", text: "UTF-8N" },
+                                    { key: "4", text: "UTF-16" },
+                                    { key: "5", text: "UTF-16LE" },
+                                    { key: "6", text: "UTF-16BE" },
+                                    { key: "7", text: "UTF-32" },
+                                    { key: "8", text: "UTF-32LE" },
+                                    { key: "9", text: "UTF-32BE" }
+                                ]}
+                                defaultSelectedKey="1"
+                                onChange={(evt, option) => {
+                                    onChange(option.key);
+                                }}
+                                errorMessage={ error && error?.message }
+                            /> }
+                            rules={{
+                                required: `${nlsHPCC.SelectA} ${nlsHPCC.Format}`
+                            }}
+                        /></td>
+                        <td><Controller
+                            control={control} name="sourceMaxRecordSize"
+                            render={({
+                                field: { onChange, name: fieldName, value },
+                                fieldState: { error }
+                            }) => <TextField
+                                name={fieldName}
+                                onChange={onChange}
+                                label={nlsHPCC.MaxRecordLength}
+                                value={value}
+                                placeholder="8192"
+                                errorMessage={ error && error?.message }
+                            /> }
+                        /></td>
+                    </tr>
+                </tbody></table>
+            </Stack>
+            <Stack>
+                <table className={componentStyles.twoColumnTable}>
+                    <tbody><tr>
+                        <td><Controller
+                            control={control} name="overwrite"
+                            render={({
+                                field : { onChange, name: fieldName, value }
+                            }) => <Checkbox name={fieldName} checked={value} onChange={onChange} label={nlsHPCC.Overwrite} /> }
+                        /></td>
+                        <td><Controller
+                            control={control} name="replicate"
+                            render={({
+                                field : { onChange, name: fieldName, value }
+                            }) => <Checkbox name={fieldName} checked={value} onChange={onChange} label={nlsHPCC.Replicate} /> }
+                        /></td>
+                    </tr>
+                    <tr>
+                        <td><Controller
+                            control={control} name="nosplit"
+                            render={({
+                                field : { onChange, name: fieldName, value }
+                            }) => <Checkbox name={fieldName} checked={value} onChange={onChange} label={nlsHPCC.NoSplit} /> }
+                        /></td>
+                        <td><Controller
+                            control={control} name="noCommon"
+                            render={({
+                                field : { onChange, name: fieldName, value }
+                            }) => <Checkbox name={fieldName} checked={value} onChange={onChange} label={nlsHPCC.NoCommon} /> }
+                        /></td>
+                    </tr>
+                    <tr>
+                        <td><Controller
+                            control={control} name="compress"
+                            render={({
+                                field : { onChange, name: fieldName, value }
+                            }) => <Checkbox name={fieldName} checked={value} onChange={onChange} label={nlsHPCC.Compress} /> }
+                        /></td>
+                        <td><Controller
+                            control={control} name="failIfNoSourceFile"
+                            render={({
+                                field : { onChange, name: fieldName, value }
+                            }) => <Checkbox name={fieldName} checked={value} onChange={onChange} label={nlsHPCC.FailIfNoSourceFile} /> }
+                        /></td>
+                    </tr>
+                    <tr>
+                        <td><Controller
+                            control={control} name="expireDays"
+                            render={({
+                                field: { onChange, name: fieldName, value },
+                                fieldState: { error }
+                            }) => <TextField
+                                name={fieldName}
+                                onChange={onChange}
+                                label={nlsHPCC.ExpireDays}
+                                value={value}
+                                errorMessage={ error && error?.message }
+                            />}
+                            rules={{
+                                min: {
+                                    value: 1,
+                                    message: nlsHPCC.ValidationErrorExpireDaysMinimum
+                                }
+                            }}
+                        /></td>
+                        <td><Controller
+                            control={control} name="delayedReplication"
+                            render={({
+                                field : { onChange, name: fieldName, value }
+                            }) => <Checkbox name={fieldName} checked={value} onChange={onChange} label={nlsHPCC.DelayedReplication} disabled={true} /> }
+                        /></td>
+                    </tr></tbody>
+                </table>
+            </Stack>
+            <Stack horizontal horizontalAlign="space-between" verticalAlign="end" styles={FormStyles.buttonStackStyles}>
+                <PrimaryButton text={nlsHPCC.Import} onClick={handleSubmit(onSubmit)} />
+            </Stack>
+        </div>
+    </Modal>;
+};

+ 5 - 1
esp/src/src-react/routes.tsx

@@ -112,7 +112,11 @@ export const routes: RoutesEx = [
     {
         mainNav: ["files"],
         path: "/landingzone",
-        action: () => import("./layouts/DojoAdapter").then(_ => <_.DojoAdapter widgetClassID="LZBrowseWidget" />)
+        children: [
+            { path: "", action: (context) => import("./components/LandingZone").then(_ => <_.LandingZone filter={parseSearch(context.search) as any} />) },
+            { path: "/legacy", action: () => import("./layouts/DojoAdapter").then(_ => <_.DojoAdapter widgetClassID="LZBrowseWidget" />) },
+            { path: "/preview/:logicalFile", action: (ctx, params) => import("./layouts/DojoAdapter").then(_ => <_.DojoAdapter widgetClassID="HexViewWidget" params={params} />) },
+        ],
     },
     {
         mainNav: ["files"],

+ 11 - 0
esp/src/src/nls/hpcc.ts

@@ -347,6 +347,7 @@ export = {
         HPCCSystems: "HPCC Systems®",
         Icon: "Icon",
         ID: "ID",
+        Import: "Import",
         Inactive: "Inactive",
         IncludeSlaveLogs: "Include slave logs",
         Index: "Index",
@@ -393,6 +394,7 @@ export = {
         LegacyGraphWidget: "Legacy Graph Widget",
         LegacyGraphLayout: "Legacy Graph Layout",
         Legend: "Legend",
+        Length: "Length",
         LDAPWarning: "<b>LDAP Services Error:</b>  &lsquo;Too Many Users&rsquo; - Please use a Filter.",
         LibrariesUsed: "Libraries Used",
         LibraryName: "Library Name",
@@ -709,6 +711,8 @@ export = {
         SearchResults: "Search Results",
         SecondsRemaining: "Seconds Remaining",
         Security: "Security",
+        SelectA: "Select a",
+        SelectValue: "Select a value",
         SeeConfigurationManager: "See Configuration Manager",
         SelectPackageFile: "Select Package File",
         SendEmail: "Send Email",
@@ -916,6 +920,7 @@ export = {
         UpdateDFs: "Update DFS",
         UpdateSuperFiles: "Update Super Files",
         Upload: "Upload",
+        Uploading: "Uploading",
         UpTime: "Up Time",
         URL: "URL",
         Usage: "Usage",
@@ -938,9 +943,15 @@ export = {
         ValidateResult: "=====Validate Result=====\n\n",
         ValidateResultHere: "(Validation result)",
         ValidationErrorEnterNumber: "Enter a valid number",
+        ValidationErrorExpireDaysMinimum: "Should not be less than 1 day",
+        ValidationErrorNamePrefix: `Should match pattern "some::prefix"`,
         ValidationErrorNumberGreater: "Cannot be greater than",
         ValidationErrorNumberLess: "Cannot be less than",
         ValidationErrorRequired: "This field is required",
+        ValidationErrorRecordSizeNumeric: "Record Length should be a number",
+        ValidationErrorRecordSizeRequired: "Record Length is required",
+        ValidationErrorTargetNameRequired: "File name is required",
+        ValidationErrorTargetNameInvalid: "Invalid file name",
         Value: "Value",
         Variable: "Variable",
         Variables: "Variables",