From cfc0734271da6b323cb2af08111e8b31052c8474 Mon Sep 17 00:00:00 2001 From: Cahllagerfeld <43843195+Cahllagerfeld@users.noreply.github.com> Date: Wed, 23 Oct 2024 12:23:15 +0000 Subject: [PATCH] feat: init components list --- src/app/components/StackComponentList.tsx | 59 ++++++++++++ src/app/components/columns.tsx | 96 +++++++++++++++++++ src/app/components/page.tsx | 5 + src/app/components/service.ts | 22 +++++ src/app/stacks/StackList.tsx | 2 +- src/app/stacks/page.tsx | 23 +---- src/components/breadcrumbs/Breadcrumbs.tsx | 2 +- .../breadcrumbs/SegmentsBreadcrumbs.tsx | 7 +- src/data/components/index.ts | 15 ++- src/layouts/AuthenticatedLayout/Sidebar.tsx | 1 + src/layouts/StackComponentsLayout/Header.tsx | 21 ++++ src/layouts/StackComponentsLayout/Tabs.tsx | 49 ++++++++++ src/layouts/StackComponentsLayout/index.tsx | 16 ++++ src/router/Router.tsx | 41 +++++--- src/router/routes.tsx | 4 +- 15 files changed, 315 insertions(+), 48 deletions(-) create mode 100644 src/app/components/StackComponentList.tsx create mode 100644 src/app/components/columns.tsx create mode 100644 src/app/components/page.tsx create mode 100644 src/app/components/service.ts create mode 100644 src/layouts/StackComponentsLayout/Header.tsx create mode 100644 src/layouts/StackComponentsLayout/Tabs.tsx create mode 100644 src/layouts/StackComponentsLayout/index.tsx diff --git a/src/app/components/StackComponentList.tsx b/src/app/components/StackComponentList.tsx new file mode 100644 index 00000000..389454fb --- /dev/null +++ b/src/app/components/StackComponentList.tsx @@ -0,0 +1,59 @@ +import Refresh from "@/assets/icons/refresh.svg?react"; + +import { componentQueries } from "@/data/components"; +import { useQuery } from "@tanstack/react-query"; +import { Button, Skeleton } from "@zenml-io/react-component-library/components/server"; +import { getComponentList } from "./columns"; +import { useComponentlistQueryParams } from "./service"; +import { SearchField } from "../../components/SearchField"; +import { DataTable } from "@zenml-io/react-component-library"; +import Pagination from "../../components/Pagination"; + +export function StackComponentList() { + const queryParams = useComponentlistQueryParams(); + + const componentList = useQuery({ + ...componentQueries.componentList({ + ...queryParams, + sort_by: "desc:updated" + }), + throwOnError: true + }); + const columns = getComponentList(); + if (componentList.isError) return null; + const { data, refetch } = componentList; + + return ( +
+
+
+
+ +
+ +
+ +
+
+ +
+
+ {data ? ( + + ) : ( + + )} +
+ {data ? ( + data.total_pages > 1 && + ) : ( + + )} +
+
+
+ ); +} diff --git a/src/app/components/columns.tsx b/src/app/components/columns.tsx new file mode 100644 index 00000000..5cce9ca8 --- /dev/null +++ b/src/app/components/columns.tsx @@ -0,0 +1,96 @@ +import { CopyButton } from "@/components/CopyButton"; +import { DisplayDate } from "@/components/DisplayDate"; +import { InlineAvatar } from "@/components/InlineAvatar"; +import { ComponentBadge } from "@/components/stack-components/ComponentBadge"; +import { snakeCaseToTitleCase } from "@/lib/strings"; +import { sanitizeUrl } from "@/lib/url"; +import { StackComponent } from "@/types/components"; +import { ColumnDef } from "@tanstack/react-table"; +import { Tag } from "@zenml-io/react-component-library/components/server"; + +export function getComponentList(): ColumnDef[] { + return [ + { + id: "name", + header: "Component", + accessorKey: "name", + cell: ({ row }) => { + const id = row.original.id; + const name = row.original.name; + return ( +
+ Flavor Icon +
+
+

{name}

+ +
+
+

{id.split("-")[0]}

+ +
+
+
+ ); + } + }, + { + id: "type", + header: "Component Type", + accessorFn: (row) => row.body?.type, + cell: ({ row }) => { + const type = row.original.body?.type || "orchestrator"; + return {snakeCaseToTitleCase(type)}; + } + }, + { + id: "flavor", + header: "Flavor", + accessorFn: (row) => row.body?.flavor, + cell: ({ row }) => { + const flavor = row.original.body?.flavor; + return ( + + Flavor Icon of Component +

{flavor}

+
+ ); + } + }, + { + id: "author", + header: "Author", + accessorFn: (row) => row.body?.user?.name, + cell: ({ row }) => { + const author = row.original.body?.user; + if (!author) return null; + return ; + } + }, + { + id: "created", + header: "Created at", + accessorFn: (row) => row.body?.created, + cell: ({ row }) => ( +

+ +

+ ) + } + ]; +} diff --git a/src/app/components/page.tsx b/src/app/components/page.tsx new file mode 100644 index 00000000..b3027b80 --- /dev/null +++ b/src/app/components/page.tsx @@ -0,0 +1,5 @@ +import { StackComponentList } from "./StackComponentList"; + +export default function ComponentsPage() { + return ; +} diff --git a/src/app/components/service.ts b/src/app/components/service.ts new file mode 100644 index 00000000..df9da6c7 --- /dev/null +++ b/src/app/components/service.ts @@ -0,0 +1,22 @@ +import { z } from "zod"; +import { StackComponentListParams } from "@/types/components"; +import { useSearchParams } from "react-router-dom"; + +const DEFAULT_PAGE = 1; + +const filterParamsSchema = z.object({ + page: z.coerce.number().min(DEFAULT_PAGE).optional().default(DEFAULT_PAGE).catch(DEFAULT_PAGE), + name: z.string().optional(), + operator: z.enum(["and", "or"]).optional() +}); + +export function useComponentlistQueryParams(): StackComponentListParams { + const [searchParams] = useSearchParams(); + + const { page, name, operator } = filterParamsSchema.parse({ + page: searchParams.get("page") || undefined, + name: searchParams.get("name") || undefined + }); + + return { page, name, logical_operator: operator }; +} diff --git a/src/app/stacks/StackList.tsx b/src/app/stacks/StackList.tsx index 53665ae6..ccb390c6 100644 --- a/src/app/stacks/StackList.tsx +++ b/src/app/stacks/StackList.tsx @@ -27,7 +27,7 @@ export function StackList() { }); return ( -
+
diff --git a/src/app/stacks/page.tsx b/src/app/stacks/page.tsx index 04d78b86..e4cd08c9 100644 --- a/src/app/stacks/page.tsx +++ b/src/app/stacks/page.tsx @@ -1,12 +1,8 @@ import { useTourContext } from "@/components/tour/TourContext"; import { useEffect } from "react"; import { StackList } from "./StackList"; -import { useBreadcrumbsContext } from "@/layouts/AuthenticatedLayout/BreadcrumbsContext"; -import { PageHeader } from "@/components/PageHeader"; export default function StacksPage() { - const { setCurrentBreadcrumbData } = useBreadcrumbsContext(); - const { setTourState, tourState: { tourActive } @@ -18,22 +14,5 @@ export default function StacksPage() { } }, [tourActive]); - useEffect(() => { - setCurrentBreadcrumbData({ segment: "stacks", data: null }); - }, []); - - return ( -
- - -
- ); -} - -function StacksHeader() { - return ( - -

Stacks

-
- ); + return ; } diff --git a/src/components/breadcrumbs/Breadcrumbs.tsx b/src/components/breadcrumbs/Breadcrumbs.tsx index a3e83a96..c0437651 100644 --- a/src/components/breadcrumbs/Breadcrumbs.tsx +++ b/src/components/breadcrumbs/Breadcrumbs.tsx @@ -27,7 +27,7 @@ export function Breadcrumbs() { useEffect(() => { let matchedData: BreadcrumbData = {}; const pathSegments = pathname.split("/").filter((segment: string) => segment !== ""); - const segmentsToCheck: string[] = ["pipelines", "runs", "stacks", "secrets"]; + const segmentsToCheck: string[] = ["pipelines", "runs", "stacks", "secrets", "components"]; const mainPaths = segmentsToCheck.some((segment) => pathSegments.includes(segment)); if (!mainPaths) { const currentSegment = diff --git a/src/components/breadcrumbs/SegmentsBreadcrumbs.tsx b/src/components/breadcrumbs/SegmentsBreadcrumbs.tsx index af52ef75..2cc160f1 100644 --- a/src/components/breadcrumbs/SegmentsBreadcrumbs.tsx +++ b/src/components/breadcrumbs/SegmentsBreadcrumbs.tsx @@ -25,6 +25,9 @@ export const matchSegmentWithRequest = ({ segment, data }: { segment: string; da stacks: { name: "Stacks" }, create: { name: "New Stack" } }, + components: { + components: { name: "Components" } + }, secrets: { secrets: { name: "Secrets" } }, @@ -92,7 +95,9 @@ export const matchSegmentWithURL = (segment: string, id: string) => { stacks: routes.stacks.overview, createStack: routes.stacks.create.index, //Secrets - secrets: routes.settings.secrets.overview + secrets: routes.settings.secrets.overview, + //components + components: routes.components.overview }; return routeMap[segment] || "#"; diff --git a/src/data/components/index.ts b/src/data/components/index.ts index c72215f3..e8e2bb77 100644 --- a/src/data/components/index.ts +++ b/src/data/components/index.ts @@ -1,4 +1,4 @@ -import { infiniteQueryOptions } from "@tanstack/react-query"; +import { infiniteQueryOptions, queryOptions } from "@tanstack/react-query"; import { fetchComponents } from "./components-list"; import { StackComponentListParams } from "@/types/components"; @@ -11,13 +11,10 @@ export const componentQueries = { getNextPageParam: (lastPage) => lastPage.index < lastPage.total_pages ? lastPage.index + 1 : null, initialPageParam: 1 + }), + componentList: (queryParams: StackComponentListParams) => + queryOptions({ + queryKey: [...componentQueries.all, queryParams], + queryFn: async () => fetchComponents(queryParams) }) - - // This is not used for now, in case we need the infinite query, and the regular one, the queryKeys should not be the same - - // componentList: (backendUrl: string, queryParams: StackComponentListParams) => - // queryOptions({ - // queryKey: [backendUrl, ...componentQueries.all, queryParams], - // queryFn: async () => fetchComponents(backendUrl, queryParams) - // }) }; diff --git a/src/layouts/AuthenticatedLayout/Sidebar.tsx b/src/layouts/AuthenticatedLayout/Sidebar.tsx index 92e0ba53..33068d2e 100644 --- a/src/layouts/AuthenticatedLayout/Sidebar.tsx +++ b/src/layouts/AuthenticatedLayout/Sidebar.tsx @@ -98,6 +98,7 @@ export function Sidebar() { { + if (segment === "stacks") setCurrentBreadcrumbData({ segment: "stacks", data: null }); + if (segment === "components") setCurrentBreadcrumbData({ segment: "components", data: null }); + }, [segment]); + + return ( + +

{segment}

+
+ ); +} diff --git a/src/layouts/StackComponentsLayout/Tabs.tsx b/src/layouts/StackComponentsLayout/Tabs.tsx new file mode 100644 index 00000000..ca38895f --- /dev/null +++ b/src/layouts/StackComponentsLayout/Tabs.tsx @@ -0,0 +1,49 @@ +import { + Tabs, + TabsContent, + TabsList, + TabsTrigger +} from "@zenml-io/react-component-library/components/client"; +import StackIcon from "@/assets/icons/stack.svg?react"; +import Container from "@/assets/icons/container.svg?react"; +import { ReactNode } from "react"; +import { routes } from "@/router/routes"; +import { useLocation, useNavigate } from "react-router-dom"; + +export function StackComponentTabs({ children }: { children: ReactNode }) { + const navigate = useNavigate(); + + const path = useLocation().pathname; + const segment = path.split("/").at(-1) as "stacks" | "components" | null; + + function changeValue(val: string) { + if (val === "stacks") { + navigate(routes.stacks.overview); + } + if (val === "components") { + navigate(routes.components.overview); + } + } + + return ( + + + + + Stacks + + + + Components + + + + + {children} + + + {children} + + + ); +} diff --git a/src/layouts/StackComponentsLayout/index.tsx b/src/layouts/StackComponentsLayout/index.tsx new file mode 100644 index 00000000..b02fc890 --- /dev/null +++ b/src/layouts/StackComponentsLayout/index.tsx @@ -0,0 +1,16 @@ +import { Outlet } from "react-router-dom"; +import { StackSectionHeader } from "./Header"; +import { StackComponentTabs } from "./Tabs"; + +export function StackComponentsLayout() { + return ( +
+ +
+ + + +
+
+ ); +} diff --git a/src/router/Router.tsx b/src/router/Router.tsx index b6e2c40b..ab4cf768 100644 --- a/src/router/Router.tsx +++ b/src/router/Router.tsx @@ -1,16 +1,17 @@ -import { RootBoundary } from "@/error-boundaries/RootBoundary"; +import { CreateStacksLayout } from "@/app/stacks/create/layout"; +import { surveyLoader } from "@/app/survey/loader"; import { useAuthContext } from "@/context/AuthContext"; +import { RootBoundary } from "@/error-boundaries/RootBoundary"; import { AuthenticatedLayout } from "@/layouts/AuthenticatedLayout"; import { PropsWithChildren, lazy } from "react"; import { Navigate, Route, createBrowserRouter, createRoutesFromElements } from "react-router-dom"; import { PageBoundary } from "../error-boundaries/PageBoundary"; import { GradientLayout } from "../layouts/GradientLayout"; import { RootLayout } from "../layouts/RootLayout"; -import { routes } from "./routes"; +import { StackComponentsLayout } from "../layouts/StackComponentsLayout"; import { authenticatedLayoutLoader, rootLoader } from "./loaders"; import { queryClient } from "./queryclient"; -import { surveyLoader } from "@/app/survey/loader"; -import { CreateStacksLayout } from "@/app/stacks/create/layout"; +import { routes } from "./routes"; const Home = lazy(() => import("@/app/page")); const Login = lazy(() => import("@/app/login/page")); @@ -33,6 +34,9 @@ const Secrets = lazy(() => import("@/app/settings/secrets/page")); const SecretDetailsPage = lazy(() => import("@/app/settings/secrets/[id]/page")); const GeneralSettings = lazy(() => import("@/app/settings/general/page")); +// Components +const Components = lazy(() => import("@/app/components/page")); + //Stacks const Stacks = lazy(() => import("@/app/stacks/page")); const CreateStack = lazy(() => import("@/app/stacks/create/page")); @@ -212,15 +216,26 @@ export const router = createBrowserRouter( } /> - } - path={routes.stacks.overview} - element={ - - - - } - /> + }> + } + path={routes.stacks.overview} + element={ + + + + } + /> + } + path={routes.components.overview} + element={ + + + + } + /> + diff --git a/src/router/routes.tsx b/src/router/routes.tsx index 02324f4b..0734b624 100644 --- a/src/router/routes.tsx +++ b/src/router/routes.tsx @@ -18,7 +18,9 @@ export const routes = { overview: "/pipelines", namespace: (namespace: string) => `/pipelines/${namespace}` }, - + components: { + overview: "/components" + }, stacks: { overview: "/stacks", create: {