diff --git a/ui/src/FeastUISansProviders.test.tsx b/ui/src/FeastUISansProviders.test.tsx index 1289cea028..09985bc133 100644 --- a/ui/src/FeastUISansProviders.test.tsx +++ b/ui/src/FeastUISansProviders.test.tsx @@ -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(); + + // 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, + }); +}); 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/components/FeaturesListDisplay.tsx b/ui/src/components/FeaturesListDisplay.tsx index abd9c1d2e4..dcb6ba81eb 100644 --- a/ui/src/components/FeaturesListDisplay.tsx +++ b/ui/src/components/FeaturesListDisplay.tsx @@ -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) => ( + + {item} + + ) + }, { name: "Value Type", field: "valueType", }, ]; + if (!link) { + columns[0].render = undefined; + } + 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/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/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. 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..0a1c48509c --- /dev/null +++ b/ui/src/pages/features/FeatureOverviewTab.tsx @@ -0,0 +1,71 @@ +import { + EuiFlexGroup, + EuiHorizontalRule, + EuiLoadingSpinner, + EuiTitle, + 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"; + +const FeatureOverviewTab = () => { + 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; + + 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;