From eb923412b65b05f88c429921ac65eb09987ff5e1 Mon Sep 17 00:00:00 2001 From: Boli Guan Date: Tue, 3 Jan 2023 16:16:09 +0800 Subject: [PATCH 1/3] Create nww feature form Signed-off-by: Boli Guan --- ui/src/api/api.tsx | 38 ++ ui/src/components/ProjectsSelect/index.tsx | 2 +- ui/src/models/model.ts | 41 ++- .../feature/components/FeatureForm/index.tsx | 343 +++++++++++++++--- .../feature/components/FeatureForm/useForm.ts | 167 +++++++++ ui/src/pages/feature/newFeature.tsx | 2 +- 6 files changed, 527 insertions(+), 66 deletions(-) create mode 100644 ui/src/pages/feature/components/FeatureForm/useForm.ts diff --git a/ui/src/api/api.tsx b/ui/src/api/api.tsx index 3fb08bad8..3e575d792 100644 --- a/ui/src/api/api.tsx +++ b/ui/src/api/api.tsx @@ -3,6 +3,7 @@ import { DataSource, Feature, FeatureLineage, + NewFeature, Role, UserRole, } from "../models/model"; @@ -262,3 +263,40 @@ export const getDependent = async (entity: string) => { return error.response; }); }; + +export const createAnchorFeature = async ( + project: string, + anchor: string, + anchorFeature: NewFeature +) => { + const axios = await authAxios(msalInstance); + return axios + .post( + `${getApiBaseUrl()}/projects/${project}/anchors/${anchor}/features`, + anchorFeature + ) + .then((response) => { + return response; + }) + .catch((error) => { + return error.response; + }); +}; + +export const createDerivedFeature = async ( + project: string, + derivedFeature: NewFeature +) => { + const axios = await authAxios(msalInstance); + return axios + .post( + `${getApiBaseUrl()}/projects/${project}/derivedfeatures`, + derivedFeature + ) + .then((response) => { + return response; + }) + .catch((error) => { + return error.response; + }); +}; diff --git a/ui/src/components/ProjectsSelect/index.tsx b/ui/src/components/ProjectsSelect/index.tsx index ca5fddf9f..8b3384118 100644 --- a/ui/src/components/ProjectsSelect/index.tsx +++ b/ui/src/components/ProjectsSelect/index.tsx @@ -4,7 +4,7 @@ import { fetchProjects } from "@/api"; import { useQuery } from "react-query"; export interface ProjectsSelectProps { - width?: number; + width?: number | string; defaultValue?: string; onChange?: (value: string) => void; } diff --git a/ui/src/models/model.ts b/ui/src/models/model.ts index b6da49361..57c2fbdf7 100644 --- a/ui/src/models/model.ts +++ b/ui/src/models/model.ts @@ -33,13 +33,14 @@ export interface FeatureType { } export interface FeatureTransformation { - transformExpr: string; - filter: string; - aggFunc: string; - limit: string; - groupBy: string; - window: string; - defExpr: string; + transformExpr?: string; + filter?: string; + aggFunc?: string; + limit?: string; + groupBy?: string; + window?: string; + defExpr?: string; + udfExpr?: string; } export interface FeatureKey { @@ -117,3 +118,29 @@ export interface Role { roleName: string; reason: string; } + +export interface NewFeature { + name: string; + featureType: FeatureType; + transformation: FeatureTransformation; + key?: FeatureKey[]; + tags?: Object; + inputAnchorFeatures?: string[]; + inputDerivedFeatures?: string[]; + // qualifiedName: string; +} + +export const ValueType = [ + "UNSPECIFIED", + "BOOLEAN", + "INT", + "LONG", + "FLOAT", + "DOUBLE", + "STRING", + "BYTES", +]; + +export const TensorCategory = ["DENSE", "SPARSE"]; + +export const VectorType = ["TENSOR"]; diff --git a/ui/src/pages/feature/components/FeatureForm/index.tsx b/ui/src/pages/feature/components/FeatureForm/index.tsx index 02f33fe8d..fdc41bfe0 100644 --- a/ui/src/pages/feature/components/FeatureForm/index.tsx +++ b/ui/src/pages/feature/components/FeatureForm/index.tsx @@ -1,71 +1,300 @@ -import React, { forwardRef, useEffect, useState } from "react"; -import { Button, Form, Input, message } from "antd"; -import { useNavigate } from "react-router-dom"; -import { createFeature, updateFeature } from "@/api"; -import { FeatureAttributes, Feature } from "@/models/model"; +import React, { forwardRef, Fragment } from "react"; +import { Button, Divider, Form, Input, Radio, Select, Space } from "antd"; +import ProjectsSelect from "@/components/ProjectsSelect"; +import { MinusCircleOutlined, PlusOutlined } from "@ant-design/icons"; +import { useBasicForm } from "./useForm"; -export interface FeatureFormProps { - isNew: boolean; - editMode: boolean; - feature?: FeatureAttributes; -} +export interface FeatureFormProps {} -const FeatureForm = (props: FeatureFormProps, ref: any) => { - const navigate = useNavigate(); - - const { isNew, editMode, feature } = props; +const { Item } = Form; - const [createLoading, setCreateLoading] = useState(false); +const formItemLayout = { + labelCol: { + xs: { span: 24 }, + sm: { span: 8 }, + }, + wrapperCol: { + xs: { span: 24 }, + sm: { span: 16 }, + }, +}; +const FeatureForm = (props: FeatureFormProps, ref: any) => { const [form] = Form.useForm(); - const handleFinish = async (values: Feature) => { - setCreateLoading(true); - try { - if (isNew) { - await createFeature(values); - message.success("New feature created"); - } else if (feature?.qualifiedName) { - values.guid = feature.qualifiedName; - await updateFeature(values); - message.success("Feature is updated successfully"); - } - navigate("/features"); - } catch (err: any) { - message.error(err.detail || err.message, 8); - } finally { - setCreateLoading(false); - } - }; - - useEffect(() => { - if (feature) { - form.setFieldsValue(feature); - } - }, [feature, form]); + const { + createLoading, + loading, + featureType, + selectTransformationType, + anchorOptions, + anchorFeatureOptions, + derivedFeatureOptions, + valueOptions, + tensorOptions, + typeOptions, + onFinish, + } = useBasicForm(form); return ( <>
- - - - - - - - - - - - - + + + + + + + + + Anchor Feature + Derived Feature + + + {featureType === 1 ? ( + <> + + + + + + + + + + remove(name)} /> + + + + ))} + + + + + + )} + + Feature Keys + + {(fields, { add, remove }) => ( + <> + {fields.map(({ key, name }, index) => ( + + +
+ + + + remove(name)} /> +
+
+ + + + + + + + + + +
+ ))} + + + + + )} +
+ Feature Type + + + + + + + + + + Transformation + + + + Expression Transformation + Window Transformation + UDF Transformation + + + + {selectTransformationType === 1 && ( + + + + )} + {selectTransformationType === 2 && ( + <> + + + + + + + + + + + + + + + + + + + + )} + {selectTransformationType === 3 && ( + + + + )} + + - + ); diff --git a/ui/src/pages/feature/components/FeatureForm/useForm.ts b/ui/src/pages/feature/components/FeatureForm/useForm.ts new file mode 100644 index 000000000..117daab4e --- /dev/null +++ b/ui/src/pages/feature/components/FeatureForm/useForm.ts @@ -0,0 +1,167 @@ +import { useEffect, useState } from "react"; +import { FormInstance, Form, SelectProps, message } from "antd"; +import { useNavigate } from "react-router-dom"; +import { + fetchProjectLineages, + createAnchorFeature, + createDerivedFeature, +} from "@/api"; +import { + ValueType, + TensorCategory, + VectorType, + NewFeature, +} from "@/models/model"; + +const valueOptions = ValueType.map((value: string) => ({ + value: value, + label: value, +})); + +const tensorOptions = TensorCategory.map((value: string) => ({ + value: value, + label: value, +})); + +const typeOptions = VectorType.map((value: string) => ({ + value: value, + label: value, +})); + +export type Options = SelectProps["options"]; + +export const useBasicForm = (form: FormInstance) => { + const navigate = useNavigate(); + + const [createLoading, setCreateLoading] = useState(false); + + const [loading, setLoading] = useState(false); + + const [anchorOptions, setAnchorOptions] = useState([]); + const [anchorFeatureOptions, setAnchorFeatureOptions] = useState([]); + const [derivedFeatureOptions, setDerivedFeatureOptions] = useState( + [] + ); + + const project = Form.useWatch("project", form); + const featureType = Form.useWatch("featureType", form); + const selectTransformationType = Form.useWatch( + "selectTransformationType", + form + ); + + const fetchData = async (project: string) => { + try { + setLoading(true); + form.setFieldValue("anchor", undefined); + form.setFieldValue("anchorFeatures", undefined); + form.setFieldValue("derivedFeatures", undefined); + const { guidEntityMap } = await fetchProjectLineages(project); + if (guidEntityMap) { + const anchorOptions: Options = []; + const anchorFeatureOptions: Options = []; + const derivedFeatureOptions: Options = []; + + Object.values(guidEntityMap).forEach((value: any) => { + const { guid, typeName, attributes } = value; + const { name } = attributes; + switch (typeName) { + case "feathr_anchor_v1": + anchorOptions.push({ value: guid, label: name }); + break; + case "feathr_anchor_feature_v1": + anchorFeatureOptions.push({ value: guid, label: name }); + break; + case "feathr_derived_feature_v1": + derivedFeatureOptions.push({ value: guid, label: name }); + break; + default: + break; + } + }); + + setAnchorOptions(anchorOptions); + setAnchorFeatureOptions(anchorFeatureOptions); + setDerivedFeatureOptions(derivedFeatureOptions); + } + } catch { + // + } finally { + setLoading(false); + } + }; + + const onFinish = async (values: any) => { + setCreateLoading(true); + try { + const tags = values.tags?.reduce((tags: any, item: any) => { + tags[item.name] = item.value; + return tags; + }, {} as any); + + const newFeature: NewFeature = { + name: values.name, + featureType: { + dimensionType: values.dimensionType, + tensorCategory: values.tensorCategory, + type: values.type, + valType: values.valType, + }, + tags, + key: values.keys, + inputAnchorFeatures: values.anchorFeatures, + inputDerivedFeatures: values.derivedFeatures, + transformation: { + transformExpr: values.transformExpr, + filter: values.filter, + aggFunc: values.aggFunc, + limit: values.limit, + groupBy: values.groupBy, + window: values.window, + defExpr: values.defExpr, + udfExpr: values.udfExpr, + }, + }; + + if (values.featureType === 1) { + await createAnchorFeature(project, values.anchor, newFeature); + } else { + await createDerivedFeature(project, newFeature); + } + message.success("New feature created"); + navigate(`/features?project=${project}`); + } catch (err: any) { + message.error(err.detail || err.message); + } finally { + setCreateLoading(false); + } + }; + + useEffect(() => { + if (project) { + fetchData(project); + } + }, [project]); + + useEffect(() => { + form.setFieldsValue({ + featureType: 1, + selectTransformationType: 1, + }); + }, [form]); + + return { + createLoading, + loading, + project, + featureType, + selectTransformationType, + anchorOptions, + anchorFeatureOptions, + derivedFeatureOptions, + valueOptions, + tensorOptions, + typeOptions, + onFinish, + }; +}; diff --git a/ui/src/pages/feature/newFeature.tsx b/ui/src/pages/feature/newFeature.tsx index 50afd64c3..851f4d035 100644 --- a/ui/src/pages/feature/newFeature.tsx +++ b/ui/src/pages/feature/newFeature.tsx @@ -6,7 +6,7 @@ const NewFeature = () => { return (
- +
); From 8de611bb106bc38c8e3c60da7ee90743b9eb79d0 Mon Sep 17 00:00:00 2001 From: Boli Guan Date: Wed, 4 Jan 2023 16:16:14 +0800 Subject: [PATCH 2/3] Update ui/.env.development Signed-off-by: Boli Guan --- ui/.env.development | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/.env.development b/ui/.env.development index 0c6c0e061..ff066fbd4 100644 --- a/ui/.env.development +++ b/ui/.env.development @@ -1,3 +1,3 @@ REACT_APP_AZURE_TENANT_ID=common -REACT_APP_API_ENDPOINT=http://127.0.0.1:8000 +REACT_APP_API_ENDPOINT=https://feathr-sql-registry.azurewebsites.net REACT_APP_ENABLE_RBAC=false From 980b3bd160c553ca5261c99b8e1d7b8c60ab4e54 Mon Sep 17 00:00:00 2001 From: Boli Guan Date: Thu, 5 Jan 2023 16:52:16 +0800 Subject: [PATCH 3/3] Fix show the 400 error message. Signed-off-by: Boli Guan --- ui/src/api/api.tsx | 23 +++------------- .../components/FeatureForm/index.tsx | 20 +++++++------- .../components/FeatureForm/useForm.ts | 26 ++++++++++++++----- 3 files changed, 33 insertions(+), 36 deletions(-) diff --git a/ui/src/api/api.tsx b/ui/src/api/api.tsx index dd6f2a7ed..6b7bb7dcb 100644 --- a/ui/src/api/api.tsx +++ b/ui/src/api/api.tsx @@ -150,9 +150,6 @@ export const addUserRole = async (role: Role) => { .then((response) => { return response }) - .catch((error) => { - return error.response - }) } export const deleteUserRole = async (userrole: UserRole) => { @@ -170,9 +167,6 @@ export const deleteUserRole = async (userrole: UserRole) => { .then((response) => { return response }) - .catch((error) => { - return error.response - }) } export const getIdToken = async (msalInstance: PublicClientApplication): Promise => { @@ -238,14 +232,9 @@ export const deleteEntity = async (enity: string) => { export const getDependent = async (entity: string) => { const axios = await authAxios(msalInstance) - return await axios - .get(`${getApiBaseUrl()}/dependent/${entity}`) - .then((response) => { - return response - }) - .catch((error) => { - return error.response - }) + return await axios.get(`${getApiBaseUrl()}/dependent/${entity}`).then((response) => { + return response + }) } export const createAnchorFeature = async ( @@ -259,9 +248,6 @@ export const createAnchorFeature = async ( .then((response) => { return response }) - .catch((error) => { - return error.response - }) } export const createDerivedFeature = async (project: string, derivedFeature: NewFeature) => { @@ -271,7 +257,4 @@ export const createDerivedFeature = async (project: string, derivedFeature: NewF .then((response) => { return response }) - .catch((error) => { - return error.response - }) } diff --git a/ui/src/pages/NewFeature/components/FeatureForm/index.tsx b/ui/src/pages/NewFeature/components/FeatureForm/index.tsx index 980db91ce..b6fca166b 100644 --- a/ui/src/pages/NewFeature/components/FeatureForm/index.tsx +++ b/ui/src/pages/NewFeature/components/FeatureForm/index.tsx @@ -5,7 +5,7 @@ import { Button, Divider, Form, Input, Radio, Select, Space } from 'antd' import ProjectsSelect from '@/components/ProjectsSelect' -import { useForm } from './useForm' +import { useForm, FeatureEnum, TransformationTypeEnum } from './useForm' export interface FeatureFormProps {} @@ -65,11 +65,11 @@ const FeatureForm = (props: FeatureFormProps, ref: any) => { - Anchor Feature - Derived Feature + Anchor Feature + Derived Feature - {featureType === 1 ? ( + {featureType === FeatureEnum.Anchor ? ( <> { - Expression Transformation - Window Transformation - UDF Transformation + Expression Transformation + Window Transformation + UDF Transformation - {selectTransformationType === 1 && ( + {selectTransformationType === TransformationTypeEnum.Expression && ( { )} - {selectTransformationType === 2 && ( + {selectTransformationType === TransformationTypeEnum.Window && ( <> { )} - {selectTransformationType === 3 && ( + {selectTransformationType === TransformationTypeEnum.UDF && ( diff --git a/ui/src/pages/NewFeature/components/FeatureForm/useForm.ts b/ui/src/pages/NewFeature/components/FeatureForm/useForm.ts index e36a3df24..73816882e 100644 --- a/ui/src/pages/NewFeature/components/FeatureForm/useForm.ts +++ b/ui/src/pages/NewFeature/components/FeatureForm/useForm.ts @@ -23,6 +23,17 @@ const typeOptions = VectorType.map((value: string) => ({ export type Options = SelectProps['options'] +export const enum FeatureEnum { + Anchor, + Derived +} + +export const enum TransformationTypeEnum { + Expression, + Window, + UDF +} + export const useForm = (form: FormInstance) => { const navigate = useNavigate() @@ -35,8 +46,11 @@ export const useForm = (form: FormInstance) => { const [derivedFeatureOptions, setDerivedFeatureOptions] = useState([]) const project = Form.useWatch('project', form) - const featureType = Form.useWatch('featureType', form) - const selectTransformationType = Form.useWatch('selectTransformationType', form) + const featureType = Form.useWatch('featureType', form) + const selectTransformationType = Form.useWatch( + 'selectTransformationType', + form + ) const fetchData = async (project: string) => { try { @@ -83,7 +97,7 @@ export const useForm = (form: FormInstance) => { setCreateLoading(true) try { const tags = values.tags?.reduce((tags: any, item: any) => { - tags[item.name] = item.value + tags[item.name] = item.value || '' return tags }, {} as any) @@ -111,7 +125,7 @@ export const useForm = (form: FormInstance) => { } } - if (values.featureType === 1) { + if (values.featureType === FeatureEnum.Anchor) { await createAnchorFeature(project, values.anchor, newFeature) } else { await createDerivedFeature(project, newFeature) @@ -133,8 +147,8 @@ export const useForm = (form: FormInstance) => { useEffect(() => { form.setFieldsValue({ - featureType: 1, - selectTransformationType: 1 + featureType: FeatureEnum.Anchor, + selectTransformationType: TransformationTypeEnum.Expression }) }, [form])