diff --git a/esp/src/src-react/components/ECLPlayground.tsx b/esp/src/src-react/components/ECLPlayground.tsx index ecee18566cd..665e0c699ad 100644 --- a/esp/src/src-react/components/ECLPlayground.tsx +++ b/esp/src/src-react/components/ECLPlayground.tsx @@ -1,14 +1,17 @@ import * as React from "react"; import { ReflexContainer, ReflexElement, ReflexSplitter } from "../layouts/react-reflex"; -import { PrimaryButton, IconButton, IIconProps, Link, Dropdown, IDropdownOption, TextField, useTheme } from "@fluentui/react"; +import { IconButton, IIconProps, Link, Dropdown, IDropdownOption, TextField, useTheme } from "@fluentui/react"; +import { Button } from "@fluentui/react-components"; +import { CheckmarkCircleRegular, DismissCircleRegular, QuestionCircleRegular } from "@fluentui/react-icons"; import { scopedLogger } from "@hpcc-js/util"; import { useOnEvent } from "@fluentui/react-hooks"; import { mergeStyleSets } from "@fluentui/style-utilities"; import { ECLEditor, IPosition } from "@hpcc-js/codemirror"; -import { Workunit, WUUpdate } from "@hpcc-js/comms"; +import { Workunit, WUUpdate, WorkunitsService } from "@hpcc-js/comms"; import { HolyGrail } from "../layouts/HolyGrail"; import { DojoAdapter } from "../layouts/DojoAdapter"; import { pushUrl } from "../util/history"; +import { debounce } from "../util/throttle"; import { darkTheme } from "../themes"; import { InfoGrid } from "./InfoGrid"; import { TabbedResults } from "./Results"; @@ -77,6 +80,9 @@ const playgroundStyles = mergeStyleSets({ borderRight: borderStyle } }, + ".fui-Button": { + height: "min-content" + }, ".ms-Label": { marginRight: "12px" }, @@ -155,43 +161,58 @@ const warningIcon: IIconProps = { title: nlsHPCC.ErrorWarnings, ariaLabel: nlsHP const resultsIcon: IIconProps = { title: nlsHPCC.Outputs, ariaLabel: nlsHPCC.Outputs, iconName: "Table" }; const graphIcon: IIconProps = { title: nlsHPCC.Visualizations, ariaLabel: nlsHPCC.Visualizations, iconName: "BarChartVerticalFill" }; -const displayErrors = (wu, editor) => { +const displayErrors = async (wu = null, editor, errors = []) => { if (!editor) return; - wu.fetchECLExceptions().then(errors => { - errors.forEach(err => { - const lineError = err.LineNo; - const lineErrorNum = lineError > 0 ? lineError - 1 : 0; - const startPos: IPosition = { - ch: (err.Column > 0) ? err.Column - 1 : 0, - line: lineErrorNum - }; - const endPos: IPosition = { - ch: editor.getLineLength(lineErrorNum), - line: lineErrorNum - }; - - switch (err.Severity) { - case "Info": - editor.highlightInfo(startPos, endPos); - break; - case "Warning": - editor.highlightWarning(startPos, endPos); - break; - case "Error": - default: - editor.highlightError(startPos, endPos); - break; - } - }); + if (wu) { + errors = await wu.fetchECLExceptions(); + } + if (!errors.length) { + editor.removeAllHighlight(); + } + errors.forEach(err => { + const lineError = err.LineNo; + const lineErrorNum = lineError > 0 ? lineError - 1 : 0; + const startPos: IPosition = { + ch: (err.Column > 0) ? err.Column - 1 : 0, + line: lineErrorNum + }; + const endPos: IPosition = { + ch: editor.getLineLength(lineErrorNum), + line: lineErrorNum + }; + + switch (err.Severity) { + case "Info": + editor.highlightInfo(startPos, endPos); + break; + case "Warning": + editor.highlightWarning(startPos, endPos); + break; + case "Error": + default: + editor.highlightError(startPos, endPos); + break; + } }); }; +const service = new WorkunitsService({ baseUrl: "" }); + +enum SyntaxCheckResult { + Unknown, + Failed, + Passed +} + interface ECLEditorToolbarProps { editor: ECLEditor; outputMode: OutputMode; setOutputMode: (_: OutputMode) => void; workunit: Workunit; setWorkunit: (_: Workunit) => void; + setSyntaxErrors: (_: any) => void; + syntaxStatusIcon: number; + setSyntaxStatusIcon: (_: number) => void; } const ECLEditorToolbar: React.FunctionComponent = ({ @@ -199,7 +220,10 @@ const ECLEditorToolbar: React.FunctionComponent = ({ outputMode, setOutputMode, workunit, - setWorkunit + setWorkunit, + setSyntaxErrors, + syntaxStatusIcon, + setSyntaxStatusIcon }) => { const [cluster, setCluster] = React.useState(""); @@ -258,6 +282,24 @@ const ECLEditorToolbar: React.FunctionComponent = ({ } }, [cluster, editor, playgroundResults, queryName, setQueryNameErrorMsg]); + const checkSyntax = React.useCallback(() => { + service.WUSyntaxCheckECL({ + ECL: editor.ecl(), + Cluster: cluster + }).then(response => { + if (response.Errors) { + setSyntaxStatusIcon(SyntaxCheckResult.Failed); + setSyntaxErrors(response.Errors.ECLException); + displayErrors(null, editor, response.Errors.ECLException); + setOutputMode(OutputMode.ERRORS); + } else { + setSyntaxStatusIcon(SyntaxCheckResult.Passed); + setSyntaxErrors([]); + displayErrors(null, editor, []); + } + }); + }, [cluster, editor, setOutputMode, setSyntaxErrors, setSyntaxStatusIcon]); + const handleKeyUp = React.useCallback((evt) => { switch (evt.key) { case "Enter": @@ -282,10 +324,19 @@ const ECLEditorToolbar: React.FunctionComponent = ({ return
{showSubmitBtn ? ( - + ) : ( - + )} + = (props const [query, setQuery] = React.useState(""); const [selectedEclSample, setSelectedEclSample] = React.useState(""); const [eclContent, setEclContent] = React.useState(""); + const [syntaxErrors, setSyntaxErrors] = React.useState([]); + const [syntaxStatusIcon, setSyntaxStatusIcon] = React.useState(SyntaxCheckResult.Unknown); const [eclSamples, setEclSamples] = React.useState([]); React.useEffect(() => { @@ -417,6 +470,13 @@ export const ECLPlayground: React.FunctionComponent = (props }, [editor]); useOnEvent(document, "eclwatch-theme-toggle", handleThemeToggle); + const handleEclChange = React.useMemo(() => debounce((evt) => { + if (editor.hasFocus()) { + setSyntaxStatusIcon(SyntaxCheckResult.Unknown); + } + }, 300), [editor]); + useOnEvent(window, "keyup", handleEclChange); + return

{nlsHPCC.title_ECLPlayground}

@@ -437,7 +497,8 @@ export const ECLPlayground: React.FunctionComponent = (props main={} footer={ @@ -453,7 +514,7 @@ export const ECLPlayground: React.FunctionComponent = (props {outputMode === OutputMode.ERRORS ? ( - + ) : outputMode === OutputMode.RESULTS ? ( diff --git a/esp/src/src-react/components/InfoGrid.tsx b/esp/src/src-react/components/InfoGrid.tsx index 2fe63499487..0e13e1d0e7b 100644 --- a/esp/src/src-react/components/InfoGrid.tsx +++ b/esp/src/src-react/components/InfoGrid.tsx @@ -32,11 +32,13 @@ interface FilterCounts { } interface InfoGridProps { - wuid: string; + wuid?: string; + syntaxErrors?: any[]; } export const InfoGrid: React.FunctionComponent = ({ - wuid + wuid = null, + syntaxErrors = [] }) => { const [costChecked, setCostChecked] = React.useState(true); @@ -46,6 +48,7 @@ export const InfoGrid: React.FunctionComponent = ({ const [otherChecked, setOtherChecked] = React.useState(true); const [filterCounts, setFilterCounts] = React.useState({ cost: 0, penalty: 0, error: 0, warning: 0, info: 0, other: 0 }); const [exceptions] = useWorkunitExceptions(wuid); + const [errors, setErrors] = React.useState([]); const [data, setData] = React.useState([]); const { selection, setSelection, @@ -61,6 +64,14 @@ export const InfoGrid: React.FunctionComponent = ({ { key: "others", onRender: () => setOtherChecked(value)} styles={{ root: { paddingTop: 8, paddingRight: 8 } }} /> } ], [filterCounts.cost, filterCounts.error, filterCounts.info, filterCounts.other, filterCounts.warning]); + React.useEffect(() => { + if (syntaxErrors.length) { + setErrors(syntaxErrors); + } else { + setErrors(exceptions); + } + }, [syntaxErrors, exceptions]); + // Grid --- const columns = React.useMemo((): FluentColumns => { return { @@ -137,7 +148,7 @@ export const InfoGrid: React.FunctionComponent = ({ info: 0, other: 0 }; - const filteredExceptions = exceptions.map((row, idx) => { + const filteredExceptions = errors?.map((row, idx) => { if (row.Source === "Cost Optimizer") { row.Severity = "Cost"; } @@ -199,7 +210,7 @@ export const InfoGrid: React.FunctionComponent = ({ }); setData(filteredExceptions); setFilterCounts(filterCounts); - }, [costChecked, errorChecked, exceptions, infoChecked, otherChecked, warningChecked]); + }, [costChecked, errorChecked, errors, infoChecked, otherChecked, warningChecked]); React.useEffect(() => { if (data.length) { diff --git a/esp/src/src/nls/hpcc.ts b/esp/src/src/nls/hpcc.ts index ff03308e6fd..8588cf18c54 100644 --- a/esp/src/src/nls/hpcc.ts +++ b/esp/src/src/nls/hpcc.ts @@ -922,6 +922,7 @@ export = { Statistics: "Statistics", SVGSource: "SVG Source", SyncSelection: "Sync To Selection", + Syntax: "Syntax", SystemServers: "System Servers", tag: "tag", Target: "Target",