Xiao-zhen-Liu commented on code in PR #5043:
URL: https://github.com/apache/texera/pull/5043#discussion_r3261286198
##########
frontend/package.json:
##########
@@ -38,6 +38,7 @@
"@codingame/monaco-vscode-java-default-extension": "8.0.4",
"@codingame/monaco-vscode-python-default-extension": "8.0.4",
"@codingame/monaco-vscode-r-default-extension": "8.0.4",
+ "@lezer/python": "1.1.18",
Review Comment:
Since this is a new library, please note the license of this library in the
PR description. @bobbai00 is there any protocol we should follow from now on
when introducing new dependencies, given our recent release experience?
##########
frontend/src/app/workspace/service/code-editor/ui-udf-parameters-parser.service.ts:
##########
@@ -0,0 +1,217 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { Injectable } from "@angular/core";
+import { parser } from "@lezer/python";
+import { AttributeType, SchemaAttribute } from
"../../types/workflow-compiling.interface";
+
+export interface UiUdfParameter {
+ attribute: SchemaAttribute;
+ value: string;
+}
+
+const CLASSES = new Set(["ProcessTupleOperator", "ProcessBatchOperator",
"ProcessTableOperator", "GenerateOperator"]);
+
+// Java enum constant names (AttributeType.java)
+const JAVA_ATTRIBUTE_TYPE_NAMES = [
+ "STRING",
+ "INTEGER",
+ "LONG",
+ "DOUBLE",
+ "BOOLEAN",
+ "TIMESTAMP",
+ "BINARY",
+ "LARGE_BINARY",
+] as const;
+
+type JavaAttributeTypeName = (typeof JAVA_ATTRIBUTE_TYPE_NAMES)[number];
+
+// Python enum constant names (core.models.AttributeType)
+const PYTHON_ATTRIBUTE_TYPE_NAMES = [
+ "STRING",
+ "INT",
+ "LONG",
+ "DOUBLE",
+ "BOOL",
+ "TIMESTAMP",
+ "BINARY",
+ "LARGE_BINARY",
+] as const;
+
+type PythonAttributeTypeName = (typeof PYTHON_ATTRIBUTE_TYPE_NAMES)[number];
+
+type ParserAttributeTypeToken = JavaAttributeTypeName |
PythonAttributeTypeName;
+type UnsupportedParserAttributeTypeToken = "BINARY" | "LARGE_BINARY";
+type SupportedParserAttributeTypeToken = Exclude<ParserAttributeTypeToken,
UnsupportedParserAttributeTypeToken>;
+type ParserSyntaxNode = ReturnType<typeof parser.parse>["topNode"];
+
+const TYPES: Readonly<Record<SupportedParserAttributeTypeToken,
AttributeType>> = {
+ STRING: "string",
+ INTEGER: "integer",
+ INT: "integer",
+ LONG: "long",
+ DOUBLE: "double",
+ BOOLEAN: "boolean",
+ BOOL: "boolean",
+ TIMESTAMP: "timestamp",
+};
+
+const JAVA_ATTRIBUTE_TYPE_NAME_SET = new
Set<string>(JAVA_ATTRIBUTE_TYPE_NAMES);
+const PYTHON_ATTRIBUTE_TYPE_NAME_SET = new
Set<string>(PYTHON_ATTRIBUTE_TYPE_NAMES);
+const SUPPORTED_UI_PARAMETER_ATTRIBUTE_TYPES = new Set<AttributeType>([
+ "string",
+ "integer",
+ "long",
+ "double",
+ "boolean",
+ "timestamp",
+]);
+
+@Injectable({ providedIn: "root" })
+export class UiUdfParametersParserService {
+ parse(code: string): UiUdfParameter[] {
+ if (!code) return [];
+
+ const result: UiUdfParameter[] = [];
+ const seen = new Set<string>();
+ const add = (parameter?: UiUdfParameter): void => {
+ const name = parameter?.attribute.attributeName;
+ if (parameter && name && !seen.has(name)) {
+ seen.add(name);
+ result.push(parameter);
+ }
+ };
+
+ parser.parse(code).iterate({
+ enter: ({ name, node }) => {
+ const className = node.getChild("VariableName");
+ if (name !== "ClassDefinition" || !className ||
!CLASSES.has(code.slice(className.from, className.to))) return;
+ node
+ .cursor()
+ .iterate(ref => (ref.name === "CallExpression" ?
(add(readCall(ref.node, code)), false) : undefined));
+ return false;
+ },
+ });
+
+ return result;
+ }
+}
+
+function readCall(call: ParserSyntaxNode, code: string): UiUdfParameter |
undefined {
+ const args = call.getChild("ArgList");
+ if (!args || code.slice(call.from, args.from).replace(/\s+/g, "") !==
"self.UiParameter") return undefined;
+
+ let attributeName: string | undefined;
+ let attributeType: AttributeType | undefined;
+ let index = 0;
+ let sawNamed = false;
+
+ for (const arg of splitArgs(code.slice(args.from + 1, args.to - 1))) {
+ const match = arg.match(/^([A-Za-z_]\w*)\s*=\s*([\s\S]+)$/);
+ const key = match?.[1];
+ const value = match?.[2] ?? arg;
+
+ if (match) sawNamed = true;
+ else if (sawNamed || index > 1) return undefined;
+
+ if ((match ? key === "name" : index === 0) && !attributeName)
attributeName = readString(value)?.trim();
+ else if ((match ? key === "type" || key === "attr_type" : index === 1) &&
!attributeType)
+ attributeType = readType(value);
+ else return undefined;
+
+ if (!match) index++;
+ if (!attributeName && (key === "name" || (!match && index === 1))) return
undefined;
+ if (!attributeType && (key === "type" || key === "attr_type" || (!match &&
index === 2))) return undefined;
+ }
+
+ return attributeName && attributeType ? { attribute: { attributeName,
attributeType }, value: "" } : undefined;
+}
+
+function splitArgs(input: string): string[] {
+ const result: string[] = [];
Review Comment:
This function parses the argument list character by character, but Lezer's
`ArgList` node already provides typed child nodes for each argument: `String`
for literals, `MemberExpression` for things like `AttributeType.INT`, and a
separate node type for `key=value` arguments. Using those nodes directly would
remove around 40 lines from this file, handle cases like f-strings, nested
calls, and multiline arguments automatically, and make it easier to extend the
parser in later PRs (for example to support default values or constraints).
Better to do this now rather than carry `splitArgs` forward into PR #2.
##########
frontend/src/app/workspace/service/code-editor/ui-udf-parameters-parser.service.ts:
##########
@@ -0,0 +1,217 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { Injectable } from "@angular/core";
+import { parser } from "@lezer/python";
+import { AttributeType, SchemaAttribute } from
"../../types/workflow-compiling.interface";
+
+export interface UiUdfParameter {
+ attribute: SchemaAttribute;
+ value: string;
+}
+
+const CLASSES = new Set(["ProcessTupleOperator", "ProcessBatchOperator",
"ProcessTableOperator", "GenerateOperator"]);
+
+// Java enum constant names (AttributeType.java)
+const JAVA_ATTRIBUTE_TYPE_NAMES = [
+ "STRING",
+ "INTEGER",
+ "LONG",
+ "DOUBLE",
+ "BOOLEAN",
+ "TIMESTAMP",
+ "BINARY",
+ "LARGE_BINARY",
+] as const;
+
+type JavaAttributeTypeName = (typeof JAVA_ATTRIBUTE_TYPE_NAMES)[number];
+
+// Python enum constant names (core.models.AttributeType)
+const PYTHON_ATTRIBUTE_TYPE_NAMES = [
+ "STRING",
+ "INT",
+ "LONG",
+ "DOUBLE",
+ "BOOL",
+ "TIMESTAMP",
+ "BINARY",
+ "LARGE_BINARY",
+] as const;
+
+type PythonAttributeTypeName = (typeof PYTHON_ATTRIBUTE_TYPE_NAMES)[number];
+
+type ParserAttributeTypeToken = JavaAttributeTypeName |
PythonAttributeTypeName;
+type UnsupportedParserAttributeTypeToken = "BINARY" | "LARGE_BINARY";
+type SupportedParserAttributeTypeToken = Exclude<ParserAttributeTypeToken,
UnsupportedParserAttributeTypeToken>;
+type ParserSyntaxNode = ReturnType<typeof parser.parse>["topNode"];
+
+const TYPES: Readonly<Record<SupportedParserAttributeTypeToken,
AttributeType>> = {
+ STRING: "string",
+ INTEGER: "integer",
+ INT: "integer",
+ LONG: "long",
+ DOUBLE: "double",
+ BOOLEAN: "boolean",
+ BOOL: "boolean",
+ TIMESTAMP: "timestamp",
+};
+
+const JAVA_ATTRIBUTE_TYPE_NAME_SET = new
Set<string>(JAVA_ATTRIBUTE_TYPE_NAMES);
+const PYTHON_ATTRIBUTE_TYPE_NAME_SET = new
Set<string>(PYTHON_ATTRIBUTE_TYPE_NAMES);
+const SUPPORTED_UI_PARAMETER_ATTRIBUTE_TYPES = new Set<AttributeType>([
+ "string",
+ "integer",
+ "long",
+ "double",
+ "boolean",
+ "timestamp",
+]);
+
+@Injectable({ providedIn: "root" })
+export class UiUdfParametersParserService {
+ parse(code: string): UiUdfParameter[] {
+ if (!code) return [];
+
+ const result: UiUdfParameter[] = [];
+ const seen = new Set<string>();
+ const add = (parameter?: UiUdfParameter): void => {
+ const name = parameter?.attribute.attributeName;
+ if (parameter && name && !seen.has(name)) {
+ seen.add(name);
+ result.push(parameter);
+ }
+ };
+
+ parser.parse(code).iterate({
+ enter: ({ name, node }) => {
+ const className = node.getChild("VariableName");
+ if (name !== "ClassDefinition" || !className ||
!CLASSES.has(code.slice(className.from, className.to))) return;
+ node
+ .cursor()
+ .iterate(ref => (ref.name === "CallExpression" ?
(add(readCall(ref.node, code)), false) : undefined));
+ return false;
Review Comment:
The `false` here is semantically important. It tells Lezer not to descend
further into the node. Putting it inside a comma expression makes it look like
an accident. A small block body is slightly longer but reads more clearly:
```ts
.iterate(ref => {
if (ref.name !== "CallExpression") return;
add(readCall(ref.node, code));
return false;
});
```
##########
frontend/src/app/workspace/component/ui-udf-parameters/ui-udf-parameters.component.html:
##########
@@ -0,0 +1,55 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+-->
+<div
+ class="ui-udf-param-list"
+ *ngIf="(model?.length ?? 0) > 0">
+ <!-- Optional header row -->
+ <div class="ui-udf-param-row header">
+ <div class="field-cell"><span class="col-title">Value</span></div>
+ <div class="field-cell"><span class="col-title">Name</span></div>
+ <div class="field-cell"><span class="col-title">Type</span></div>
+ </div>
+
+ <div
+ class="ui-udf-param-row"
+ *ngFor="let param of (model || []); let i = index; trackBy:
trackByParamName">
+ <ng-container *ngIf="field.fieldGroup?.[i] as rowField">
+ <!-- Value -->
+ <div class="field-cell">
+ <ng-container *ngIf="getValueField(rowField) as valueField">
Review Comment:
These getters (`getValueField`, `getNameField`, `getTypeField`) re-run
`setDisabled` on every change-detection cycle. Each call assigns a fresh
`props` object and re-calls `disable()`. Disabling a control should happen once
per row, not on every cycle. Please move this logic into each row's
`FormlyFieldConfig.hooks.onInit` hook, or better, set `disabled: true` directly
in the field schema when the schema is built.
##########
frontend/src/app/workspace/service/code-editor/ui-udf-parameters-parser.service.ts:
##########
@@ -0,0 +1,217 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { Injectable } from "@angular/core";
+import { parser } from "@lezer/python";
+import { AttributeType, SchemaAttribute } from
"../../types/workflow-compiling.interface";
+
+export interface UiUdfParameter {
+ attribute: SchemaAttribute;
+ value: string;
+}
+
+const CLASSES = new Set(["ProcessTupleOperator", "ProcessBatchOperator",
"ProcessTableOperator", "GenerateOperator"]);
Review Comment:
Two things here. (1) This matches the literal class name, so `class
MyOp(ProcessTupleOperator):` won't have its parameters parsed. Is that
intentional? If yes, please add a test that documents the behavior. If no, the
parser should also check base classes. (2) These four names are coupled to the
Python SDK. Please add a one-line comment pointing at where they're defined
upstream, so future maintainers know to keep them in sync.
##########
frontend/src/app/workspace/service/code-editor/ui-udf-parameters-sync.service.ts:
##########
@@ -0,0 +1,127 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { Injectable } from "@angular/core";
+import { isEqual } from "lodash-es";
+import { ReplaySubject } from "rxjs";
+import { WorkflowActionService } from
"../workflow-graph/model/workflow-action.service";
+import { UiUdfParameter, UiUdfParametersParserService } from
"./ui-udf-parameters-parser.service";
+import { isDefined } from "../../../common/util/predicate";
+import {
+ DUAL_INPUT_PORTS_PYTHON_UDF_V2_OP_TYPE,
+ PYTHON_UDF_SOURCE_V2_OP_TYPE,
+ PYTHON_UDF_V2_OP_TYPE,
+} from "../workflow-graph/model/workflow-graph";
+import { YType } from "../../types/shared-editing.interface";
+import type { Text as YText } from "yjs";
+
+@Injectable({ providedIn: "root" })
+export class UiUdfParametersSyncService {
+ private readonly uiParametersChangedSubject = new ReplaySubject<{
operatorId: string; parameters: UiUdfParameter[] }>(
+ 1
+ );
+
+ readonly uiParametersChanged$ =
this.uiParametersChangedSubject.asObservable();
+
+ constructor(
+ private workflowActionService: WorkflowActionService,
+ private uiUdfParametersParserService: UiUdfParametersParserService
+ ) {}
+
+ /**
+ * Attach directly to YText and sync whenever it changes
+ */
+ attachToYCode(operatorId: string, yCode: YText): () => void {
Review Comment:
Every keystroke triggers a full Lezer parse, a scan of the parsed tree, and
an `isEqual` check. Lezer is fast, but at typing speed in a long UDF body this
does more work than necessary. Please wrap the handler with `debounceTime(200)`
or similar. This is the standard pattern at the editor-to-graph boundary.
##########
frontend/src/app/workspace/service/code-editor/ui-udf-parameters-parser.service.ts:
##########
@@ -0,0 +1,217 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { Injectable } from "@angular/core";
+import { parser } from "@lezer/python";
+import { AttributeType, SchemaAttribute } from
"../../types/workflow-compiling.interface";
+
+export interface UiUdfParameter {
+ attribute: SchemaAttribute;
+ value: string;
+}
+
+const CLASSES = new Set(["ProcessTupleOperator", "ProcessBatchOperator",
"ProcessTableOperator", "GenerateOperator"]);
+
+// Java enum constant names (AttributeType.java)
+const JAVA_ATTRIBUTE_TYPE_NAMES = [
+ "STRING",
+ "INTEGER",
+ "LONG",
+ "DOUBLE",
+ "BOOLEAN",
+ "TIMESTAMP",
+ "BINARY",
+ "LARGE_BINARY",
+] as const;
+
+type JavaAttributeTypeName = (typeof JAVA_ATTRIBUTE_TYPE_NAMES)[number];
+
+// Python enum constant names (core.models.AttributeType)
+const PYTHON_ATTRIBUTE_TYPE_NAMES = [
+ "STRING",
+ "INT",
+ "LONG",
+ "DOUBLE",
+ "BOOL",
+ "TIMESTAMP",
+ "BINARY",
+ "LARGE_BINARY",
+] as const;
+
+type PythonAttributeTypeName = (typeof PYTHON_ATTRIBUTE_TYPE_NAMES)[number];
+
+type ParserAttributeTypeToken = JavaAttributeTypeName |
PythonAttributeTypeName;
+type UnsupportedParserAttributeTypeToken = "BINARY" | "LARGE_BINARY";
+type SupportedParserAttributeTypeToken = Exclude<ParserAttributeTypeToken,
UnsupportedParserAttributeTypeToken>;
+type ParserSyntaxNode = ReturnType<typeof parser.parse>["topNode"];
+
+const TYPES: Readonly<Record<SupportedParserAttributeTypeToken,
AttributeType>> = {
+ STRING: "string",
+ INTEGER: "integer",
+ INT: "integer",
+ LONG: "long",
+ DOUBLE: "double",
+ BOOLEAN: "boolean",
+ BOOL: "boolean",
+ TIMESTAMP: "timestamp",
+};
+
+const JAVA_ATTRIBUTE_TYPE_NAME_SET = new
Set<string>(JAVA_ATTRIBUTE_TYPE_NAMES);
+const PYTHON_ATTRIBUTE_TYPE_NAME_SET = new
Set<string>(PYTHON_ATTRIBUTE_TYPE_NAMES);
+const SUPPORTED_UI_PARAMETER_ATTRIBUTE_TYPES = new Set<AttributeType>([
+ "string",
+ "integer",
+ "long",
+ "double",
+ "boolean",
+ "timestamp",
+]);
+
+@Injectable({ providedIn: "root" })
+export class UiUdfParametersParserService {
+ parse(code: string): UiUdfParameter[] {
+ if (!code) return [];
+
+ const result: UiUdfParameter[] = [];
+ const seen = new Set<string>();
+ const add = (parameter?: UiUdfParameter): void => {
+ const name = parameter?.attribute.attributeName;
+ if (parameter && name && !seen.has(name)) {
+ seen.add(name);
+ result.push(parameter);
+ }
+ };
+
+ parser.parse(code).iterate({
+ enter: ({ name, node }) => {
+ const className = node.getChild("VariableName");
+ if (name !== "ClassDefinition" || !className ||
!CLASSES.has(code.slice(className.from, className.to))) return;
+ node
+ .cursor()
+ .iterate(ref => (ref.name === "CallExpression" ?
(add(readCall(ref.node, code)), false) : undefined));
+ return false;
+ },
+ });
+
+ return result;
+ }
+}
+
+function readCall(call: ParserSyntaxNode, code: string): UiUdfParameter |
undefined {
+ const args = call.getChild("ArgList");
+ if (!args || code.slice(call.from, args.from).replace(/\s+/g, "") !==
"self.UiParameter") return undefined;
+
+ let attributeName: string | undefined;
+ let attributeType: AttributeType | undefined;
+ let index = 0;
+ let sawNamed = false;
+
+ for (const arg of splitArgs(code.slice(args.from + 1, args.to - 1))) {
+ const match = arg.match(/^([A-Za-z_]\w*)\s*=\s*([\s\S]+)$/);
+ const key = match?.[1];
+ const value = match?.[2] ?? arg;
+
+ if (match) sawNamed = true;
+ else if (sawNamed || index > 1) return undefined;
+
+ if ((match ? key === "name" : index === 0) && !attributeName)
attributeName = readString(value)?.trim();
+ else if ((match ? key === "type" || key === "attr_type" : index === 1) &&
!attributeType)
+ attributeType = readType(value);
+ else return undefined;
+
+ if (!match) index++;
+ if (!attributeName && (key === "name" || (!match && index === 1))) return
undefined;
+ if (!attributeType && (key === "type" || key === "attr_type" || (!match &&
index === 2))) return undefined;
+ }
Review Comment:
This block is hard to read: three pieces of state (`match`, `key`, `index`)
and four interleaved conditions, with two post-block guards that fire *after*
`index` has been incremented, so they check at a different point than the
in-block branches. A future edit (like "now handle a third argument") would be
very risky.
If the AST suggestion from the `splitArgs` comment is applied, most of this
complexity disappears, since the loop will iterate over typed child nodes and
assign to a small accumulator. If not, please split this into a small helper
that parses one argument and returns `{ kind: 'name'|'type', value }`, and have
the loop dispatch on the result.
##########
frontend/src/app/workspace/service/code-editor/ui-udf-parameters-sync.service.ts:
##########
@@ -0,0 +1,127 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { Injectable } from "@angular/core";
+import { isEqual } from "lodash-es";
+import { ReplaySubject } from "rxjs";
+import { WorkflowActionService } from
"../workflow-graph/model/workflow-action.service";
+import { UiUdfParameter, UiUdfParametersParserService } from
"./ui-udf-parameters-parser.service";
+import { isDefined } from "../../../common/util/predicate";
+import {
+ DUAL_INPUT_PORTS_PYTHON_UDF_V2_OP_TYPE,
+ PYTHON_UDF_SOURCE_V2_OP_TYPE,
+ PYTHON_UDF_V2_OP_TYPE,
+} from "../workflow-graph/model/workflow-graph";
+import { YType } from "../../types/shared-editing.interface";
+import type { Text as YText } from "yjs";
+
+@Injectable({ providedIn: "root" })
+export class UiUdfParametersSyncService {
+ private readonly uiParametersChangedSubject = new ReplaySubject<{
operatorId: string; parameters: UiUdfParameter[] }>(
+ 1
+ );
+
+ readonly uiParametersChanged$ =
this.uiParametersChangedSubject.asObservable();
+
+ constructor(
+ private workflowActionService: WorkflowActionService,
+ private uiUdfParametersParserService: UiUdfParametersParserService
+ ) {}
+
+ /**
+ * Attach directly to YText and sync whenever it changes
+ */
+ attachToYCode(operatorId: string, yCode: YText): () => void {
+ const handler = () => {
+ const latestCode = yCode.toString();
+ this.syncStructureFromCode(operatorId, latestCode);
+ };
+
+ yCode.observe(handler);
+
+ handler();
+
+ // return cleanup function
+ return () => yCode.unobserve(handler);
+ }
+
+ syncStructureFromCode(operatorId: string, codeFromEditor?: string): void {
+ const operator =
this.workflowActionService.getTexeraGraph().getOperator(operatorId);
+
+ if (!operator || !this.isSupportedPythonUdfType(operator.operatorType)) {
+ return;
+ }
+
+ const code = codeFromEditor ?? this.getSharedCode(operatorId);
+ if (!isDefined(code)) {
+ return;
+ }
+
+ const existingParameters = operator.operatorProperties?.uiParameters ?? [];
+ const mergedUiParameters = this.buildParsedShapeWithPreservedValues(code,
existingParameters);
+
+ if (isEqual(existingParameters, mergedUiParameters)) {
+ return;
+ }
+
+ // Emit event so UI updates
+ this.uiParametersChangedSubject.next({
+ operatorId,
+ parameters: mergedUiParameters,
+ });
+ }
+
+ private buildParsedShapeWithPreservedValues(code: string,
existingParameters: any[]): UiUdfParameter[] {
+ const parsedParameters = this.uiUdfParametersParserService.parse(code);
+
+ const existingValues = new Map<string, string>();
+ existingParameters.forEach((parameter: any) => {
+ const parameterName = parameter?.attribute?.attributeName ??
parameter?.attribute?.name;
Review Comment:
What shape is the `?? parameter?.attribute?.name` branch handling? Is it
saved workflows from earlier in development? If so, please either write a
one-time migration and delete the fallback, or leave a TODO with a target
removal date. Carrying a legacy reader in brand-new code without a removal
target tends to stick around indefinitely. Also, please type
`existingParameters` as `UiUdfParameter[]` instead of `any[]`. TypeScript can
then catch shape drift on this boundary.
##########
frontend/src/app/workspace/service/code-editor/ui-udf-parameters-sync.service.ts:
##########
@@ -0,0 +1,127 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { Injectable } from "@angular/core";
+import { isEqual } from "lodash-es";
+import { ReplaySubject } from "rxjs";
+import { WorkflowActionService } from
"../workflow-graph/model/workflow-action.service";
+import { UiUdfParameter, UiUdfParametersParserService } from
"./ui-udf-parameters-parser.service";
+import { isDefined } from "../../../common/util/predicate";
+import {
+ DUAL_INPUT_PORTS_PYTHON_UDF_V2_OP_TYPE,
+ PYTHON_UDF_SOURCE_V2_OP_TYPE,
+ PYTHON_UDF_V2_OP_TYPE,
+} from "../workflow-graph/model/workflow-graph";
+import { YType } from "../../types/shared-editing.interface";
+import type { Text as YText } from "yjs";
+
+@Injectable({ providedIn: "root" })
+export class UiUdfParametersSyncService {
+ private readonly uiParametersChangedSubject = new ReplaySubject<{
operatorId: string; parameters: UiUdfParameter[] }>(
+ 1
+ );
+
+ readonly uiParametersChanged$ =
this.uiParametersChangedSubject.asObservable();
+
+ constructor(
+ private workflowActionService: WorkflowActionService,
+ private uiUdfParametersParserService: UiUdfParametersParserService
+ ) {}
+
+ /**
+ * Attach directly to YText and sync whenever it changes
+ */
+ attachToYCode(operatorId: string, yCode: YText): () => void {
+ const handler = () => {
+ const latestCode = yCode.toString();
+ this.syncStructureFromCode(operatorId, latestCode);
+ };
+
+ yCode.observe(handler);
+
+ handler();
+
+ // return cleanup function
+ return () => yCode.unobserve(handler);
+ }
+
+ syncStructureFromCode(operatorId: string, codeFromEditor?: string): void {
+ const operator =
this.workflowActionService.getTexeraGraph().getOperator(operatorId);
+
+ if (!operator || !this.isSupportedPythonUdfType(operator.operatorType)) {
+ return;
+ }
+
+ const code = codeFromEditor ?? this.getSharedCode(operatorId);
+ if (!isDefined(code)) {
+ return;
+ }
+
+ const existingParameters = operator.operatorProperties?.uiParameters ?? [];
+ const mergedUiParameters = this.buildParsedShapeWithPreservedValues(code,
existingParameters);
+
+ if (isEqual(existingParameters, mergedUiParameters)) {
+ return;
+ }
+
+ // Emit event so UI updates
+ this.uiParametersChangedSubject.next({
+ operatorId,
+ parameters: mergedUiParameters,
+ });
+ }
+
+ private buildParsedShapeWithPreservedValues(code: string,
existingParameters: any[]): UiUdfParameter[] {
+ const parsedParameters = this.uiUdfParametersParserService.parse(code);
+
+ const existingValues = new Map<string, string>();
+ existingParameters.forEach((parameter: any) => {
+ const parameterName = parameter?.attribute?.attributeName ??
parameter?.attribute?.name;
+
+ if (isDefined(parameterName) && isDefined(parameter?.value)) {
+ existingValues.set(parameterName, parameter.value);
+ }
+ });
+
+ return parsedParameters.map(parameter => ({
+ ...parameter,
+ value: existingValues.get(parameter.attribute.attributeName) ?? "",
+ }));
+ }
+
+ private getSharedCode(operatorId: string): string | undefined {
+ try {
+ const sharedOperatorType =
this.workflowActionService.getTexeraGraph().getSharedOperatorType(operatorId);
+
+ const operatorProperties = sharedOperatorType.get("operatorProperties")
as YType<
+ Readonly<{ [key: string]: any }>
+ >;
+
+ const yCode = operatorProperties.get("code") as YText;
+ return yCode?.toString();
+ } catch {
+ return undefined;
+ }
+ }
+
Review Comment:
This duplicates the operator-type list in `isPythonUdf(operator)` at
`workflow-graph.ts:84`. If a new Python UDF type gets added later, only one of
the two lists will get updated. Please refactor `isPythonUdf` to also accept an
operatorType string (or call it with the operator object that was already
fetched above), and delete this method.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]