Skip to content

Commit 675052d

Browse files
author
Kuba Nowakowski
committed
feat: add Flink SQL visual template editor (CEP, Dedup, Window Dedup, Window Top-N)
1 parent 0635b69 commit 675052d

33 files changed

Lines changed: 4144 additions & 2 deletions

designer/client/src/actions/nk/assignSettings.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ export type FeaturesSettings = {
4444
surveySettings: SurveySettings;
4545
stickyNotesSettings: StickyNotesSettings;
4646
testCases: { multipleEnabled: boolean };
47+
flinkSqlTemplateEditor?: boolean;
4748
};
4849

4950
export type StickyNotesSettings = {

designer/client/src/components/graph/node-modal/ParameterExpressionField.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ export function ParameterExpressionField({ FieldWrapper, ...props }: ParameterEx
6565
);
6666

6767
const fieldErrors = getValidationErrorsForField(errors, parameter.name);
68+
const outputVarErrors = getValidationErrorsForField(errors, "outputVar");
6869

6970
const parameterDefinition = useMemo(
7071
() => findParamDefinitionByName(parameterDefinitions, parameter.name),
@@ -86,6 +87,7 @@ export function ParameterExpressionField({ FieldWrapper, ...props }: ParameterEx
8687
testResultsToShow={testResultsState.testResultsToShow}
8788
variableTypes={variableTypes}
8889
fieldErrors={fieldErrors}
90+
outputVarErrors={outputVarErrors}
8991
endAdornment={endAdornment}
9092
inputAdornmentEnd={inputAdornmentEnd}
9193
/>
@@ -94,6 +96,7 @@ export function ParameterExpressionField({ FieldWrapper, ...props }: ParameterEx
9496
endAdornment,
9597
inputAdornmentEnd,
9698
fieldErrors,
99+
outputVarErrors,
97100
isEditMode,
98101
listFieldPath,
99102
node,

designer/client/src/components/graph/node-modal/customNode.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,12 @@ export function CustomNode({
4242
(): boolean => !!ProcessUtils.extractComponentDefinition(node, processDefinitionData.components)?.returnType || !!node.outputVar,
4343
[node, processDefinitionData.components],
4444
);
45+
const isFlinkSqlNode = useMemo(() => findParameters(node).some((p) => p.name === "flinkSqlQuery"), [node]);
4546

4647
return (
4748
<>
4849
<NameField node={node} isEditMode={isEditMode} showValidation={showValidation} setProperty={setProperty} errors={errors} />
49-
{hasOutputVar && (
50+
{hasOutputVar && !isFlinkSqlNode && (
5051
<NodeField
5152
node={node}
5253
isEditMode={isEditMode}

designer/client/src/components/graph/node-modal/editors/expression/ExpressionField.tsx

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { ReactNode } from "react";
33
import React, { useCallback, useMemo } from "react";
44

55
import type { NodeResultsForContext } from "../../../../../common/TestResultUtils";
6+
import { useUserSettings } from "../../../../../common/useUserSettings";
67
import type { UIParameter } from "../../../../../types/definition";
78
import type { NodeType } from "../../../../../types/node";
89
import type { VariableTypes } from "../../../../../types/validation";
@@ -11,9 +12,12 @@ import ExpressionTestResults from "../../tests/ExpressionTestResults";
1112
import EditableEditor from "../EditableEditor";
1213
import type { FieldError } from "../Validators";
1314
import type { OnValueChange } from "./Editor";
15+
import { FlinkSqlTemplateEditor } from "./FlinkSqlTemplateEditor";
1416
import type { ExpressionObj } from "./types";
1517
import { EditorType } from "./types";
1618

19+
const FLINK_SQL_PARAM = "flinkSqlQuery";
20+
1721
export type ExpressionFieldProps = {
1822
fieldName: string;
1923
fieldLabel: string;
@@ -27,6 +31,7 @@ export type ExpressionFieldProps = {
2731
testResultsToShow: NodeResultsForContext;
2832
variableTypes: VariableTypes;
2933
fieldErrors: FieldError[];
34+
outputVarErrors?: FieldError[];
3035
endAdornment?: ReactNode;
3136
inputAdornmentEnd?: ReactNode;
3237
};
@@ -45,6 +50,7 @@ function ExpressionField(props: ExpressionFieldProps): React.JSX.Element {
4550
testResultsToShow,
4651
variableTypes,
4752
fieldErrors,
53+
outputVarErrors,
4854
endAdornment,
4955
inputAdornmentEnd,
5056
} = props;
@@ -53,6 +59,7 @@ function ExpressionField(props: ExpressionFieldProps): React.JSX.Element {
5359
const exprTextPath = `${exprPath}.expression`;
5460
const expressionObj = useMemo(() => get(editedNode, exprPath), [editedNode, exprPath]);
5561
const editors = useMemo(() => parameterDefinition?.editors || [], [parameterDefinition?.editors]);
62+
const [showFlinkSqlTemplateEditor] = useUserSettings("node.showFlinkSqlTemplateEditor");
5663

5764
const onValueChange: OnValueChange = useCallback(
5865
(value: ExpressionObj) => {
@@ -61,7 +68,10 @@ function ExpressionField(props: ExpressionFieldProps): React.JSX.Element {
6168
[exprPath, setNodeDataAt],
6269
);
6370

64-
const editor = useMemo(
71+
const isFlinkSqlQuery =
72+
showFlinkSqlTemplateEditor && fieldName === FLINK_SQL_PARAM && editors.some((e) => e.type === EditorType.SQL_PARAMETER_EDITOR);
73+
74+
const sqlEditor = useMemo(
6575
() => (
6676
<EditableEditor
6777
defaultValue={parameterDefinition?.defaultValue}
@@ -99,6 +109,38 @@ function ExpressionField(props: ExpressionFieldProps): React.JSX.Element {
99109
],
100110
);
101111

112+
const editor = useMemo(() => {
113+
if (isFlinkSqlQuery) {
114+
return (
115+
<FlinkSqlTemplateEditor
116+
expressionObj={expressionObj}
117+
onValueChange={onValueChange}
118+
readOnly={readOnly}
119+
variableTypes={variableTypes}
120+
SqlEditorComponent={sqlEditor}
121+
outputVar={editedNode.outputVar}
122+
onOutputVarChange={(name) => setNodeDataAt("outputVar", name)}
123+
fieldErrors={fieldErrors}
124+
outputVarErrors={outputVarErrors}
125+
showValidation={showValidation}
126+
/>
127+
);
128+
}
129+
return sqlEditor;
130+
}, [
131+
isFlinkSqlQuery,
132+
expressionObj,
133+
onValueChange,
134+
readOnly,
135+
variableTypes,
136+
sqlEditor,
137+
editedNode.outputVar,
138+
setNodeDataAt,
139+
outputVarErrors,
140+
fieldErrors,
141+
showValidation,
142+
]);
143+
102144
const isFixedValues = useMemo(
103145
() =>
104146
editors.some(
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import { Box, Chip, CircularProgress, FormControl, MenuItem, Select, Tooltip, Typography } from "@mui/material";
2+
import React from "react";
3+
import { useTranslation } from "react-i18next";
4+
5+
import type { VariableTypes } from "../../../../../../types/validation";
6+
import { FormRow } from "./components/FormRow";
7+
import { nuMenuProps, nuSelectSx } from "./components/nuInputSx";
8+
import { getTypeColor } from "./components/typeColors";
9+
import type { InputField } from "./types";
10+
import { buildInputFieldsForVariable, EXCLUDED_INPUT_VARIABLES } from "./types";
11+
12+
interface Props {
13+
fields: InputField[];
14+
onChange: (fields: InputField[]) => void;
15+
readOnly?: boolean;
16+
variableTypes: VariableTypes;
17+
}
18+
19+
function variableHasRecordTime(varName: string, variableTypes: VariableTypes): boolean {
20+
const typingResult = variableTypes[varName];
21+
if (!typingResult || !("fields" in typingResult) || !typingResult.fields) return false;
22+
return "record_time" in (typingResult.fields as Record<string, unknown>);
23+
}
24+
25+
function getAvailableVariables(variableTypes: VariableTypes): string[] {
26+
return Object.entries(variableTypes)
27+
.filter(
28+
([name, v]) =>
29+
!EXCLUDED_INPUT_VARIABLES.has(name) &&
30+
"fields" in (v as object) &&
31+
(v as { fields?: unknown }).fields &&
32+
Object.keys((v as { fields: object }).fields).length > 0,
33+
)
34+
.map(([name]) => name);
35+
}
36+
37+
export function InputFieldsSection({ fields, onChange, readOnly, variableTypes }: Props) {
38+
const { t } = useTranslation();
39+
40+
const availableVars = getAvailableVariables(variableTypes);
41+
const currentVar = fields[0]?.source.split(".")[0] ?? "";
42+
const hasConflictingRecordTime = currentVar ? variableHasRecordTime(currentVar, variableTypes) : false;
43+
const isLoadingTypes =
44+
availableVars.length === 0 &&
45+
Object.keys(variableTypes).some((k) => !EXCLUDED_INPUT_VARIABLES.has(k)) &&
46+
Object.values(variableTypes).every((v) => !("fields" in (v as object)));
47+
48+
const handleVarChange = (varName: string) => {
49+
if (varName) onChange(buildInputFieldsForVariable(varName, variableTypes));
50+
};
51+
52+
return (
53+
<FormRow
54+
label={t("flinkSql.inputFields.variable", "Input variable:")}
55+
tooltip={t(
56+
"flinkSql.inputFields.tooltip",
57+
"The stream variable whose fields are used as the input source. Select a variable to choose which fields are projected into the query.",
58+
)}
59+
>
60+
<Box>
61+
{isLoadingTypes ? (
62+
<Box display="flex" alignItems="center" gap={1} height={35}>
63+
<CircularProgress size={14} />
64+
<Typography variant="body2" color="text.disabled" sx={{ fontSize: "0.85rem" }}>
65+
{t("flinkSql.inputFields.loadingTypes", "Loading variable types\u2026")}
66+
</Typography>
67+
</Box>
68+
) : (
69+
<FormControl size="small" variant="outlined" fullWidth>
70+
<Select
71+
value={currentVar}
72+
onChange={(e) => handleVarChange(e.target.value)}
73+
disabled={readOnly || availableVars.length === 0}
74+
displayEmpty
75+
renderValue={(v) =>
76+
v ? (
77+
`#${v}`
78+
) : (
79+
<Typography variant="body2" color="text.disabled" sx={{ fontSize: "0.85rem" }}>
80+
{t("flinkSql.inputFields.placeholder", "Select variable\u2026")}
81+
</Typography>
82+
)
83+
}
84+
MenuProps={nuMenuProps}
85+
sx={nuSelectSx}
86+
>
87+
{availableVars.map((v) => (
88+
<MenuItem key={v} value={v} sx={{ fontSize: "0.85rem" }}>
89+
#{v}
90+
</MenuItem>
91+
))}
92+
</Select>
93+
</FormControl>
94+
)}
95+
96+
{/* Field chips */}
97+
{fields.length > 0 && (
98+
<Box display="flex" flexWrap="wrap" gap={0.5} mt={0.75}>
99+
{fields.map((f) => (
100+
<Chip
101+
key={f.name}
102+
label={f.alias}
103+
size="small"
104+
sx={{
105+
fontSize: "0.7rem",
106+
fontWeight: 500,
107+
height: 20,
108+
backgroundColor: `${getTypeColor(f.type)}22`,
109+
borderColor: getTypeColor(f.type),
110+
border: `1px solid ${getTypeColor(f.type)}66`,
111+
color: getTypeColor(f.type),
112+
}}
113+
/>
114+
))}
115+
{hasConflictingRecordTime ? (
116+
<Tooltip
117+
title={t(
118+
"flinkSql.inputFields.recordTimeConflict",
119+
"record_time is always added automatically and cannot be projected from the input variable.",
120+
)}
121+
placement="top"
122+
>
123+
<Chip
124+
label="record_time"
125+
size="small"
126+
sx={{
127+
fontSize: "0.7rem",
128+
fontWeight: 500,
129+
height: 20,
130+
backgroundColor: "action.disabledBackground",
131+
border: "1px solid",
132+
borderColor: "divider",
133+
color: "text.disabled",
134+
textDecoration: "line-through",
135+
cursor: "default",
136+
}}
137+
/>
138+
</Tooltip>
139+
) : (
140+
<Typography
141+
variant="caption"
142+
sx={{ fontSize: "0.68rem", color: "text.disabled", alignSelf: "center", ml: 0.5 }}
143+
>
144+
{t("flinkSql.inputFields.recordTime", "+ record_time")}
145+
</Typography>
146+
)}
147+
</Box>
148+
)}
149+
</Box>
150+
</FormRow>
151+
);
152+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import ContentCopyIcon from "@mui/icons-material/ContentCopy";
2+
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
3+
import { Accordion, AccordionDetails, AccordionSummary, Box, IconButton, Tooltip, Typography } from "@mui/material";
4+
import React, { useState } from "react";
5+
import { useTranslation } from "react-i18next";
6+
7+
interface Props {
8+
sql: string;
9+
}
10+
11+
export function SqlPreview({ sql }: Props) {
12+
const { t } = useTranslation();
13+
const [copied, setCopied] = useState(false);
14+
15+
const handleCopy = () => {
16+
navigator.clipboard.writeText(sql).then(() => {
17+
setCopied(true);
18+
setTimeout(() => setCopied(false), 1500);
19+
});
20+
};
21+
22+
return (
23+
<Accordion
24+
disableGutters
25+
elevation={0}
26+
sx={(theme) => ({
27+
backgroundColor: theme.palette.background.default,
28+
border: `1px solid ${theme.palette.divider}`,
29+
"&:before": { display: "none" },
30+
borderRadius: "6px !important",
31+
overflow: "hidden",
32+
})}
33+
>
34+
<AccordionSummary
35+
expandIcon={<ExpandMoreIcon sx={{ fontSize: "1.1rem" }} />}
36+
sx={(theme) => ({
37+
minHeight: 36,
38+
py: 0,
39+
px: 1.5,
40+
"&.Mui-expanded": { minHeight: 36 },
41+
"& .MuiAccordionSummary-content": { my: 0.75 },
42+
})}
43+
>
44+
<Typography
45+
variant="caption"
46+
color="text.secondary"
47+
sx={{ textTransform: "uppercase", letterSpacing: "0.08em", fontWeight: 600, fontSize: "0.7rem" }}
48+
>
49+
{t("flinkSql.preview.title", "Generated SQL")}
50+
</Typography>
51+
</AccordionSummary>
52+
<AccordionDetails sx={{ p: 0, position: "relative" }}>
53+
<Tooltip title={copied ? t("flinkSql.preview.copied", "Copied!") : t("flinkSql.preview.copy", "Copy")}>
54+
<IconButton size="small" onClick={handleCopy} sx={{ position: "absolute", top: 4, right: 4, zIndex: 1 }}>
55+
<ContentCopyIcon sx={{ fontSize: "0.9rem" }} />
56+
</IconButton>
57+
</Tooltip>
58+
<Box
59+
component="pre"
60+
sx={(theme) => ({
61+
m: 0,
62+
p: 1.5,
63+
pr: 5,
64+
fontFamily: "'JetBrains Mono', 'Fira Code', monospace",
65+
fontSize: "0.75rem",
66+
color: theme.palette.text.primary,
67+
overflowX: "auto",
68+
whiteSpace: "pre",
69+
lineHeight: 1.6,
70+
backgroundColor: "rgba(0,0,0,0.15)",
71+
})}
72+
>
73+
{sql}
74+
</Box>
75+
</AccordionDetails>
76+
</Accordion>
77+
);
78+
}

0 commit comments

Comments
 (0)