Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add pages for individual Features to the Feast UI #2850

Merged
merged 7 commits into from
Jul 1, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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