From b60c5243fdc79cc2a527cce3827e5e17c940c8e5 Mon Sep 17 00:00:00 2001 From: Daniel Kim Date: Fri, 17 Jun 2022 15:13:35 -0700 Subject: [PATCH 1/7] Add feature page functionality Signed-off-by: Daniel Kim --- ui/src/FeastUISansProviders.tsx | 9 +- ui/src/custom-tabs/TabsRegistryContext.tsx | 31 ++++++- .../feature-demo-tab/DemoCustomTab.tsx | 83 +++++++++++++++++++ .../feature-demo-tab/useDemoQuery.tsx | 44 ++++++++++ ui/src/custom-tabs/types.ts | 20 ++++- ui/src/graphics/FeatureIcon.tsx | 52 ++++++++++++ ui/src/index.tsx | 8 ++ ui/src/pages/features/FeatureInstance.tsx | 62 ++++++++++++++ ui/src/pages/features/FeatureOverviewTab.tsx | 72 ++++++++++++++++ ui/src/pages/features/FeatureRawData.tsx | 25 ++++++ ui/src/pages/features/useLoadFeature.ts | 29 +++++++ ui/src/parsers/feastFeatures.ts | 11 +++ ui/src/parsers/jsonType.ts | 11 +++ .../FeatureCustomTabLoadingWrapper.tsx | 37 +++++++++ 14 files changed, 486 insertions(+), 8 deletions(-) create mode 100644 ui/src/custom-tabs/feature-demo-tab/DemoCustomTab.tsx create mode 100644 ui/src/custom-tabs/feature-demo-tab/useDemoQuery.tsx create mode 100644 ui/src/graphics/FeatureIcon.tsx create mode 100644 ui/src/pages/features/FeatureInstance.tsx create mode 100644 ui/src/pages/features/FeatureOverviewTab.tsx create mode 100644 ui/src/pages/features/FeatureRawData.tsx create mode 100644 ui/src/pages/features/useLoadFeature.ts create mode 100644 ui/src/parsers/feastFeatures.ts create mode 100644 ui/src/parsers/jsonType.ts create mode 100644 ui/src/utils/custom-tabs/FeatureCustomTabLoadingWrapper.tsx diff --git a/ui/src/FeastUISansProviders.tsx b/ui/src/FeastUISansProviders.tsx index 628068f0f0..8a0e0b94db 100644 --- a/ui/src/FeastUISansProviders.tsx +++ b/ui/src/FeastUISansProviders.tsx @@ -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"; @@ -86,10 +87,12 @@ const FeastUISansProviders = ({ path="feature-view/" element={} /> + }> + } - /> + path="feature-view/:FeatureViewName/feature/:FeatureName/*" + element={} + /> } diff --git a/ui/src/custom-tabs/TabsRegistryContext.tsx b/ui/src/custom-tabs/TabsRegistryContext.tsx index a5321e9c40..9f493e6d11 100644 --- a/ui/src/custom-tabs/TabsRegistryContext.tsx +++ b/ui/src/custom-tabs/TabsRegistryContext.tsx @@ -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"; @@ -19,6 +20,7 @@ import { RegularFeatureViewCustomTabRegistrationInterface, OnDemandFeatureViewCustomTabRegistrationInterface, FeatureServiceCustomTabRegistrationInterface, + FeatureCustomTabRegistrationInterface, DataSourceCustomTabRegistrationInterface, EntityCustomTabRegistrationInterface, DatasetCustomTabRegistrationInterface, @@ -29,6 +31,7 @@ interface FeastTabsRegistryInterface { RegularFeatureViewCustomTabs?: RegularFeatureViewCustomTabRegistrationInterface[]; OnDemandFeatureViewCustomTabs?: OnDemandFeatureViewCustomTabRegistrationInterface[]; FeatureServiceCustomTabs?: FeatureServiceCustomTabRegistrationInterface[]; + FeatureCustomTabs?: FeatureCustomTabRegistrationInterface[]; DataSourceCustomTabs?: DataSourceCustomTabRegistrationInterface[]; EntityCustomTabs?: EntityCustomTabRegistrationInterface[]; DatasetCustomTabs?: DatasetCustomTabRegistrationInterface[]; @@ -154,6 +157,15 @@ const useFeatureServiceCustomTabs = (navigate: NavigateFunction) => { ); }; +const useFeatureCustomTabs = (navigate: NavigateFunction) => { + const { FeatureCustomTabs } = React.useContext(TabsRegistryContext); + + return useGenericCustomTabsNavigation( + FeatureCustomTabs || [], + navigate + ); +}; + const useDataSourceCustomTabs = (navigate: NavigateFunction) => { const { DataSourceCustomTabs } = React.useContext(TabsRegistryContext); @@ -211,6 +223,15 @@ const useFeatureServiceCustomTabRoutes = () => { ); }; +const useEntityCustomTabRoutes = () => { + const { EntityCustomTabs } = React.useContext(TabsRegistryContext); + + return genericCustomTabRoutes( + EntityCustomTabs || [], + EntityCustomTabLoadingWrapper + ); +}; + const useDataSourceCustomTabRoutes = () => { const { DataSourceCustomTabs } = React.useContext(TabsRegistryContext); @@ -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 ); }; @@ -244,6 +265,7 @@ export { useRegularFeatureViewCustomTabs, useOnDemandFeatureViewCustomTabs, useFeatureServiceCustomTabs, + useFeatureCustomTabs, useDataSourceCustomTabs, useEntityCustomTabs, useDatasetCustomTabs, @@ -251,6 +273,7 @@ export { useRegularFeatureViewCustomTabRoutes, useOnDemandFeatureViewCustomTabRoutes, useFeatureServiceCustomTabRoutes, + useFeatureCustomTabRoutes, useDataSourceCustomTabRoutes, useEntityCustomTabRoutes, useDatasetCustomTabRoutes, diff --git a/ui/src/custom-tabs/feature-demo-tab/DemoCustomTab.tsx b/ui/src/custom-tabs/feature-demo-tab/DemoCustomTab.tsx new file mode 100644 index 0000000000..fda920daf3 --- /dev/null +++ b/ui/src/custom-tabs/feature-demo-tab/DemoCustomTab.tsx @@ -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 ; + } + + if (isError) { + // Handle Data Fetching Error + // https://elastic.github.io/eui/#/display/empty-prompt + return ( + Unable to load your demo page} + body={ +

+ There was an error loading the Dashboard application. Contact your + administrator for help. +

+ } + /> + ); + } + + // Feast UI uses the Elastic UI component system. + // and are particularly + // useful for layouts. + return ( + + + +

Hello World. The following is fetched data.

+ + {isSuccess && data && ( + +
{JSON.stringify(data, null, 2)}
+
+ )} +
+ +

... and this is data from Feast UI’s own query.

+ + {feastObjectQuery.isSuccess && feastObjectQuery.featureData && ( + +
{JSON.stringify(feastObjectQuery.featureData, null, 2)}
+
+ )} +
+
+
+ ); +}; + +export default DemoCustomTab; diff --git a/ui/src/custom-tabs/feature-demo-tab/useDemoQuery.tsx b/ui/src/custom-tabs/feature-demo-tab/useDemoQuery.tsx new file mode 100644 index 0000000000..b93602dbe3 --- /dev/null +++ b/ui/src/custom-tabs/feature-demo-tab/useDemoQuery.tsx @@ -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; + +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( + 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 }; diff --git a/ui/src/custom-tabs/types.ts b/ui/src/custom-tabs/types.ts index f80c56d0e2..1e555d6185 100644 --- a/ui/src/custom-tabs/types.ts +++ b/ui/src/custom-tabs/types.ts @@ -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"; @@ -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; @@ -61,6 +62,21 @@ interface EntityCustomTabRegistrationInterface }: EntityCustomTabProps) => JSX.Element; } +// Type for Feature Custom Tabs +interface FeatureCustomTabProps { + id: string | undefined; + feastObjectQuery: ReturnType; +} +interface FeatureCustomTabRegistrationInterface + extends CustomTabRegistrationInterface { + Component: ({ + id, + feastObjectQuery, + ...args + }: FeatureCustomTabProps) => JSX.Element; +} + + // Type for Feature Service Custom Tabs interface FeatureServiceCustomTabProps { id: string | undefined; @@ -117,6 +133,8 @@ export type { DataSourceCustomTabProps, EntityCustomTabRegistrationInterface, EntityCustomTabProps, + FeatureCustomTabRegistrationInterface, + FeatureCustomTabProps, DatasetCustomTabRegistrationInterface, DatasetCustomTabProps, }; diff --git a/ui/src/graphics/FeatureIcon.tsx b/ui/src/graphics/FeatureIcon.tsx new file mode 100644 index 0000000000..e2e06749bc --- /dev/null +++ b/ui/src/graphics/FeatureIcon.tsx @@ -0,0 +1,52 @@ +import React from "react"; + +const FeatureIcon = ({ + size, + className, +}: { + size: number; + className?: string; +}) => { + return ( + + + + + + + + ); +}; + +const FeatureIcon16 = () => { + return ; +}; + +const FeatureIcon32 = () => { + return ( + + ); +}; + +export { FeatureIcon, FeatureIcon16, FeatureIcon32 }; diff --git a/ui/src/index.tsx b/ui/src/index.tsx index 3a6269a8b7..8cd73cf094 100644 --- a/ui/src/index.tsx +++ b/ui/src/index.tsx @@ -21,6 +21,7 @@ import FSDemoCustomTab from "./custom-tabs/feature-service-demo-tab/DemoCustomTa import DSDemoCustomTab from "./custom-tabs/data-source-demo-tab/DemoCustomTab"; import EntDemoCustomTab from "./custom-tabs/entity-demo-tab/DemoCustomTab"; import DatasetDemoCustomTab from "./custom-tabs/dataset-demo-tab/DemoCustomTab"; +import FDemoCustomTab from "./custom-tabs/feature-demo-tab/DemoCustomTab"; const queryClient = new QueryClient(); @@ -67,6 +68,13 @@ const tabsRegistry = { Component: DatasetDemoCustomTab, }, ], + FeatureCustomTabs: [ + { + label: "Custom Tab Demo", + path: "demo-tab", + Component: FDemoCustomTab, + }, + ], }; ReactDOM.render( diff --git a/ui/src/pages/features/FeatureInstance.tsx b/ui/src/pages/features/FeatureInstance.tsx new file mode 100644 index 0000000000..6eb7d0f2d6 --- /dev/null +++ b/ui/src/pages/features/FeatureInstance.tsx @@ -0,0 +1,62 @@ +import React from "react"; +import { Route, Routes, useNavigate, useParams } from "react-router-dom"; +import { + EuiPageHeader, + EuiPageContent, + EuiPageContentBody, +} from "@elastic/eui"; + +import { FeatureIcon32 } from "../../graphics/FeatureIcon"; +import { useMatchExact } from "../../hooks/useMatchSubpath"; +import FeatureOverviewTab from "./FeatureOverviewTab"; +import { useDocumentTitle } from "../../hooks/useDocumentTitle"; +import { + useFeatureCustomTabs, + useFeatureCustomTabRoutes, +} from "../../custom-tabs/TabsRegistryContext"; + +const FeatureInstance = () => { + const navigate = useNavigate(); + let { FeatureViewName, FeatureName } = useParams(); + + const { customNavigationTabs } = useFeatureCustomTabs(navigate); + const CustomTabRoutes = useFeatureCustomTabRoutes(); + + useDocumentTitle(`${FeatureName} | ${FeatureViewName} | Feast`); + + return ( + + { + navigate(""); + }, + }, + ...customNavigationTabs, + ]} + /> + + + + } /> + {CustomTabRoutes} + + + + + ); +}; + +export default FeatureInstance; diff --git a/ui/src/pages/features/FeatureOverviewTab.tsx b/ui/src/pages/features/FeatureOverviewTab.tsx new file mode 100644 index 0000000000..ad50c4b08e --- /dev/null +++ b/ui/src/pages/features/FeatureOverviewTab.tsx @@ -0,0 +1,72 @@ +import { + EuiFlexGroup, + EuiHorizontalRule, + EuiLoadingSpinner, + EuiTitle, +} from "@elastic/eui"; +import { + EuiPanel, + EuiText, + EuiFlexItem, + EuiSpacer, + EuiDescriptionList, + EuiDescriptionListTitle, + EuiDescriptionListDescription, +} from "@elastic/eui"; +import React from "react"; +import { useParams } from "react-router-dom"; +import TagsDisplay from "../../components/TagsDisplay"; +import useLoadFeature from "./useLoadFeature"; + +const FeatureOverviewTab = () => { + let { FeatureViewName, FeatureName } = useParams(); + + const eName = FeatureViewName === undefined ? "" : FeatureViewName; + const fName = FeatureName === undefined ? "" : FeatureName; + const { isLoading, isSuccess, isError, data, featureData } = useLoadFeature(eName, fName); + const isEmpty = data === undefined || featureData === undefined; + // const isEmpty = featureData === undefined; + + return ( + + {isLoading && ( + + Loading + + )} + {isEmpty &&

No Feature with name {FeatureName} in FeatureView {FeatureViewName}

} + {isError &&

Error loading Feature {FeatureName} in FeatureView {FeatureViewName}

} + {isSuccess && data && ( + + + + + +

Properties

+
+ + + Name + + {featureData?.name} + + + Value Type + + {featureData?.valueType} + + + FeatureView + + {FeatureViewName} + + +
+
+
+
+ )} +
+ ); +}; +export default FeatureOverviewTab; diff --git a/ui/src/pages/features/FeatureRawData.tsx b/ui/src/pages/features/FeatureRawData.tsx new file mode 100644 index 0000000000..efbe29d431 --- /dev/null +++ b/ui/src/pages/features/FeatureRawData.tsx @@ -0,0 +1,25 @@ +import React from "react"; +import { EuiPanel } from "@elastic/eui"; +import { useParams } from "react-router-dom"; +import useLoadFeature from "./useLoadFeature"; + +const FeatureRawData = () => { + let { FeatureViewName, FeatureName } = useParams(); + + const eName = FeatureViewName === undefined ? "" : FeatureViewName; + const fName = FeatureName === undefined ? "" : FeatureName; + + const { isSuccess, data } = useLoadFeature(eName, fName); + + return isSuccess && data ? ( + +
{JSON.stringify(data, null, 2)}
+
+ ) : ( + + No data so sad ;-; + + ); +}; + +export default FeatureRawData; diff --git a/ui/src/pages/features/useLoadFeature.ts b/ui/src/pages/features/useLoadFeature.ts new file mode 100644 index 0000000000..5ddaf28204 --- /dev/null +++ b/ui/src/pages/features/useLoadFeature.ts @@ -0,0 +1,29 @@ +import { useContext } from "react"; +import RegistryPathContext from "../../contexts/RegistryPathContext"; +import useLoadRegistry from "../../queries/useLoadRegistry"; + +const useLoadFeature = (featureViewName: string, featureName: string) => { + const registryUrl = useContext(RegistryPathContext); + const registryQuery = useLoadRegistry(registryUrl); + + const data = + registryQuery.data === undefined + ? undefined + : registryQuery.data.objects.featureViews?.find((fv) => { + return fv.spec.name === featureViewName; + }); + + const featureData = + data === undefined + ? undefined + : data?.spec.features.find((f) => { + return f.name === featureName; + }); + + return { + ...registryQuery, + featureData, + }; +}; + +export default useLoadFeature; diff --git a/ui/src/parsers/feastFeatures.ts b/ui/src/parsers/feastFeatures.ts new file mode 100644 index 0000000000..129120c168 --- /dev/null +++ b/ui/src/parsers/feastFeatures.ts @@ -0,0 +1,11 @@ +import { z } from "zod"; +import { FEAST_FEATURE_VALUE_TYPES } from "./types"; +import { jsonSchema } from "./jsonType" + +const FeastFeatureSchema = z.object({ + name: z.string(), + valueType: z.nativeEnum(FEAST_FEATURE_VALUE_TYPES), + metadata: jsonSchema.optional(), +}); + +export { FeastFeatureSchema }; diff --git a/ui/src/parsers/jsonType.ts b/ui/src/parsers/jsonType.ts new file mode 100644 index 0000000000..be484b5477 --- /dev/null +++ b/ui/src/parsers/jsonType.ts @@ -0,0 +1,11 @@ +import { z } from "zod"; + +// Taken from the zod documentation code - accepts any JSON object. +const literalSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]); +type Literal = z.infer; +type Json = Literal | { [key: string]: Json } | Json[]; +const jsonSchema: z.ZodType = z.lazy(() => + z.union([literalSchema, z.array(jsonSchema), z.record(jsonSchema)]) +); + +export { jsonSchema }; diff --git a/ui/src/utils/custom-tabs/FeatureCustomTabLoadingWrapper.tsx b/ui/src/utils/custom-tabs/FeatureCustomTabLoadingWrapper.tsx new file mode 100644 index 0000000000..7880f82490 --- /dev/null +++ b/ui/src/utils/custom-tabs/FeatureCustomTabLoadingWrapper.tsx @@ -0,0 +1,37 @@ +import React from "react"; +import { useParams } from "react-router-dom"; + +import { FeatureCustomTabProps } from "../../custom-tabs/types"; +import useLoadFeature from "../../pages/features/useLoadFeature"; + +interface FeatureCustomTabLoadingWrapperProps { + Component: (props: FeatureCustomTabProps) => JSX.Element; +} + +const FeatureCustomTabLoadingWrapper = ({ + Component, +}: FeatureCustomTabLoadingWrapperProps) => { + console.log(useParams()); + const { FeatureViewName, FeatureName } = useParams(); + + if (!FeatureViewName) { + throw new Error( + `This route has no 'FeatureViewName' part. This route is likely not supposed to render this component.` + ); + } + + if (!FeatureName) { + throw new Error( + `This route has no 'FeatureName' part. This route is likely not supposed to render this component.` + ); + } + + const feastObjectQuery = useLoadFeature(FeatureViewName, FeatureName); + + // do I include FeatureViewName in this? + return ( + + ); +}; + +export default FeatureCustomTabLoadingWrapper; From 5a0c46682f11a4c4a8f6a48f4a9c32fbadd52b54 Mon Sep 17 00:00:00 2001 From: Daniel Kim Date: Fri, 17 Jun 2022 16:10:59 -0700 Subject: [PATCH 2/7] Add links in feature view and feature pages Signed-off-by: Daniel Kim --- ui/src/components/FeaturesListDisplay.tsx | 12 ++++++++++-- ui/src/pages/features/FeatureOverviewTab.tsx | 11 +++++------ 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/ui/src/components/FeaturesListDisplay.tsx b/ui/src/components/FeaturesListDisplay.tsx index abd9c1d2e4..21f4da477d 100644 --- a/ui/src/components/FeaturesListDisplay.tsx +++ b/ui/src/components/FeaturesListDisplay.tsx @@ -1,5 +1,5 @@ import React, { useContext } from "react"; -import { EuiBasicTable, EuiLoadingSpinner, EuiBadge } from "@elastic/eui"; +import { EuiBasicTable, EuiLoadingSpinner, EuiBadge, EuiLink } from "@elastic/eui"; import { FeastFeatureColumnType } from "../parsers/feastFeatureViews"; import useLoadFeatureViewSummaryStatistics from "../queries/useLoadFeatureViewSummaryStatistics"; import SparklineHistogram from "./SparklineHistogram"; @@ -16,7 +16,15 @@ const FeaturesList = ({ featureViewName, features }: FeaturesListProps) => { useLoadFeatureViewSummaryStatistics(featureViewName); let columns: { name: string; render?: any; field: any }[] = [ - { name: "Name", field: "name" }, + { + name: "Name", + field: "name", + render: (item: string) => ( + + {item} + + ) + }, { name: "Value Type", field: "valueType", diff --git a/ui/src/pages/features/FeatureOverviewTab.tsx b/ui/src/pages/features/FeatureOverviewTab.tsx index ad50c4b08e..9e4308003e 100644 --- a/ui/src/pages/features/FeatureOverviewTab.tsx +++ b/ui/src/pages/features/FeatureOverviewTab.tsx @@ -1,31 +1,28 @@ import { EuiFlexGroup, EuiHorizontalRule, + EuiLink, EuiLoadingSpinner, EuiTitle, } from "@elastic/eui"; import { EuiPanel, - EuiText, EuiFlexItem, - EuiSpacer, EuiDescriptionList, EuiDescriptionListTitle, EuiDescriptionListDescription, } from "@elastic/eui"; import React from "react"; import { useParams } from "react-router-dom"; -import TagsDisplay from "../../components/TagsDisplay"; import useLoadFeature from "./useLoadFeature"; const FeatureOverviewTab = () => { - let { FeatureViewName, FeatureName } = useParams(); + let { projectName, FeatureViewName, FeatureName } = useParams(); const eName = FeatureViewName === undefined ? "" : FeatureViewName; const fName = FeatureName === undefined ? "" : FeatureName; const { isLoading, isSuccess, isError, data, featureData } = useLoadFeature(eName, fName); const isEmpty = data === undefined || featureData === undefined; - // const isEmpty = featureData === undefined; return ( @@ -58,7 +55,9 @@ const FeatureOverviewTab = () => { FeatureView - {FeatureViewName} + + {FeatureViewName} + From eb337c31552b7468f7a1abfda83e81fa018e76e0 Mon Sep 17 00:00:00 2001 From: Daniel Kim Date: Wed, 29 Jun 2022 12:08:11 -0700 Subject: [PATCH 3/7] Modify Feast provider test to include new Feature pages Signed-off-by: Daniel Kim --- ui/src/FeastUISansProviders.test.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/src/FeastUISansProviders.test.tsx b/ui/src/FeastUISansProviders.test.tsx index 1289cea028..12aa52ea13 100644 --- a/ui/src/FeastUISansProviders.test.tsx +++ b/ui/src/FeastUISansProviders.test.tsx @@ -69,6 +69,7 @@ test("routes are reachable", async () => { const mainRoutesNames = [ "Data Sources", "Entities", + "Features", "Feature Views", "Feature Services", "Datasets", From 0f61df897d69edadc85af476c84ceac1d210c8b0 Mon Sep 17 00:00:00 2001 From: Daniel Kim Date: Thu, 30 Jun 2022 11:15:38 -0700 Subject: [PATCH 4/7] Add initial version of test Signed-off-by: Daniel Kim --- ui/src/FeastUISansProviders.test.tsx | 47 +++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/ui/src/FeastUISansProviders.test.tsx b/ui/src/FeastUISansProviders.test.tsx index 12aa52ea13..daf7bf8307 100644 --- a/ui/src/FeastUISansProviders.test.tsx +++ b/ui/src/FeastUISansProviders.test.tsx @@ -69,7 +69,6 @@ test("routes are reachable", async () => { const mainRoutesNames = [ "Data Sources", "Entities", - "Features", "Feature Views", "Feature Services", "Datasets", @@ -95,3 +94,49 @@ 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(); + + // 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.findByText(/Feature Views/i); + const fvRegExp = new RegExp(featureViewName, "i"); + + userEvent.click( + screen.getByRole("link", { name: "credit_history" }), + leftClick + ) + + // await screen.findByText("Features"); + const fRegExp = new RegExp(featureName, "i"); + + userEvent.click( + screen.getByRole("link", { name: fRegExp }), + leftClick + ) + + // Should land on a page with the heading + screen.getByRole("heading", { + name: "Features", + level: 1, + }); + + + expect(window.location.href).toContain("feature-view") +}); From df4cc7f030920f55776ed1b7f047c89aafa7a950 Mon Sep 17 00:00:00 2001 From: Daniel Kim Date: Thu, 30 Jun 2022 15:47:19 -0700 Subject: [PATCH 5/7] Make some changes to test and remove feature tab functionality from ondemand FVs Signed-off-by: Daniel Kim --- ui/src/FeastUISansProviders.test.tsx | 16 +++++++--------- ui/src/components/FeaturesListDisplay.tsx | 12 ++++++++++-- ui/src/components/TagSearch.tsx | 2 +- .../OnDemandFeatureViewOverviewTab.tsx | 6 +++++- .../RegularFeatureViewOverviewTab.tsx | 4 +++- 5 files changed, 26 insertions(+), 14 deletions(-) diff --git a/ui/src/FeastUISansProviders.test.tsx b/ui/src/FeastUISansProviders.test.tsx index daf7bf8307..3a110b5579 100644 --- a/ui/src/FeastUISansProviders.test.tsx +++ b/ui/src/FeastUISansProviders.test.tsx @@ -115,28 +115,26 @@ test("features are reachable", async () => { name: "Feature Views", }); - // await screen.findByText(/Feature Views/i); + await screen.findAllByText(/Feature Views/i); const fvRegExp = new RegExp(featureViewName, "i"); userEvent.click( - screen.getByRole("link", { name: "credit_history" }), + screen.getByRole("link", { name: fvRegExp }), leftClick ) - // await screen.findByText("Features"); + await screen.findByText(featureName); const fRegExp = new RegExp(featureName, "i"); + console.debug(featureName) userEvent.click( - screen.getByRole("link", { name: fRegExp }), + screen.getByRole("link", { name: featureName }), leftClick ) - // Should land on a page with the heading + // await screen.findByText("Feature: " + featureName); screen.getByRole("heading", { - name: "Features", + name: "Feature: " + featureName, level: 1, }); - - - expect(window.location.href).toContain("feature-view") }); diff --git a/ui/src/components/FeaturesListDisplay.tsx b/ui/src/components/FeaturesListDisplay.tsx index 21f4da477d..8397d92f37 100644 --- a/ui/src/components/FeaturesListDisplay.tsx +++ b/ui/src/components/FeaturesListDisplay.tsx @@ -6,11 +6,13 @@ import SparklineHistogram from "./SparklineHistogram"; import FeatureFlagsContext from "../contexts/FeatureFlagsContext"; 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); @@ -20,7 +22,7 @@ const FeaturesList = ({ featureViewName, features }: FeaturesListProps) => { name: "Name", field: "name", render: (item: string) => ( - + {item} ) @@ -31,6 +33,12 @@ const FeaturesList = ({ featureViewName, features }: FeaturesListProps) => { }, ]; + if (!link) { + columns[0].render = undefined; + } + + console.log(columns); + if (enabledFeatureStatistics) { columns.push( ...[ diff --git a/ui/src/components/TagSearch.tsx b/ui/src/components/TagSearch.tsx index e89d4a44cc..e3f7cdd98f 100644 --- a/ui/src/components/TagSearch.tsx +++ b/ui/src/components/TagSearch.tsx @@ -163,7 +163,7 @@ const TagSearch = ({ // HTMLInputElement is hooked into useInputHack inputNode.current = node; }, - onfocus: () => { + onFocus: () => { setHasFocus(true); }, fullWidth: true, diff --git a/ui/src/pages/feature-views/OnDemandFeatureViewOverviewTab.tsx b/ui/src/pages/feature-views/OnDemandFeatureViewOverviewTab.tsx index 1ea509d8df..0922f62102 100644 --- a/ui/src/pages/feature-views/OnDemandFeatureViewOverviewTab.tsx +++ b/ui/src/pages/feature-views/OnDemandFeatureViewOverviewTab.tsx @@ -15,6 +15,7 @@ import { RequestDataSourceType, FeatureViewProjectionType, } from "../../parsers/feastODFVS"; +import { useParams } from "react-router-dom"; import { EntityRelation } from "../../parsers/parseEntityRelationships"; import { FEAST_FCO_TYPES } from "../../parsers/types"; import useLoadRelationshipData from "../../queries/useLoadRelationshipsData"; @@ -39,6 +40,7 @@ const OnDemandFeatureViewOverviewTab = ({ data, }: OnDemandFeatureViewOverviewTabProps) => { const inputs = Object.entries(data.spec.sources); + const { projectName } = useParams(); const relationshipQuery = useLoadRelationshipData(); const fsNames = relationshipQuery.data @@ -71,10 +73,12 @@ const OnDemandFeatureViewOverviewTab = ({

Features ({data.spec.features.length})

- {data.spec.features ? ( + {projectName && data.spec.features ? ( ) : ( No Tags sepcified on this feature view. diff --git a/ui/src/pages/feature-views/RegularFeatureViewOverviewTab.tsx b/ui/src/pages/feature-views/RegularFeatureViewOverviewTab.tsx index d284d697e8..689bc6b902 100644 --- a/ui/src/pages/feature-views/RegularFeatureViewOverviewTab.tsx +++ b/ui/src/pages/feature-views/RegularFeatureViewOverviewTab.tsx @@ -69,10 +69,12 @@ const RegularFeatureViewOverviewTab = ({

Features ({data.spec.features.length})

- {data.spec.features ? ( + {projectName && data.spec.features ? ( ) : ( No features specified on this feature view. From 9f5b30f9052ae0539017bdef7d72bb2484eb1e46 Mon Sep 17 00:00:00 2001 From: Daniel Kim Date: Fri, 1 Jul 2022 10:33:00 -0700 Subject: [PATCH 6/7] Change feature link EuiLinks to EuiCustomLinks Signed-off-by: Daniel Kim --- ui/src/FeastUISansProviders.test.tsx | 3 +-- ui/src/components/FeaturesListDisplay.tsx | 9 +++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/ui/src/FeastUISansProviders.test.tsx b/ui/src/FeastUISansProviders.test.tsx index 3a110b5579..09985bc133 100644 --- a/ui/src/FeastUISansProviders.test.tsx +++ b/ui/src/FeastUISansProviders.test.tsx @@ -125,10 +125,9 @@ test("features are reachable", async () => { await screen.findByText(featureName); const fRegExp = new RegExp(featureName, "i"); - console.debug(featureName) userEvent.click( - screen.getByRole("link", { name: featureName }), + screen.getByRole("link", { name: fRegExp }), leftClick ) // Should land on a page with the heading diff --git a/ui/src/components/FeaturesListDisplay.tsx b/ui/src/components/FeaturesListDisplay.tsx index 8397d92f37..7f486132dc 100644 --- a/ui/src/components/FeaturesListDisplay.tsx +++ b/ui/src/components/FeaturesListDisplay.tsx @@ -4,6 +4,7 @@ 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; @@ -22,9 +23,11 @@ const FeaturesList = ({ projectName, featureViewName, features, link }: Features name: "Name", field: "name", render: (item: string) => ( - + {item} - + ) }, { @@ -36,8 +39,6 @@ const FeaturesList = ({ projectName, featureViewName, features, link }: Features if (!link) { columns[0].render = undefined; } - - console.log(columns); if (enabledFeatureStatistics) { columns.push( From 3ceb7b2d810fcd33eb5cfe761d0a638c2abb0dd6 Mon Sep 17 00:00:00 2001 From: Daniel Kim Date: Fri, 1 Jul 2022 10:34:31 -0700 Subject: [PATCH 7/7] Change other links to EuiCustomLinks Signed-off-by: Daniel Kim --- ui/src/components/FeaturesListDisplay.tsx | 2 +- ui/src/pages/features/FeatureOverviewTab.tsx | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/ui/src/components/FeaturesListDisplay.tsx b/ui/src/components/FeaturesListDisplay.tsx index 7f486132dc..dcb6ba81eb 100644 --- a/ui/src/components/FeaturesListDisplay.tsx +++ b/ui/src/components/FeaturesListDisplay.tsx @@ -1,5 +1,5 @@ import React, { useContext } from "react"; -import { EuiBasicTable, EuiLoadingSpinner, EuiBadge, EuiLink } from "@elastic/eui"; +import { EuiBasicTable, EuiLoadingSpinner, EuiBadge } from "@elastic/eui"; import { FeastFeatureColumnType } from "../parsers/feastFeatureViews"; import useLoadFeatureViewSummaryStatistics from "../queries/useLoadFeatureViewSummaryStatistics"; import SparklineHistogram from "./SparklineHistogram"; diff --git a/ui/src/pages/features/FeatureOverviewTab.tsx b/ui/src/pages/features/FeatureOverviewTab.tsx index 9e4308003e..0a1c48509c 100644 --- a/ui/src/pages/features/FeatureOverviewTab.tsx +++ b/ui/src/pages/features/FeatureOverviewTab.tsx @@ -1,17 +1,15 @@ import { EuiFlexGroup, EuiHorizontalRule, - EuiLink, EuiLoadingSpinner, EuiTitle, -} from "@elastic/eui"; -import { EuiPanel, EuiFlexItem, EuiDescriptionList, EuiDescriptionListTitle, EuiDescriptionListDescription, } from "@elastic/eui"; +import EuiCustomLink from "../../components/EuiCustomLink"; import React from "react"; import { useParams } from "react-router-dom"; import useLoadFeature from "./useLoadFeature"; @@ -55,9 +53,11 @@ const FeatureOverviewTab = () => { FeatureView - + {FeatureViewName} - +