Skip to content

Commit

Permalink
feat: Add pages for individual Features to the Feast UI (#2850)
Browse files Browse the repository at this point in the history
* Add feature page functionality

Signed-off-by: Daniel Kim <danielk@twitter.com>

* Add links in feature view and feature pages

Signed-off-by: Daniel Kim <danielk@twitter.com>

* Modify Feast provider test to include new Feature pages

Signed-off-by: Daniel Kim <danielk@twitter.com>

* Add initial version of test

Signed-off-by: Daniel Kim <danielk@twitter.com>

* Make some changes to test and remove feature tab functionality from ondemand FVs

Signed-off-by: Daniel Kim <danielk@twitter.com>

* Change feature link EuiLinks to EuiCustomLinks

Signed-off-by: Daniel Kim <danielk@twitter.com>

* Change other links to EuiCustomLinks

Signed-off-by: Daniel Kim <danielk@twitter.com>
  • Loading branch information
kindalime authored Jul 1, 2022
1 parent 8abc2ef commit 9b97fca
Show file tree
Hide file tree
Showing 19 changed files with 556 additions and 13 deletions.
43 changes: 43 additions & 0 deletions ui/src/FeastUISansProviders.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,46 @@ test("routes are reachable", async () => {
});
}
});


const featureViewName = registry.featureViews[0].spec.name;
const featureName = registry.featureViews[0].spec.features[0].name;

test("features are reachable", async () => {
render(<FeastUISansProviders />);

// Wait for content to load
await screen.findByText(/Explore this Project/i);
const routeRegExp = new RegExp("Feature Views", "i");

userEvent.click(
screen.getByRole("button", { name: routeRegExp }),
leftClick
);

screen.getByRole("heading", {
name: "Feature Views",
});

await screen.findAllByText(/Feature Views/i);
const fvRegExp = new RegExp(featureViewName, "i");

userEvent.click(
screen.getByRole("link", { name: fvRegExp }),
leftClick
)

await screen.findByText(featureName);
const fRegExp = new RegExp(featureName, "i");

userEvent.click(
screen.getByRole("link", { name: fRegExp }),
leftClick
)
// Should land on a page with the heading
// await screen.findByText("Feature: " + featureName);
screen.getByRole("heading", {
name: "Feature: " + featureName,
level: 1,
});
});
9 changes: 6 additions & 3 deletions ui/src/FeastUISansProviders.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import DatasourceIndex from "./pages/data-sources/Index";
import DatasetIndex from "./pages/saved-data-sets/Index";
import EntityIndex from "./pages/entities/Index";
import EntityInstance from "./pages/entities/EntityInstance";
import FeatureInstance from "./pages/features/FeatureInstance";
import FeatureServiceIndex from "./pages/feature-services/Index";
import FeatureViewIndex from "./pages/feature-views/Index";
import FeatureViewInstance from "./pages/feature-views/FeatureViewInstance";
Expand Down Expand Up @@ -86,10 +87,12 @@ const FeastUISansProviders = ({
path="feature-view/"
element={<FeatureViewIndex />}
/>
<Route path="feature-view/:featureViewName/*" element={<FeatureViewInstance />}>
</Route>
<Route
path="feature-view/:featureViewName/*"
element={<FeatureViewInstance />}
/>
path="feature-view/:FeatureViewName/feature/:FeatureName/*"
element={<FeatureInstance />}
/>
<Route
path="feature-service/"
element={<FeatureServiceIndex />}
Expand Down
21 changes: 19 additions & 2 deletions ui/src/components/FeaturesListDisplay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,42 @@ import { FeastFeatureColumnType } from "../parsers/feastFeatureViews";
import useLoadFeatureViewSummaryStatistics from "../queries/useLoadFeatureViewSummaryStatistics";
import SparklineHistogram from "./SparklineHistogram";
import FeatureFlagsContext from "../contexts/FeatureFlagsContext";
import EuiCustomLink from "./EuiCustomLink";

interface FeaturesListProps {
projectName: string;
featureViewName: string;
features: FeastFeatureColumnType[];
link: boolean;
}

const FeaturesList = ({ featureViewName, features }: FeaturesListProps) => {
const FeaturesList = ({ projectName, featureViewName, features, link }: FeaturesListProps) => {
const { enabledFeatureStatistics } = useContext(FeatureFlagsContext);
const { isLoading, isError, isSuccess, data } =
useLoadFeatureViewSummaryStatistics(featureViewName);

let columns: { name: string; render?: any; field: any }[] = [
{ name: "Name", field: "name" },
{
name: "Name",
field: "name",
render: (item: string) => (
<EuiCustomLink
href={`/p/${projectName}/feature-view/${featureViewName}/feature/${item}`}
to={`/p/${projectName}/feature-view/${featureViewName}/feature/${item}`}>
{item}
</EuiCustomLink>
)
},
{
name: "Value Type",
field: "valueType",
},
];

if (!link) {
columns[0].render = undefined;
}

if (enabledFeatureStatistics) {
columns.push(
...[
Expand Down
2 changes: 1 addition & 1 deletion ui/src/components/TagSearch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ const TagSearch = ({
// HTMLInputElement is hooked into useInputHack
inputNode.current = node;
},
onfocus: () => {
onFocus: () => {
setHasFocus(true);
},
fullWidth: true,
Expand Down
31 changes: 27 additions & 4 deletions ui/src/custom-tabs/TabsRegistryContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
import RegularFeatureViewCustomTabLoadingWrapper from "../utils/custom-tabs/RegularFeatureViewCustomTabLoadingWrapper";
import OnDemandFeatureViewCustomTabLoadingWrapper from "../utils/custom-tabs/OnDemandFeatureViewCustomTabLoadingWrapper";
import FeatureServiceCustomTabLoadingWrapper from "../utils/custom-tabs/FeatureServiceCustomTabLoadingWrapper";
import FeatureCustomTabLoadingWrapper from "../utils/custom-tabs/FeatureCustomTabLoadingWrapper";
import DataSourceCustomTabLoadingWrapper from "../utils/custom-tabs/DataSourceCustomTabLoadingWrapper";
import EntityCustomTabLoadingWrapper from "../utils/custom-tabs/EntityCustomTabLoadingWrapper";
import DatasetCustomTabLoadingWrapper from "../utils/custom-tabs/DatasetCustomTabLoadingWrapper";
Expand All @@ -19,6 +20,7 @@ import {
RegularFeatureViewCustomTabRegistrationInterface,
OnDemandFeatureViewCustomTabRegistrationInterface,
FeatureServiceCustomTabRegistrationInterface,
FeatureCustomTabRegistrationInterface,
DataSourceCustomTabRegistrationInterface,
EntityCustomTabRegistrationInterface,
DatasetCustomTabRegistrationInterface,
Expand All @@ -29,6 +31,7 @@ interface FeastTabsRegistryInterface {
RegularFeatureViewCustomTabs?: RegularFeatureViewCustomTabRegistrationInterface[];
OnDemandFeatureViewCustomTabs?: OnDemandFeatureViewCustomTabRegistrationInterface[];
FeatureServiceCustomTabs?: FeatureServiceCustomTabRegistrationInterface[];
FeatureCustomTabs?: FeatureCustomTabRegistrationInterface[];
DataSourceCustomTabs?: DataSourceCustomTabRegistrationInterface[];
EntityCustomTabs?: EntityCustomTabRegistrationInterface[];
DatasetCustomTabs?: DatasetCustomTabRegistrationInterface[];
Expand Down Expand Up @@ -154,6 +157,15 @@ const useFeatureServiceCustomTabs = (navigate: NavigateFunction) => {
);
};

const useFeatureCustomTabs = (navigate: NavigateFunction) => {
const { FeatureCustomTabs } = React.useContext(TabsRegistryContext);

return useGenericCustomTabsNavigation<FeatureCustomTabRegistrationInterface>(
FeatureCustomTabs || [],
navigate
);
};

const useDataSourceCustomTabs = (navigate: NavigateFunction) => {
const { DataSourceCustomTabs } = React.useContext(TabsRegistryContext);

Expand Down Expand Up @@ -211,6 +223,15 @@ const useFeatureServiceCustomTabRoutes = () => {
);
};

const useEntityCustomTabRoutes = () => {
const { EntityCustomTabs } = React.useContext(TabsRegistryContext);

return genericCustomTabRoutes(
EntityCustomTabs || [],
EntityCustomTabLoadingWrapper
);
};

const useDataSourceCustomTabRoutes = () => {
const { DataSourceCustomTabs } = React.useContext(TabsRegistryContext);

Expand All @@ -220,12 +241,12 @@ const useDataSourceCustomTabRoutes = () => {
);
};

const useEntityCustomTabRoutes = () => {
const { EntityCustomTabs } = React.useContext(TabsRegistryContext);
const useFeatureCustomTabRoutes = () => {
const { FeatureCustomTabs } = React.useContext(TabsRegistryContext);

return genericCustomTabRoutes(
EntityCustomTabs || [],
EntityCustomTabLoadingWrapper
FeatureCustomTabs || [],
FeatureCustomTabLoadingWrapper
);
};

Expand All @@ -244,13 +265,15 @@ export {
useRegularFeatureViewCustomTabs,
useOnDemandFeatureViewCustomTabs,
useFeatureServiceCustomTabs,
useFeatureCustomTabs,
useDataSourceCustomTabs,
useEntityCustomTabs,
useDatasetCustomTabs,
// Routes
useRegularFeatureViewCustomTabRoutes,
useOnDemandFeatureViewCustomTabRoutes,
useFeatureServiceCustomTabRoutes,
useFeatureCustomTabRoutes,
useDataSourceCustomTabRoutes,
useEntityCustomTabRoutes,
useDatasetCustomTabRoutes,
Expand Down
83 changes: 83 additions & 0 deletions ui/src/custom-tabs/feature-demo-tab/DemoCustomTab.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import React from "react";

import {
// Feature View Custom Tabs will get these props
FeatureCustomTabProps,
} from "../types";

import {
EuiLoadingContent,
EuiEmptyPrompt,
EuiFlexGroup,
EuiFlexItem,
EuiCode,
EuiSpacer,
} from "@elastic/eui";

// Separating out the query is not required,
// but encouraged for code readability
import useDemoQuery from "./useDemoQuery";

const DemoCustomTab = ({ id, feastObjectQuery }: FeatureCustomTabProps) => {
// Use React Query to fetch data
// that is custom to this tab.
// See: https://react-query.tanstack.com/guides/queries

const { isLoading, isError, isSuccess, data } = useDemoQuery({
featureView: id,
});

if (isLoading) {
// Handle Loading State
// https://elastic.github.io/eui/#/display/loading
return <EuiLoadingContent lines={3} />;
}

if (isError) {
// Handle Data Fetching Error
// https://elastic.github.io/eui/#/display/empty-prompt
return (
<EuiEmptyPrompt
iconType="alert"
color="danger"
title={<h2>Unable to load your demo page</h2>}
body={
<p>
There was an error loading the Dashboard application. Contact your
administrator for help.
</p>
}
/>
);
}

// Feast UI uses the Elastic UI component system.
// <EuiFlexGroup> and <EuiFlexItem> are particularly
// useful for layouts.
return (
<React.Fragment>
<EuiFlexGroup>
<EuiFlexItem grow={1}>
<p>Hello World. The following is fetched data.</p>
<EuiSpacer />
{isSuccess && data && (
<EuiCode>
<pre>{JSON.stringify(data, null, 2)}</pre>
</EuiCode>
)}
</EuiFlexItem>
<EuiFlexItem grow={2}>
<p>... and this is data from Feast UI&rsquo;s own query.</p>
<EuiSpacer />
{feastObjectQuery.isSuccess && feastObjectQuery.featureData && (
<EuiCode>
<pre>{JSON.stringify(feastObjectQuery.featureData, null, 2)}</pre>
</EuiCode>
)}
</EuiFlexItem>
</EuiFlexGroup>
</React.Fragment>
);
};

export default DemoCustomTab;
44 changes: 44 additions & 0 deletions ui/src/custom-tabs/feature-demo-tab/useDemoQuery.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { useQuery } from "react-query";
import { z } from "zod";

// Use Zod to check the shape of the
// json object being loaded
const demoSchema = z.object({
hello: z.string(),
name: z.string().optional(),
});

// Make the type of the object available
type DemoDataType = z.infer<typeof demoSchema>;

interface DemoQueryInterface {
featureView: string | undefined;
}

const useDemoQuery = ({ featureView }: DemoQueryInterface) => {
// React Query manages caching for you based on query keys
// See: https://react-query.tanstack.com/guides/query-keys
const queryKey = `demo-tab-namespace:${featureView}`;

// Pass the type to useQuery
// so that components consuming the
// result gets nice type hints
// on the other side.
return useQuery<DemoDataType>(
queryKey,
() => {
// Customizing the URL based on your needs
const url = `/demo-custom-tabs/demo.json`;

return fetch(url)
.then((res) => res.json())
.then((data) => demoSchema.parse(data)); // Use zod to parse results
},
{
enabled: !!featureView, // Only start the query when the variable is not undefined
}
);
};

export default useDemoQuery;
export type { DemoDataType };
20 changes: 19 additions & 1 deletion ui/src/custom-tabs/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
useLoadOnDemandFeatureView,
useLoadRegularFeatureView,
} from "../pages/feature-views/useLoadFeatureView";
import useLoadFeature from "../pages/features/useLoadFeature";
import useLoadFeatureService from "../pages/feature-services/useLoadFeatureService";
import useLoadDataSource from "../pages/data-sources/useLoadDataSource";
import useLoadEntity from "../pages/entities/useLoadEntity";
Expand Down Expand Up @@ -47,7 +48,7 @@ interface OnDemandFeatureViewCustomTabRegistrationInterface
}: OnDemandFeatureViewCustomTabProps) => JSX.Element;
}

// Type for Feature Service Custom Tabs
// Type for Entity Custom Tabs
interface EntityCustomTabProps {
id: string | undefined;
feastObjectQuery: ReturnType<typeof useLoadEntity>;
Expand All @@ -61,6 +62,21 @@ interface EntityCustomTabRegistrationInterface
}: EntityCustomTabProps) => JSX.Element;
}

// Type for Feature Custom Tabs
interface FeatureCustomTabProps {
id: string | undefined;
feastObjectQuery: ReturnType<typeof useLoadFeature>;
}
interface FeatureCustomTabRegistrationInterface
extends CustomTabRegistrationInterface {
Component: ({
id,
feastObjectQuery,
...args
}: FeatureCustomTabProps) => JSX.Element;
}


// Type for Feature Service Custom Tabs
interface FeatureServiceCustomTabProps {
id: string | undefined;
Expand Down Expand Up @@ -117,6 +133,8 @@ export type {
DataSourceCustomTabProps,
EntityCustomTabRegistrationInterface,
EntityCustomTabProps,
FeatureCustomTabRegistrationInterface,
FeatureCustomTabProps,
DatasetCustomTabRegistrationInterface,
DatasetCustomTabProps,
};
Loading

0 comments on commit 9b97fca

Please sign in to comment.