Skip to content

Commit fe2c749

Browse files
feat: Add UI REST integration for data sources and feature views
Co-authored-by: Cursor <cursoragent@cursor.com> Signed-off-by: Rohit Bharmal <rohitbharmal123@gmail.com>
1 parent 5f35a0f commit fe2c749

15 files changed

Lines changed: 1057 additions & 375 deletions

ui/src/components/FeatureViewFormModal.tsx

Lines changed: 264 additions & 114 deletions
Large diffs are not rendered by default.

ui/src/pages/data-sources/DataSourceOverviewTab.tsx

Lines changed: 135 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -15,56 +15,115 @@ import {
1515
EuiDescriptionListDescription,
1616
EuiSpacer,
1717
} from "@elastic/eui";
18-
import React, { useContext, useState } from "react";
18+
import React, { useState } from "react";
1919
import { useParams } from "react-router-dom";
20-
import PermissionsDisplay from "../../components/PermissionsDisplay";
2120
import DataSourceFormModal, {
2221
DataSourceFormData,
2322
} from "../../components/DataSourceFormModal";
24-
import RegistryPathContext from "../../contexts/RegistryPathContext";
25-
import { FEAST_FCO_TYPES } from "../../parsers/types";
2623
import { feast } from "../../protos";
27-
import useLoadRegistry from "../../queries/useLoadRegistry";
28-
import { getEntityPermissions } from "../../utils/permissionUtils";
24+
import { useApplyDataSource } from "../../queries/mutations/useDataSourceMutations";
2925
import BatchSourcePropertiesView from "./BatchSourcePropertiesView";
3026
import FeatureViewEdgesList from "../entities/FeatureViewEdgesList";
3127
import RequestDataSourceSchemaTable from "./RequestDataSourceSchemaTable";
3228
import useLoadDataSource from "./useLoadDataSource";
3329
import { useUIVersion } from "../../contexts/UIVersionContext";
3430

35-
const buildEditFormData = (ds: feast.core.IDataSource): DataSourceFormData => {
36-
const tags = ds.tags
37-
? Object.entries(ds.tags).map(([key, value]) => ({ key, value }))
31+
const buildEditFormData = (ds: any): DataSourceFormData => {
32+
const spec = ds.spec || ds;
33+
const tags = spec.tags
34+
? Object.entries(spec.tags).map(([key, value]) => ({
35+
key,
36+
value: value as string,
37+
}))
3838
: [];
3939

4040
return {
41-
name: ds.name || "",
42-
description: ds.description || "",
43-
owner: ds.owner || "",
44-
sourceType: String(ds.type ?? 0),
45-
timestampField: ds.timestampField || "",
46-
createdTimestampColumn: ds.createdTimestampColumn || "",
41+
name: spec.name || ds.name || "",
42+
description: spec.description || ds.description || "",
43+
owner: spec.owner || ds.owner || "",
44+
sourceType: String(spec.type ?? ds.type ?? 0),
45+
timestampField: spec.timestampField || ds.timestampField || "",
46+
createdTimestampColumn:
47+
spec.createdTimestampColumn || ds.createdTimestampColumn || "",
4748
tags,
48-
fileUri: ds.fileOptions?.uri || "",
49-
bigqueryTable: ds.bigqueryOptions?.table || "",
50-
bigqueryQuery: ds.bigqueryOptions?.query || "",
51-
snowflakeTable: ds.snowflakeOptions?.table || "",
52-
snowflakeDatabase: ds.snowflakeOptions?.database || "",
53-
snowflakeSchema: ds.snowflakeOptions?.schema || "",
54-
redshiftTable: ds.redshiftOptions?.table || "",
55-
redshiftDatabase: ds.redshiftOptions?.database || "",
56-
redshiftSchema: ds.redshiftOptions?.schema || "",
57-
kafkaBootstrapServers: ds.kafkaOptions?.kafkaBootstrapServers || "",
58-
kafkaTopic: ds.kafkaOptions?.topic || "",
59-
sparkTable: ds.sparkOptions?.table || "",
60-
sparkPath: ds.sparkOptions?.path || "",
49+
fileUri: spec.fileOptions?.uri || ds.fileOptions?.uri || "",
50+
bigqueryTable:
51+
spec.bigqueryOptions?.table || ds.bigqueryOptions?.table || "",
52+
bigqueryQuery:
53+
spec.bigqueryOptions?.query || ds.bigqueryOptions?.query || "",
54+
snowflakeTable:
55+
spec.snowflakeOptions?.table || ds.snowflakeOptions?.table || "",
56+
snowflakeDatabase:
57+
spec.snowflakeOptions?.database || ds.snowflakeOptions?.database || "",
58+
snowflakeSchema:
59+
spec.snowflakeOptions?.schema || ds.snowflakeOptions?.schema || "",
60+
redshiftTable:
61+
spec.redshiftOptions?.table || ds.redshiftOptions?.table || "",
62+
redshiftDatabase:
63+
spec.redshiftOptions?.database || ds.redshiftOptions?.database || "",
64+
redshiftSchema:
65+
spec.redshiftOptions?.schema || ds.redshiftOptions?.schema || "",
66+
kafkaBootstrapServers:
67+
spec.kafkaOptions?.kafkaBootstrapServers ||
68+
ds.kafkaOptions?.kafkaBootstrapServers ||
69+
"",
70+
kafkaTopic: spec.kafkaOptions?.topic || ds.kafkaOptions?.topic || "",
71+
sparkTable: spec.sparkOptions?.table || ds.sparkOptions?.table || "",
72+
sparkPath: spec.sparkOptions?.path || ds.sparkOptions?.path || "",
6173
};
6274
};
6375

76+
const formDataToPayload = (formData: DataSourceFormData, project: string) => {
77+
const payload: Record<string, any> = {
78+
name: formData.name,
79+
project,
80+
type: parseInt(formData.sourceType, 10),
81+
timestamp_field: formData.timestampField,
82+
created_timestamp_column: formData.createdTimestampColumn,
83+
description: formData.description,
84+
owner: formData.owner,
85+
tags: Object.fromEntries(
86+
formData.tags.filter((t) => t.key.trim()).map((t) => [t.key, t.value]),
87+
),
88+
};
89+
90+
const st = formData.sourceType;
91+
if (st === String(feast.core.DataSource.SourceType.BATCH_FILE)) {
92+
payload.file_options = { uri: formData.fileUri };
93+
} else if (st === String(feast.core.DataSource.SourceType.BATCH_BIGQUERY)) {
94+
payload.bigquery_options = {
95+
table: formData.bigqueryTable,
96+
query: formData.bigqueryQuery,
97+
};
98+
} else if (st === String(feast.core.DataSource.SourceType.BATCH_SNOWFLAKE)) {
99+
payload.snowflake_options = {
100+
table: formData.snowflakeTable,
101+
database: formData.snowflakeDatabase,
102+
schema_: formData.snowflakeSchema,
103+
};
104+
} else if (st === String(feast.core.DataSource.SourceType.BATCH_REDSHIFT)) {
105+
payload.redshift_options = {
106+
table: formData.redshiftTable,
107+
database: formData.redshiftDatabase,
108+
schema_: formData.redshiftSchema,
109+
};
110+
} else if (st === String(feast.core.DataSource.SourceType.STREAM_KAFKA)) {
111+
payload.kafka_options = {
112+
kafka_bootstrap_servers: formData.kafkaBootstrapServers,
113+
topic: formData.kafkaTopic,
114+
};
115+
} else if (st === String(feast.core.DataSource.SourceType.BATCH_SPARK)) {
116+
payload.spark_options = {
117+
table: formData.sparkTable,
118+
path: formData.sparkPath,
119+
};
120+
}
121+
122+
return payload;
123+
};
124+
64125
const DataSourceOverviewTab = () => {
65-
let { dataSourceName, projectName } = useParams();
66-
const registryUrl = useContext(RegistryPathContext);
67-
const registryQuery = useLoadRegistry(registryUrl, projectName);
126+
const { dataSourceName, projectName } = useParams();
68127

69128
const dsName = dataSourceName === undefined ? "" : dataSourceName;
70129
const { isLoading, isSuccess, isError, data, consumingFeatureViews } =
@@ -74,16 +133,32 @@ const DataSourceOverviewTab = () => {
74133
const { isV2 } = useUIVersion();
75134
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
76135
const [successMessage, setSuccessMessage] = useState<string | null>(null);
136+
const [errorMessage, setErrorMessage] = useState<string | null>(null);
137+
const applyDataSource = useApplyDataSource();
77138

78139
const handleEditSubmit = (formData: DataSourceFormData) => {
79-
console.log("Data source edit payload:", formData);
80-
setIsEditModalOpen(false);
81-
setSuccessMessage(
82-
`Changes to "${formData.name}" are ready to apply. Backend integration coming soon.`,
83-
);
84-
setTimeout(() => setSuccessMessage(null), 5000);
140+
const payload = formDataToPayload(formData, projectName || "");
141+
applyDataSource.mutate(payload as any, {
142+
onSuccess: () => {
143+
setIsEditModalOpen(false);
144+
setErrorMessage(null);
145+
setSuccessMessage(
146+
`Data source "${formData.name}" updated successfully.`,
147+
);
148+
setTimeout(() => setSuccessMessage(null), 5000);
149+
},
150+
onError: (err: unknown) => {
151+
const message =
152+
err instanceof Error ? err.message : "An unexpected error occurred.";
153+
setErrorMessage(message);
154+
setTimeout(() => setErrorMessage(null), 8000);
155+
},
156+
});
85157
};
86158

159+
const spec = data?.spec || data;
160+
const sourceType = spec?.type;
161+
87162
return (
88163
<React.Fragment>
89164
{isLoading && (
@@ -106,6 +181,17 @@ const DataSourceOverviewTab = () => {
106181
<EuiSpacer size="m" />
107182
</>
108183
)}
184+
{errorMessage && (
185+
<>
186+
<EuiCallOut
187+
title={errorMessage}
188+
color="danger"
189+
iconType="alert"
190+
size="s"
191+
/>
192+
<EuiSpacer size="m" />
193+
</>
194+
)}
109195
{isV2 && (
110196
<>
111197
<EuiFlexGroup justifyContent="flexEnd">
@@ -130,16 +216,16 @@ const DataSourceOverviewTab = () => {
130216
<h2>Properties</h2>
131217
</EuiTitle>
132218
<EuiHorizontalRule margin="xs" />
133-
{data.fileOptions || data.bigqueryOptions ? (
134-
<BatchSourcePropertiesView batchSource={data} />
135-
) : data.type ? (
219+
{spec?.fileOptions || spec?.bigqueryOptions ? (
220+
<BatchSourcePropertiesView batchSource={spec} />
221+
) : sourceType ? (
136222
<React.Fragment>
137223
<EuiDescriptionList>
138224
<EuiDescriptionListTitle>
139225
Source Type
140226
</EuiDescriptionListTitle>
141227
<EuiDescriptionListDescription>
142-
{feast.core.DataSource.SourceType[data.type]}
228+
{sourceType}
143229
</EuiDescriptionListDescription>
144230
</EuiDescriptionList>
145231
</React.Fragment>
@@ -152,20 +238,18 @@ const DataSourceOverviewTab = () => {
152238
<EuiSpacer size="m" />
153239
<EuiFlexGroup>
154240
<EuiFlexItem>
155-
{data.requestDataOptions ? (
241+
{spec?.requestDataOptions ? (
156242
<EuiPanel hasBorder={true}>
157243
<EuiTitle size="xs">
158244
<h2>Request Source Schema</h2>
159245
</EuiTitle>
160246
<EuiHorizontalRule margin="xs"></EuiHorizontalRule>
161247
<RequestDataSourceSchemaTable
162248
fields={
163-
data?.requestDataOptions?.schema!.map((obj) => {
164-
return {
165-
fieldName: obj.name!,
166-
valueType: obj.valueType!,
167-
};
168-
})!
249+
spec.requestDataOptions.schema?.map((obj: any) => ({
250+
fieldName: obj.name,
251+
valueType: obj.valueType,
252+
})) || []
169253
}
170254
/>
171255
</EuiPanel>
@@ -183,32 +267,12 @@ const DataSourceOverviewTab = () => {
183267
<EuiHorizontalRule margin="xs"></EuiHorizontalRule>
184268
{consumingFeatureViews && consumingFeatureViews.length > 0 ? (
185269
<FeatureViewEdgesList
186-
fvNames={consumingFeatureViews.map((f) => {
187-
return f.target.name;
188-
})}
189-
/>
190-
) : (
191-
<EuiText>No consuming feature views</EuiText>
192-
)}
193-
</EuiPanel>
194-
<EuiSpacer size="m" />
195-
<EuiPanel hasBorder={true}>
196-
<EuiTitle size="xs">
197-
<h2>Permissions</h2>
198-
</EuiTitle>
199-
<EuiHorizontalRule margin="xs"></EuiHorizontalRule>
200-
{registryQuery.data?.permissions ? (
201-
<PermissionsDisplay
202-
permissions={getEntityPermissions(
203-
registryQuery.data.permissions,
204-
FEAST_FCO_TYPES.dataSource,
205-
dsName,
270+
fvNames={consumingFeatureViews.map(
271+
(f: any) => f.target.name,
206272
)}
207273
/>
208274
) : (
209-
<EuiText>
210-
No permissions defined for this data source.
211-
</EuiText>
275+
<EuiText>No consuming feature views</EuiText>
212276
)}
213277
</EuiPanel>
214278
</EuiFlexItem>

ui/src/pages/data-sources/DataSourcesListingTable.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ const DatasourcesListingTable = ({
3232
name: "Type",
3333
field: "type",
3434
sortable: true,
35-
render: (valueType: feast.core.DataSource.SourceType) => {
35+
render: (valueType: feast.core.DataSource.SourceType | string) => {
36+
if (typeof valueType === "string") return valueType;
3637
return feast.core.DataSource.SourceType[valueType];
3738
},
3839
},

0 commit comments

Comments
 (0)