Skip to content

Commit

Permalink
feat: init components list
Browse files Browse the repository at this point in the history
  • Loading branch information
Cahllagerfeld committed Oct 23, 2024
1 parent 8b60b0d commit cfc0734
Show file tree
Hide file tree
Showing 15 changed files with 315 additions and 48 deletions.
59 changes: 59 additions & 0 deletions src/app/components/StackComponentList.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<section>
<div className="flex flex-col gap-5">
<div className="flex flex-wrap items-center justify-between gap-y-4">
<div className="flex items-center gap-2">
<SearchField searchParams={queryParams} />
</div>

<div className="flex items-center justify-between gap-2">
<Button intent="primary" emphasis="subtle" size="md" onClick={() => refetch()}>
<Refresh className="h-5 w-5 fill-theme-text-brand" />
Refresh
</Button>
</div>
</div>

<div className="flex flex-col items-center gap-5">
<div className="w-full">
{data ? (
<DataTable columns={columns} data={data.items} />
) : (
<Skeleton className="h-[500px] w-full" />
)}
</div>
{data ? (
data.total_pages > 1 && <Pagination searchParams={queryParams} paginate={data} />
) : (
<Skeleton className="h-[36px] w-[300px]" />
)}
</div>
</div>
</section>
);
}
96 changes: 96 additions & 0 deletions src/app/components/columns.tsx
Original file line number Diff line number Diff line change
@@ -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<StackComponent>[] {
return [
{
id: "name",
header: "Component",
accessorKey: "name",
cell: ({ row }) => {
const id = row.original.id;
const name = row.original.name;
return (
<div className="group/copybutton flex items-center gap-2">
<img
width={32}
height={32}
src={sanitizeUrl(row.original.body?.logo_url || "")}
alt="Flavor Icon"
/>
<div>
<div className="flex items-center gap-1">
<h2 className="text-text-md font-semibold">{name}</h2>
<CopyButton copyText={name} />
</div>
<div className="flex items-center gap-1">
<p className="text-text-xs text-theme-text-secondary">{id.split("-")[0]}</p>
<CopyButton copyText={id} />
</div>
</div>
</div>
);
}
},
{
id: "type",
header: "Component Type",
accessorFn: (row) => row.body?.type,
cell: ({ row }) => {
const type = row.original.body?.type || "orchestrator";
return <ComponentBadge type={type}>{snakeCaseToTitleCase(type)}</ComponentBadge>;
}
},
{
id: "flavor",
header: "Flavor",
accessorFn: (row) => row.body?.flavor,
cell: ({ row }) => {
const flavor = row.original.body?.flavor;
return (
<Tag
rounded={false}
className="w-fit gap-1 text-theme-text-primary"
color="grey"
emphasis="minimal"
>
<img
width={20}
height={20}
src={sanitizeUrl(row.original.body?.logo_url || "")}
alt="Flavor Icon of Component"
/>
<p>{flavor}</p>
</Tag>
);
}
},
{
id: "author",
header: "Author",
accessorFn: (row) => row.body?.user?.name,
cell: ({ row }) => {
const author = row.original.body?.user;
if (!author) return null;
return <InlineAvatar username={author.name || "default"} />;
}
},
{
id: "created",
header: "Created at",
accessorFn: (row) => row.body?.created,
cell: ({ row }) => (
<p className="text-text-sm text-theme-text-secondary">
<DisplayDate dateString={row.original.body?.created || ""} />
</p>
)
}
];
}
5 changes: 5 additions & 0 deletions src/app/components/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { StackComponentList } from "./StackComponentList";

export default function ComponentsPage() {
return <StackComponentList />;
}
22 changes: 22 additions & 0 deletions src/app/components/service.ts
Original file line number Diff line number Diff line change
@@ -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 };
}
2 changes: 1 addition & 1 deletion src/app/stacks/StackList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export function StackList() {
});

return (
<section className="p-5">
<section>
<div className="flex flex-col gap-5">
<div className="flex flex-wrap items-center justify-between gap-y-4">
<SearchField searchParams={queryParams} />
Expand Down
23 changes: 1 addition & 22 deletions src/app/stacks/page.tsx
Original file line number Diff line number Diff line change
@@ -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 }
Expand All @@ -18,22 +14,5 @@ export default function StacksPage() {
}
}, [tourActive]);

useEffect(() => {
setCurrentBreadcrumbData({ segment: "stacks", data: null });
}, []);

return (
<div>
<StacksHeader />
<StackList />
</div>
);
}

function StacksHeader() {
return (
<PageHeader>
<h1 className="text-display-xs font-semibold">Stacks</h1>
</PageHeader>
);
return <StackList />;
}
2 changes: 1 addition & 1 deletion src/components/breadcrumbs/Breadcrumbs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
7 changes: 6 additions & 1 deletion src/components/breadcrumbs/SegmentsBreadcrumbs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
},
Expand Down Expand Up @@ -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] || "#";
Expand Down
15 changes: 6 additions & 9 deletions src/data/components/index.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -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)
// })
};
1 change: 1 addition & 0 deletions src/layouts/AuthenticatedLayout/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ export function Sidebar() {
<SidebarLink
id="stacks-sidebar-link"
routePatterns={[
routes.components.overview,
routes.stacks.overview,
routes.stacks.create.index,
routes.stacks.create.newInfra,
Expand Down
21 changes: 21 additions & 0 deletions src/layouts/StackComponentsLayout/Header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { useEffect } from "react";
import { useLocation } from "react-router-dom";
import { PageHeader } from "../../components/PageHeader";
import { useBreadcrumbsContext } from "../AuthenticatedLayout/BreadcrumbsContext";

export function StackSectionHeader() {
const { setCurrentBreadcrumbData } = useBreadcrumbsContext();
const path = useLocation().pathname;
const segment = path.split("/").at(-1) as "stacks" | "components" | null;

useEffect(() => {
if (segment === "stacks") setCurrentBreadcrumbData({ segment: "stacks", data: null });
if (segment === "components") setCurrentBreadcrumbData({ segment: "components", data: null });
}, [segment]);

return (
<PageHeader>
<h1 className="text-display-xs font-semibold capitalize">{segment}</h1>
</PageHeader>
);
}
49 changes: 49 additions & 0 deletions src/layouts/StackComponentsLayout/Tabs.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Tabs onValueChange={changeValue} value={segment || "stacks"}>
<TabsList>
<TabsTrigger className="flex items-center gap-2 text-text-md" value="stacks">
<StackIcon className="h-5 w-5 fill-theme-text-tertiary group-data-[state=active]/trigger:fill-theme-surface-strong" />
<span>Stacks</span>
</TabsTrigger>
<TabsTrigger className="flex items-center gap-2 text-text-md" value="components">
<Container className="h-5 w-5 fill-theme-text-tertiary group-data-[state=active]/trigger:fill-theme-surface-strong" />
<span>Components</span>
</TabsTrigger>
</TabsList>

<TabsContent className="m-0 mt-5 border-0 bg-transparent p-0" value="stacks">
{children}
</TabsContent>
<TabsContent className="m-0 mt-5 border-0 bg-transparent p-0" value="components">
{children}
</TabsContent>
</Tabs>
);
}
16 changes: 16 additions & 0 deletions src/layouts/StackComponentsLayout/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Outlet } from "react-router-dom";
import { StackSectionHeader } from "./Header";
import { StackComponentTabs } from "./Tabs";

export function StackComponentsLayout() {
return (
<div>
<StackSectionHeader />
<section className="p-5">
<StackComponentTabs>
<Outlet />
</StackComponentTabs>
</section>
</div>
);
}
Loading

0 comments on commit cfc0734

Please sign in to comment.