diff --git a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/udf/python/DualInputPortsPythonUDFOpDescV2.scala b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/udf/python/DualInputPortsPythonUDFOpDescV2.scala index 4c4e3dc0098..f053130423f 100644 --- a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/udf/python/DualInputPortsPythonUDFOpDescV2.scala +++ b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/udf/python/DualInputPortsPythonUDFOpDescV2.scala @@ -77,6 +77,13 @@ class DualInputPortsPythonUDFOpDescV2 extends LogicalOp { ) var outputColumns: List[Attribute] = List() + @JsonProperty + @JsonSchemaTitle("Parameters") + @JsonPropertyDescription( + "Parameters inferred from active self.UiParameter(...) calls in the Python script" + ) + var uiParameters: List[UiUDFParameter] = List() + override def getPhysicalOp( workflowId: WorkflowIdentity, executionId: ExecutionIdentity diff --git a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/udf/python/PythonUDFOpDescV2.scala b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/udf/python/PythonUDFOpDescV2.scala index 88e8846b994..479b7564589 100644 --- a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/udf/python/PythonUDFOpDescV2.scala +++ b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/udf/python/PythonUDFOpDescV2.scala @@ -79,6 +79,13 @@ class PythonUDFOpDescV2 extends LogicalOp { ) var outputColumns: List[Attribute] = List() + @JsonProperty + @JsonSchemaTitle("Parameters") + @JsonPropertyDescription( + "Parameters inferred from active self.UiParameter(...) calls in the Python script" + ) + var uiParameters: List[UiUDFParameter] = List() + override def getPhysicalOp( workflowId: WorkflowIdentity, executionId: ExecutionIdentity diff --git a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/udf/python/UiUDFParameter.scala b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/udf/python/UiUDFParameter.scala new file mode 100644 index 00000000000..71ce2596788 --- /dev/null +++ b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/udf/python/UiUDFParameter.scala @@ -0,0 +1,41 @@ +/* + * 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. + */ + +package org.apache.texera.amber.operator.udf.python + +import com.fasterxml.jackson.annotation.JsonProperty +import com.kjetland.jackson.jsonSchema.annotations.JsonSchemaTitle +import org.apache.texera.amber.core.tuple.Attribute +import org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString + +import javax.validation.Valid +import javax.validation.constraints.NotNull + +class UiUDFParameter { + + @JsonProperty(required = true) + @JsonSchemaTitle("Attribute") + @Valid + @NotNull(message = "Attribute is required") + var attribute: Attribute = _ + + @JsonProperty() + @JsonSchemaTitle("Value") + var value: EncodableString = "" +} diff --git a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/udf/python/source/PythonUDFSourceOpDescV2.scala b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/udf/python/source/PythonUDFSourceOpDescV2.scala index b575612d884..2d4f4c53594 100644 --- a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/udf/python/source/PythonUDFSourceOpDescV2.scala +++ b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/udf/python/source/PythonUDFSourceOpDescV2.scala @@ -27,6 +27,7 @@ import org.apache.texera.amber.core.virtualidentity.{ExecutionIdentity, Workflow import org.apache.texera.amber.core.workflow.{OutputPort, PhysicalOp, SchemaPropagationFunc} import org.apache.texera.amber.operator.metadata.{OperatorGroupConstants, OperatorInfo} import org.apache.texera.amber.operator.source.SourceOperatorDescriptor +import org.apache.texera.amber.operator.udf.python.UiUDFParameter class PythonUDFSourceOpDescV2 extends SourceOperatorDescriptor { @@ -54,6 +55,13 @@ class PythonUDFSourceOpDescV2 extends SourceOperatorDescriptor { @JsonPropertyDescription("The columns of the source") var columns: List[Attribute] = List.empty + @JsonProperty + @JsonSchemaTitle("Parameters") + @JsonPropertyDescription( + "Parameters inferred from active self.UiParameter(...) calls in the Python script" + ) + var uiParameters: List[UiUDFParameter] = List() + override def getPhysicalOp( workflowId: WorkflowIdentity, executionId: ExecutionIdentity diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 6006fb22ad4..fa4b21de61f 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -156,6 +156,7 @@ import { NzTreeModule } from "ng-zorro-antd/tree"; import { NzTreeViewModule } from "ng-zorro-antd/tree-view"; import { NzNoAnimationModule } from "ng-zorro-antd/core/animation"; import { TreeModule } from "@ali-hm/angular-tree-component"; +import { UiUdfParametersComponent } from "./workspace/component/ui-udf-parameters/ui-udf-parameters.component"; import { ResultExportationComponent } from "./workspace/component/result-exportation/result-exportation.component"; import { ReportGenerationService } from "./workspace/service/report-generation/report-generation.service"; import { SearchBarComponent } from "./dashboard/component/user/search-bar/search-bar.component"; @@ -257,6 +258,7 @@ registerLocaleData(en); AgentPanelComponent, AgentChatComponent, AgentRegistrationComponent, + UiUdfParametersComponent, AgentInteractionComponent, DatasetFileSelectorComponent, DatasetVersionSelectorComponent, diff --git a/frontend/src/app/common/formly/formly-config.ts b/frontend/src/app/common/formly/formly-config.ts index c3995abb544..707ddfa7975 100644 --- a/frontend/src/app/common/formly/formly-config.ts +++ b/frontend/src/app/common/formly/formly-config.ts @@ -27,6 +27,7 @@ import { PresetWrapperComponent } from "./preset-wrapper/preset-wrapper.componen import { DatasetFileSelectorComponent } from "../../workspace/component/dataset-file-selector/dataset-file-selector.component"; import { CollabWrapperComponent } from "./collab-wrapper/collab-wrapper/collab-wrapper.component"; import { FormlyRepeatDndComponent } from "./repeat-dnd/repeat-dnd.component"; +import { UiUdfParametersComponent } from "../../workspace/component/ui-udf-parameters/ui-udf-parameters.component"; import { DatasetVersionSelectorComponent } from "../../workspace/component/dataset-version-selector/dataset-version-selector.component"; /** @@ -80,6 +81,7 @@ export const TEXERA_FORMLY_CONFIG = { { name: "inputautocomplete", component: DatasetFileSelectorComponent, wrappers: ["form-field"] }, { name: "datasetversionselector", component: DatasetVersionSelectorComponent, wrappers: ["form-field"] }, { name: "repeat-section-dnd", component: FormlyRepeatDndComponent }, + { name: "ui-udf-parameters", component: UiUdfParametersComponent, wrappers: ["form-field"] }, ], wrappers: [ { name: "preset-wrapper", component: PresetWrapperComponent }, diff --git a/frontend/src/app/workspace/component/property-editor/operator-property-edit-frame/operator-property-edit-frame.component.scss b/frontend/src/app/workspace/component/property-editor/operator-property-edit-frame/operator-property-edit-frame.component.scss index 4126a9ee1ce..aa32d22b4aa 100644 --- a/frontend/src/app/workspace/component/property-editor/operator-property-edit-frame/operator-property-edit-frame.component.scss +++ b/frontend/src/app/workspace/component/property-editor/operator-property-edit-frame/operator-property-edit-frame.component.scss @@ -73,3 +73,17 @@ margin-bottom: 0; } } + +/* ================================ + Style ONLY the UDF Parameters field + ================================ */ + +:host ::ng-deep label[for*="ui-udf-parameters"] { + font-weight: 700; +} + +:host ::ng-deep nz-form-item:has(label[for*="ui-udf-parameters"]) { + border-top: 1.5px solid #d1d1d1; + padding-top: 12px; + margin-top: 8px; +} diff --git a/frontend/src/app/workspace/component/property-editor/operator-property-edit-frame/operator-property-edit-frame.component.ts b/frontend/src/app/workspace/component/property-editor/operator-property-edit-frame/operator-property-edit-frame.component.ts index da62034ed85..00fd4c38f07 100644 --- a/frontend/src/app/workspace/component/property-editor/operator-property-edit-frame/operator-property-edit-frame.component.ts +++ b/frontend/src/app/workspace/component/property-editor/operator-property-edit-frame/operator-property-edit-frame.component.ts @@ -58,7 +58,6 @@ import * as Y from "yjs"; import { OperatorSchema } from "src/app/workspace/types/operator-schema.interface"; import { AttributeType, PortSchema } from "../../../types/workflow-compiling.interface"; import { GuiConfigService } from "../../../../common/service/gui-config.service"; - Quill.register("modules/cursors", QuillCursors); /** @@ -443,7 +442,9 @@ export class OperatorPropertyEditFrameComponent implements OnInit, OnChanges, On if (mappedField.key === "fileName") { mappedField.type = "inputautocomplete"; } - + if (mappedField.key === "uiParameters") { + mappedField.type = "ui-udf-parameters"; + } if (mappedField.key === "datasetVersionPath") { mappedField.type = "datasetversionselector"; } diff --git a/frontend/src/app/workspace/component/ui-udf-parameters/ui-udf-parameters.component.html b/frontend/src/app/workspace/component/ui-udf-parameters/ui-udf-parameters.component.html new file mode 100644 index 00000000000..0ecfb16a780 --- /dev/null +++ b/frontend/src/app/workspace/component/ui-udf-parameters/ui-udf-parameters.component.html @@ -0,0 +1,55 @@ + +
+ +
+
Value
+
Name
+
Type
+
+ +
+ + +
+ + + +
+ + +
+ + + +
+ + +
+ + + +
+
+
+
diff --git a/frontend/src/app/workspace/component/ui-udf-parameters/ui-udf-parameters.component.scss b/frontend/src/app/workspace/component/ui-udf-parameters/ui-udf-parameters.component.scss new file mode 100644 index 00000000000..901edaf1fc9 --- /dev/null +++ b/frontend/src/app/workspace/component/ui-udf-parameters/ui-udf-parameters.component.scss @@ -0,0 +1,39 @@ +/** + * 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. + */ + +.ui-udf-param-row { + display: grid; + grid-template-columns: 250px 250px 1fr; + gap: 12px; + align-items: start; +} + +.field-cell { + min-width: 0; +} + +/* Remove Formly/Ant label spacing */ +:host ::ng-deep .ant-form-item { + margin-bottom: 0; +} + +/* Hide Formly labels*/ +:host ::ng-deep .ant-form-item-label { + display: none; +} diff --git a/frontend/src/app/workspace/component/ui-udf-parameters/ui-udf-parameters.component.ts b/frontend/src/app/workspace/component/ui-udf-parameters/ui-udf-parameters.component.ts new file mode 100644 index 00000000000..0d9b774fc15 --- /dev/null +++ b/frontend/src/app/workspace/component/ui-udf-parameters/ui-udf-parameters.component.ts @@ -0,0 +1,91 @@ +/** + * 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 { Component } from "@angular/core"; +import { FieldArrayType, FormlyFieldConfig } from "@ngx-formly/core"; + +@Component({ + selector: "texera-ui-udf-parameters", + templateUrl: "./ui-udf-parameters.component.html", + styleUrls: ["./ui-udf-parameters.component.scss"], +}) +export class UiUdfParametersComponent extends FieldArrayType { + private getField(rowField: FormlyFieldConfig, key: string): FormlyFieldConfig | undefined { + return rowField.fieldGroup?.find(f => f.key === key); + } + + private getAttributeChild(rowField: FormlyFieldConfig, childKey: string): FormlyFieldConfig | undefined { + const attributeGroup = this.getField(rowField, "attribute"); + return attributeGroup?.fieldGroup?.find(f => f.key === childKey); + } + + private setDisabled(field: FormlyFieldConfig | undefined, disabled: boolean): FormlyFieldConfig | undefined { + if (!field) return undefined; + + // 1) Modern Formly + field.props = { ...(field.props ?? {}), disabled }; + + // 2) Compatibility for templates/wrappers still using templateOptions + // (`as any` so we don't get nagged by the @deprecated JSDoc) + (field as any).templateOptions = { ...((field as any).templateOptions ?? {}), disabled }; + + // 3) Enforce at the reactive form level + if (field.formControl) { + if (disabled) { + field.formControl.disable({ emitEvent: false }); + } else { + field.formControl.enable({ emitEvent: false }); + } + } else { + // If control isn't created yet, disable it at init time. + const prevOnInit = field.hooks?.onInit; + field.hooks = { + ...(field.hooks ?? {}), + onInit: f => { + prevOnInit?.(f); + if (disabled) { + f.formControl?.disable({ emitEvent: false }); + } else { + f.formControl?.enable({ emitEvent: false }); + } + }, + }; + } + + return field; + } + + // Disable Name + getNameField(rowField: FormlyFieldConfig): FormlyFieldConfig | undefined { + return this.setDisabled(this.getAttributeChild(rowField, "attributeName"), true); + } + + // Disable Type + getTypeField(rowField: FormlyFieldConfig): FormlyFieldConfig | undefined { + return this.setDisabled(this.getAttributeChild(rowField, "attributeType"), true); + } + + // Value editable + getValueField(rowField: FormlyFieldConfig): FormlyFieldConfig | undefined { + return this.setDisabled(this.getField(rowField, "value"), false); + } + + trackByParamName = (index: number, param: any): string | number => { + return param?.attribute?.attributeName ?? index; + }; +}