diff --git a/package.json b/package.json index dd5a2fba..636b8f8f 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ }, "devDependencies": { "@playwright/test": "^1.48.1", + "@tailwindcss/container-queries": "^0.1.1", "@tailwindcss/forms": "^0.5.7", "@tailwindcss/typography": "^0.5.10", "@types/lodash.debounce": "^4.0.9", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bc597181..b42cb148 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -91,6 +91,9 @@ importers: '@playwright/test': specifier: ^1.48.1 version: 1.48.1 + '@tailwindcss/container-queries': + specifier: ^0.1.1 + version: 0.1.1(tailwindcss@3.4.14) '@tailwindcss/forms': specifier: ^0.5.7 version: 0.5.7(tailwindcss@3.4.14) @@ -1495,6 +1498,11 @@ packages: '@swc/types@0.1.13': resolution: {integrity: sha512-JL7eeCk6zWCbiYQg2xQSdLXQJl8Qoc9rXmG2cEKvHe3CKwMHwHGpfOb8frzNLmbycOo6I51qxnLnn9ESf4I20Q==} + '@tailwindcss/container-queries@0.1.1': + resolution: {integrity: sha512-p18dswChx6WnTSaJCSGx6lTmrGzNNvm2FtXmiO6AuA1V4U5REyoqwmT6kgAsIMdjo07QdAfYXHJ4hnMtfHzWgA==} + peerDependencies: + tailwindcss: '>=3.2.0' + '@tailwindcss/forms@0.5.7': resolution: {integrity: sha512-QE7X69iQI+ZXwldE+rzasvbJiyV/ju1FGHH0Qn2W3FKbuYtqp8LKcy6iSw79fVUT5/Vvf+0XgLCeYVG+UV6hOw==} peerDependencies: @@ -4908,6 +4916,10 @@ snapshots: dependencies: '@swc/counter': 0.1.3 + '@tailwindcss/container-queries@0.1.1(tailwindcss@3.4.14)': + dependencies: + tailwindcss: 3.4.14 + '@tailwindcss/forms@0.5.7(tailwindcss@3.4.14)': dependencies: mini-svg-data-uri: 1.4.4 diff --git a/src/app/components/[componentId]/page.tsx b/src/app/components/[componentId]/page.tsx new file mode 100644 index 00000000..3ffb6550 --- /dev/null +++ b/src/app/components/[componentId]/page.tsx @@ -0,0 +1,19 @@ +import { useParams } from "react-router-dom"; +import { StackComponentsDetailHeader } from "../../../components/stack-components/component-detail/Header"; +import { StackComponentTabs } from "@/components/stack-components/component-detail/Tabs"; +import { StackList } from "../../stacks/StackList"; + +export default function ComponentDetailPage() { + const { componentId } = useParams() as { componentId: string }; + + return ( +
+ + } + componentId={componentId} + /> +
+ ); +} diff --git a/src/app/components/columns.tsx b/src/app/components/columns.tsx index cc46d027..3d340537 100644 --- a/src/app/components/columns.tsx +++ b/src/app/components/columns.tsx @@ -1,8 +1,8 @@ import { CopyButton } from "@/components/CopyButton"; import { DisplayDate } from "@/components/DisplayDate"; import { InlineAvatar } from "@/components/InlineAvatar"; +import { ComponentSheet } from "@/components/stack-components/component-sheet"; import { ComponentBadge } from "@/components/stack-components/ComponentBadge"; -import { ComponentFallbackDialog } from "@/components/stack-components/ComponentFallbackDialog"; import { snakeCaseToTitleCase } from "@/lib/strings"; import { sanitizeUrl } from "@/lib/url"; import { getUsername } from "@/lib/user"; @@ -29,18 +29,19 @@ export function getComponentList(): ColumnDef[] { />
- + - +
-

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

+ + +
diff --git a/src/app/stacks/ResumeBanner.tsx b/src/app/stacks/ResumeBanner.tsx new file mode 100644 index 00000000..8ce2ece6 --- /dev/null +++ b/src/app/stacks/ResumeBanner.tsx @@ -0,0 +1,21 @@ +import { useState } from "react"; +import { parseWizardData } from "./create/new-infrastructure/persist"; +import { parseWizardData as parseTerraform } from "./create/terraform/persist"; +import { ResumeStackBanner } from "./ResumeStackBanner"; +import { ResumeTerraformBanner } from "./ResumeTerraformBanner"; + +export function ResumeBanners() { + const [hasResumeableStack, setResumeableStack] = useState(parseWizardData().success); + const [hasResumeableTerraform, setResumeableTerraform] = useState( + parseTerraform().success + ); + + return ( +
+ {hasResumeableStack && } + {hasResumeableTerraform && ( + + )} +
+ ); +} diff --git a/src/app/stacks/StackList.tsx b/src/app/stacks/StackList.tsx index ccb390c6..14e53755 100644 --- a/src/app/stacks/StackList.tsx +++ b/src/app/stacks/StackList.tsx @@ -6,23 +6,19 @@ import { stackQueries } from "@/data/stacks"; import { routes } from "@/router/routes"; import { useQuery } from "@tanstack/react-query"; import { Button, DataTable, Skeleton } from "@zenml-io/react-component-library"; -import { useState } from "react"; import { Link } from "react-router-dom"; import { getStackColumns } from "./columns"; -import { parseWizardData } from "./create/new-infrastructure/persist"; -import { parseWizardData as parseTerraform } from "./create/terraform/persist"; -import { ResumeStackBanner } from "./ResumeStackBanner"; import { useStacklistQueryParams } from "./service"; -import { ResumeTerraformBanner } from "./ResumeTerraformBanner"; +import { StackListQueryParams } from "../../types/stack"; -export function StackList() { - const [hasResumeableStack, setResumeableStack] = useState(parseWizardData().success); - const [hasResumeableTerraform, setResumeableTerraform] = useState( - parseTerraform().success - ); +type Props = { + fixedQueryParams?: StackListQueryParams; +}; + +export function StackList({ fixedQueryParams = {} }: Props) { const queryParams = useStacklistQueryParams(); const { refetch, data } = useQuery({ - ...stackQueries.stackList({ ...queryParams, sort_by: "desc:updated" }), + ...stackQueries.stackList({ ...queryParams, sort_by: "desc:updated", ...fixedQueryParams }), throwOnError: true }); @@ -45,10 +41,6 @@ export function StackList() {
- {hasResumeableStack && } - {hasResumeableTerraform && ( - - )}
{data ? ( diff --git a/src/app/stacks/page.tsx b/src/app/stacks/page.tsx index e4cd08c9..742a0f48 100644 --- a/src/app/stacks/page.tsx +++ b/src/app/stacks/page.tsx @@ -1,6 +1,7 @@ import { useTourContext } from "@/components/tour/TourContext"; import { useEffect } from "react"; import { StackList } from "./StackList"; +import { ResumeBanners } from "./ResumeBanner"; export default function StacksPage() { const { @@ -14,5 +15,10 @@ export default function StacksPage() { } }, [tourActive]); - return ; + return ( +
+ + +
+ ); } diff --git a/src/assets/icons/expand-full.svg b/src/assets/icons/expand-full.svg new file mode 100644 index 00000000..ed43f01f --- /dev/null +++ b/src/assets/icons/expand-full.svg @@ -0,0 +1,4 @@ + + + diff --git a/src/components/Pagination.tsx b/src/components/Pagination.tsx index 4a5a7c12..28da215b 100644 --- a/src/components/Pagination.tsx +++ b/src/components/Pagination.tsx @@ -10,6 +10,7 @@ import { useNavigate } from "react-router-dom"; import { ResponsePage } from "@/types/common"; type Props = { + inMemoryHandler?: (page: number) => void; // Maybe handle this with a generic? // eslint-disable-next-line @typescript-eslint/no-explicit-any searchParams: Record; @@ -17,12 +18,16 @@ type Props = { paginate: Omit, "items">; }; -export default function Pagination({ paginate, searchParams }: Props) { +export default function Pagination({ paginate, searchParams, inMemoryHandler }: Props) { const navigate = useNavigate(); const { index, total_pages } = paginate; function goToPage(page: number) { + if (!!inMemoryHandler) { + inMemoryHandler(page); + return; + } const queryParams = new URLSearchParams(objectToSearchParams(searchParams)); queryParams.set("page", page.toString()); diff --git a/src/components/SearchField.tsx b/src/components/SearchField.tsx index 919dd745..bd0491bd 100644 --- a/src/components/SearchField.tsx +++ b/src/components/SearchField.tsx @@ -6,6 +6,7 @@ import { InputHTMLAttributes, forwardRef, useEffect, useMemo, useState } from "r import { useNavigate, useSearchParams } from "react-router-dom"; type Props = { + inMemoryHandler?: (val: string) => void; searchContains?: boolean; // eslint-disable-next-line @typescript-eslint/no-explicit-any searchParams: Record; @@ -14,7 +15,7 @@ type Props = { export const SearchField = forwardRef< HTMLInputElement, InputHTMLAttributes & Props ->(({ searchParams, searchContains = true, ...rest }, ref) => { +>(({ searchParams, searchContains = true, inMemoryHandler, ...rest }, ref) => { const navigate = useNavigate(); const [existingParams] = useSearchParams(); @@ -35,6 +36,10 @@ export const SearchField = forwardRef< }, [debouncedSearch]); function updateSearchQuery(value: string) { + if (!!inMemoryHandler) { + inMemoryHandler(value); + return; + } const queryParams = new URLSearchParams({ ...Object.fromEntries(existingParams), ...objectToSearchParams(searchParams) diff --git a/src/components/artifacts/artifact-node-sheet/DetailCards.tsx b/src/components/artifacts/artifact-node-sheet/DetailCards.tsx index 539a9675..c0da40e3 100644 --- a/src/components/artifacts/artifact-node-sheet/DetailCards.tsx +++ b/src/components/artifacts/artifact-node-sheet/DetailCards.tsx @@ -7,7 +7,6 @@ import { ExecutionStatusIcon, getExecutionStatusTagColor } from "@/components/Ex import { InlineAvatar } from "@/components/InlineAvatar"; import { Key, KeyValue, Value } from "@/components/KeyValue"; import { useArtifactVersion } from "@/data/artifact-versions/artifact-version-detail-query"; -import { useComponentDetail } from "@/data/components/component-detail-query"; import { usePipelineRun } from "@/data/pipeline-runs/pipeline-run-detail-query"; import { useStepDetail } from "@/data/steps/step-detail-query"; import { routes } from "@/router/routes"; @@ -22,6 +21,8 @@ import { import { Link } from "react-router-dom"; import { Codesnippet } from "../../CodeSnippet"; import { CollapsibleCard } from "../../CollapsibleCard"; +import { useQuery } from "@tanstack/react-query"; +import { componentQueries } from "../../../data/components"; type Props = { artifactVersionId: string; @@ -177,12 +178,10 @@ export function DataCard({ artifactVersionId }: Props) { } = useArtifactVersion({ versionId: artifactVersionId }); const artifactStoreId = artifactVersionData?.metadata?.artifact_store_id; - const { data: storeData, isSuccess: isStoreSuccess } = useComponentDetail( - { - componentId: artifactStoreId! - }, - { enabled: !!artifactStoreId } - ); + const { data: storeData, isSuccess: isStoreSuccess } = useQuery({ + ...componentQueries.componentDetail(artifactStoreId!), + enabled: !!artifactStoreId + }); if (isArtifactVersionError) { return ; diff --git a/src/components/breadcrumbs/SegmentsBreadcrumbs.tsx b/src/components/breadcrumbs/SegmentsBreadcrumbs.tsx index 2cc160f1..54b0aeea 100644 --- a/src/components/breadcrumbs/SegmentsBreadcrumbs.tsx +++ b/src/components/breadcrumbs/SegmentsBreadcrumbs.tsx @@ -28,6 +28,13 @@ export const matchSegmentWithRequest = ({ segment, data }: { segment: string; da components: { components: { name: "Components" } }, + componentDetail: { + components: { name: "Components" }, + component_detail: { + id: data?.id, + name: data?.name + } + }, secrets: { secrets: { name: "Secrets" } }, @@ -113,7 +120,8 @@ export const matchSegmentWithTab = (segment: string) => { metadata: , runs: , templates: , - stack: + stack: , + stacks: }; return routeMap[segment] || ; diff --git a/src/components/stack-components/ComponentFallbackDialog.tsx b/src/components/stack-components/ComponentFallbackDialog.tsx deleted file mode 100644 index 5070727b..00000000 --- a/src/components/stack-components/ComponentFallbackDialog.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import { snakeCaseToDashCase, snakeCaseToLowerCase, snakeCaseToTitleCase } from "@/lib/strings"; -import { StackComponentType } from "@/types/components"; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogTrigger -} from "@zenml-io/react-component-library"; -import { ComponentPropsWithoutRef, ElementRef, forwardRef } from "react"; -import { Codesnippet } from "../CodeSnippet"; -import { InfoBox } from "../Infobox"; - -type Props = { - name: string; - type: StackComponentType; -}; - -export const ComponentFallbackDialog = forwardRef< - ElementRef, - ComponentPropsWithoutRef & Props ->(({ name, children, type, ...rest }, ref) => { - return ( - - {children} - - - {snakeCaseToTitleCase(type || "")} - -
- -
-

- Describe your {snakeCaseToLowerCase(type)} -

- -
-
-

- Update your {snakeCaseToLowerCase(type)} -

- -
-
-
-
- ); -}); - -ComponentFallbackDialog.displayName = "ComponentFallbackDialog"; - -type InfoProps = { - type: StackComponentType; -}; -function Info({ type }: InfoProps) { - return ( - -
-
-

- We are working on the new Stack Components experience. -

-

- Meanwhile you can use the CLI to manage your {snakeCaseToLowerCase(type)}. -

-
-
-
- ); -} diff --git a/src/components/stack-components/component-detail/ConfigTab.tsx b/src/components/stack-components/component-detail/ConfigTab.tsx new file mode 100644 index 00000000..c20368a9 --- /dev/null +++ b/src/components/stack-components/component-detail/ConfigTab.tsx @@ -0,0 +1,117 @@ +import ChevronDown from "@/assets/icons/chevron-down.svg?react"; +import { + CollapsibleContent, + CollapsibleHeader, + CollapsiblePanel, + CollapsibleTrigger +} from "@zenml-io/react-component-library"; +import { sanitizeUrl } from "@/lib/url"; +import { getUsername } from "@/lib/user"; +import { Skeleton, Tag } from "@zenml-io/react-component-library/components/server"; +import { useState } from "react"; +import { useComponent } from "./hooks"; +import { KeyValue } from "../../KeyValue"; +import { DisplayDate } from "../../DisplayDate"; +import { InlineAvatar } from "../../InlineAvatar"; +import { NestedCollapsible } from "../../NestedCollapsible"; + +type Props = { + componentId: string; +}; +export function ComponentConfigTab({ componentId }: Props) { + return ( +
+
+ +
+
+ +
+
+ ); +} + +function BasicParams({ componentId }: Props) { + const [open, setOpen] = useState(true); + const component = useComponent(componentId); + if (component.isError) return null; + if (component.isPending) return ; + + const user = component.data.body?.user; + const created = component.data.body?.created; + const updated = component.data.body?.updated; + + return ( + + + + + Basic Parameters + + + +
+ {/* +

{component.data.id}

+ +
+ } + /> */} + + + Flavor Icon of Component +

{component.data.body?.flavor}

+ + } + /> + : "Not available"} + /> + : "Not available"} + /> + : "Not available"} + /> + + + + ); +} + +function ConfigCollapsible({ componentId }: Props) { + const component = useComponent(componentId); + if (component.isError) return null; + if (component.isPending) return ; + return ( + + ); +} diff --git a/src/components/stack-components/component-detail/Header.tsx b/src/components/stack-components/component-detail/Header.tsx new file mode 100644 index 00000000..2c4806b3 --- /dev/null +++ b/src/components/stack-components/component-detail/Header.tsx @@ -0,0 +1,85 @@ +import { snakeCaseToTitleCase } from "@/lib/strings"; +import { sanitizeUrl } from "@/lib/url"; +import { Skeleton, Badge } from "@zenml-io/react-component-library/components/server"; +import { useComponent } from "./hooks"; +import { useEffect } from "react"; +import { PageHeader } from "../../PageHeader"; +import { useBreadcrumbsContext } from "../../../layouts/AuthenticatedLayout/BreadcrumbsContext"; +import { CopyButton } from "../../CopyButton"; + +type Props = { + componentId: string; +}; +export function StackComponentsDetailHeader({ + componentId, + isPanel +}: Props & { isPanel: boolean }) { + return ( + + +
+ + + +
+
+ ); +} + +function ComponentId({ componentId, isPanel }: Props & { isPanel: boolean }) { + const component = useComponent(componentId); + const { setCurrentBreadcrumbData } = useBreadcrumbsContext(); + useEffect(() => { + if (component.data && !isPanel) { + setCurrentBreadcrumbData({ segment: "componentDetail", data: component.data }); + } + }, [component.data, isPanel]); + + if (component.isError) return null; + if (component.isPending) return ; + const id = component.data.id; + return ( +
+

{id}

+ +
+ ); +} + +function ComponentName({ componentId }: Props) { + const component = useComponent(componentId); + + if (component.isError) return null; + if (component.isPending) return ; + return

{component.data.name}

; +} + +function ComponentIcon({ componentId }: Props) { + const component = useComponent(componentId); + + if (component.isError) return null; + if (component.isPending) return ; + return ( + {`Icon + ); +} + +function ComponentType({ componentId }: Props) { + const component = useComponent(componentId); + + if (component.isError) return null; + if (component.isPending) return ; + return ( + + {snakeCaseToTitleCase(component.data.body?.type || "")} + + ); +} diff --git a/src/components/stack-components/component-detail/Tabs.tsx b/src/components/stack-components/component-detail/Tabs.tsx new file mode 100644 index 00000000..d2e9f98d --- /dev/null +++ b/src/components/stack-components/component-detail/Tabs.tsx @@ -0,0 +1,52 @@ +import Stacks from "@/assets/icons/stack.svg?react"; +import Tools from "@/assets/icons/tool-02.svg?react"; +import { ComponentConfigTab } from "./ConfigTab"; +import { useSelectedTab } from "./service"; +import { ReactNode, useState } from "react"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@zenml-io/react-component-library"; +import { useNavigate } from "react-router-dom"; + +type Props = { + componentId: string; + isPanel: boolean; + stacksTabContent: ReactNode; +}; +export function StackComponentTabs({ componentId, isPanel, stacksTabContent }: Props) { + const [inMemoryTab, setInMemoryTab] = useState("configuration"); + const selectedTab = useSelectedTab(); + const navigate = useNavigate(); + + function handleTabChage(tab: string) { + if (isPanel) { + setInMemoryTab(tab); + return; + } + const current = new URLSearchParams(); + current.set("tab", tab); + navigate(`?${current.toString()}`); + } + + return ( +
+ + + + + Configuration + + + + Stacks + + + + + + + + {stacksTabContent} + + +
+ ); +} diff --git a/src/components/stack-components/component-detail/hooks.ts b/src/components/stack-components/component-detail/hooks.ts new file mode 100644 index 00000000..98b3f624 --- /dev/null +++ b/src/components/stack-components/component-detail/hooks.ts @@ -0,0 +1,6 @@ +import { componentQueries } from "@/data/components"; +import { useQuery } from "@tanstack/react-query"; + +export function useComponent(id: string) { + return useQuery({ ...componentQueries.componentDetail(id), throwOnError: true }); +} diff --git a/src/components/stack-components/component-detail/service.ts b/src/components/stack-components/component-detail/service.ts new file mode 100644 index 00000000..42c23e9e --- /dev/null +++ b/src/components/stack-components/component-detail/service.ts @@ -0,0 +1,15 @@ +import { useSearchParams } from "react-router-dom"; +import { z } from "zod"; + +const tabParamSchema = z.object({ + tab: z.enum(["configuration", "stacks"]).optional().default("configuration").catch("stacks") +}); + +export function useSelectedTab() { + const [searchParams] = useSearchParams(); + const { tab } = tabParamSchema.parse({ + tab: searchParams.get("tab") || undefined + }); + + return tab; +} diff --git a/src/components/stack-components/component-sheet/index.tsx b/src/components/stack-components/component-sheet/index.tsx new file mode 100644 index 00000000..dc12aee2 --- /dev/null +++ b/src/components/stack-components/component-sheet/index.tsx @@ -0,0 +1,48 @@ +"use client"; +import ChevronRight from "@/assets/icons/chevron-right-double.svg?react"; +import Expand from "@/assets/icons/expand-full.svg?react"; +import { Sheet, SheetClose, SheetContent, SheetTrigger } from "@zenml-io/react-component-library"; +import { routes } from "@/router/routes"; +import { Button } from "@zenml-io/react-component-library/components/server"; +import { PropsWithChildren } from "react"; +import { Link } from "react-router-dom"; +import { StackComponentsDetailHeader } from "../component-detail/Header"; +import { StackComponentTabs } from "../component-detail/Tabs"; +import { StackList } from "./stacks-tab/StackList"; + +type Props = { + componentId: string; + onOpenChange?: (isOpen: boolean) => void; +}; + +export function ComponentSheet({ children, onOpenChange, componentId }: PropsWithChildren) { + return ( + + {children} + +
+ + +
+
+ + } + componentId={componentId} + /> +
+
+
+ ); +} diff --git a/src/components/stack-components/component-sheet/stacks-tab/StackList.tsx b/src/components/stack-components/component-sheet/stacks-tab/StackList.tsx new file mode 100644 index 00000000..18923601 --- /dev/null +++ b/src/components/stack-components/component-sheet/stacks-tab/StackList.tsx @@ -0,0 +1,76 @@ +"use client"; +import Refresh from "@/assets/icons/refresh.svg?react"; +import { stackQueries } from "@/data/stacks"; +import { StackListQueryParams } from "@/types/stack"; +import { useQuery } from "@tanstack/react-query"; +import { DataTable } from "@zenml-io/react-component-library"; +import { Button, Skeleton } from "@zenml-io/react-component-library/components/server"; +import { useState } from "react"; +import Pagination from "../../../Pagination"; +import { SearchField } from "../../../SearchField"; +import { getStackColumnsPanel } from "./columns"; + +type Props = { + componentId: string; +}; + +export function StackList({ componentId }: Props) { + const [search, setSearch] = useState(undefined); + const [page, setPage] = useState(1); + // const { orgId, tenantID } = useParams<{ orgId: string; tenantID: string }>(); + + const inlineQueries: StackListQueryParams = { + name: search, + page + }; + + const { refetch, data } = useQuery( + stackQueries.stackList({ + ...inlineQueries, + component_id: componentId, + sort_by: "desc:updated" + }) + ); + + return ( +
+
+
+
+ { + setPage(1); + if (!Boolean(val)) setSearch(undefined); + setSearch(`contains:${val}`); + }} + searchParams={{}} + /> +
+ +
+ +
+
+
+
+ {data ? ( + + ) : ( + + )} +
+ {data ? ( + data.total_pages > 1 && ( + + ) + ) : ( + + )} +
+
+
+ ); +} diff --git a/src/components/stack-components/component-sheet/stacks-tab/columns.tsx b/src/components/stack-components/component-sheet/stacks-tab/columns.tsx new file mode 100644 index 00000000..64f6b514 --- /dev/null +++ b/src/components/stack-components/component-sheet/stacks-tab/columns.tsx @@ -0,0 +1,56 @@ +import { Stack } from "@/types/stack"; +import { User } from "@/types/user"; +import { ColumnDef } from "@tanstack/react-table"; +import { Avatar, AvatarFallback } from "@zenml-io/react-component-library"; +import { CopyButton } from "../../../CopyButton"; +import { DisplayDate } from "../../../DisplayDate"; +import { InlineAvatar } from "../../../InlineAvatar"; + +export function getStackColumnsPanel(): ColumnDef[] { + return [ + { + id: "name", + header: "Stack", + accessorFn: (row) => row.name, + cell: ({ row }) => { + const { name, id } = row.original; + return ( +
+ + {name[0]} + +
+
+

{name}

+
+
+

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

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

+ ()} /> +

+ ) + } + ]; +} diff --git a/src/components/stacks/Sheet/index.tsx b/src/components/stacks/Sheet/index.tsx index 9769875e..7f3cf6a2 100644 --- a/src/components/stacks/Sheet/index.tsx +++ b/src/components/stacks/Sheet/index.tsx @@ -20,8 +20,9 @@ import { PropsWithChildren, useEffect } from "react"; import { CopyButton } from "../../CopyButton"; import { Numberbox } from "../../NumberBox"; import { ComponentBadge } from "../../stack-components/ComponentBadge"; -import { ComponentFallbackDialog } from "../../stack-components/ComponentFallbackDialog"; import { IntegrationsContextProvider, useIntegrationsContext } from "./IntegrationsContext"; +import { Link } from "react-router-dom"; +import { routes } from "../../../router/routes"; type Props = { stackId: string; @@ -137,12 +138,9 @@ function ComponentListItem({ component }: ComponentListItemProps) { src={sanitizeUrl(component.body?.logo_url || "")} />
- - - + + {component.name} +

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

diff --git a/src/components/stacks/info/ComponentCollapsible.tsx b/src/components/stacks/info/ComponentCollapsible.tsx index 2055aba9..08da1b91 100644 --- a/src/components/stacks/info/ComponentCollapsible.tsx +++ b/src/components/stacks/info/ComponentCollapsible.tsx @@ -1,11 +1,12 @@ import { snakeCaseToTitleCase } from "@/lib/strings"; import { sanitizeUrl } from "@/lib/url"; +import { routes } from "@/router/routes"; import { PipelineRun } from "@/types//pipeline-runs"; import { StackComponent } from "@/types/components"; import { Box, Button } from "@zenml-io/react-component-library/components/server"; +import { Link } from "react-router-dom"; import { NestedCollapsible } from "../../NestedCollapsible"; import { ComponentBadge } from "../../stack-components/ComponentBadge"; -import { ComponentInfoDialog } from "./ComponentInfoDialog"; type Props = { component: StackComponent; @@ -18,13 +19,11 @@ export function ComponentCollapsible({ component, run }: Props) { if (!settings || Object.keys(settings).length === 0) { return ( - - - + + + + + ); } @@ -41,15 +40,14 @@ export function ComponentCollapsible({ component, run }: Props) { } data={settings} > - - - + ); } diff --git a/src/components/stacks/info/ComponentInfoDialog.tsx b/src/components/stacks/info/ComponentInfoDialog.tsx deleted file mode 100644 index e04873b0..00000000 --- a/src/components/stacks/info/ComponentInfoDialog.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { snakeCaseToDashCase, snakeCaseToLowerCase, snakeCaseToTitleCase } from "@/lib/strings"; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogTrigger -} from "@zenml-io/react-component-library/components/client"; -import { ComponentPropsWithoutRef, ElementRef, forwardRef } from "react"; -import { InfoBox } from "../../Infobox"; -import { StackComponentType } from "@/types/components"; -import { Codesnippet } from "../../CodeSnippet"; - -type Props = { - name: string; - type: StackComponentType; -}; -export const ComponentInfoDialog = forwardRef< - ElementRef, - ComponentPropsWithoutRef & Props ->(({ name, children, type, ...rest }, ref) => { - { - return ( - - {children} - - - {snakeCaseToTitleCase(type || "")} - -
- -
-
-

- We are working on the new Stacks experience. -

-

- Meanwhile you can use the CLI to check the details of your component. -

-
-
-
-
-

- Describe your {snakeCaseToLowerCase(type)} -

- -
-
-
-
- ); - } -}); - -ComponentInfoDialog.displayName = "ComponentInfoDialog"; diff --git a/src/data/components/component-detail-query.ts b/src/data/components/component-detail-query.ts index 1851ed6f..ac22c68f 100644 --- a/src/data/components/component-detail-query.ts +++ b/src/data/components/component-detail-query.ts @@ -1,20 +1,12 @@ import { FetchError } from "@/lib/fetch-error"; -import { createApiPath, apiPaths } from "../api"; -import { UseQueryOptions, useQuery } from "@tanstack/react-query"; -import { StackComponent } from "@/types/components"; import { notFound } from "@/lib/not-found-error"; +import { StackComponent } from "@/types/components"; +import { apiPaths, createApiPath } from "../api"; import { fetcher } from "../fetch"; -type ComponentDetail = { - componentId: string; -}; - -export function getComponentDetailQueryKey({ componentId }: ComponentDetail) { - return ["components", componentId]; -} - -export async function fetchComponentDetail({ componentId }: ComponentDetail) { +export async function fetchComponentDetail(componentId: string): Promise { const url = createApiPath(apiPaths.components.detail(componentId)); + const res = await fetcher(url, { method: "GET", headers: { @@ -22,27 +14,23 @@ export async function fetchComponentDetail({ componentId }: ComponentDetail) { } }); - if (res.status === 404) { - notFound(); - } + if (res.status === 404) notFound(); if (!res.ok) { + const errorData: string = await res + .json() + .then((data) => { + if (Array.isArray(data.detail)) { + return data.detail[1]; + } + return data.detail; + }) + .catch(() => "Error while fetching stack components"); throw new FetchError({ - message: `Error while fetching component ${componentId}`, status: res.status, - statusText: res.statusText + statusText: res.statusText, + message: errorData }); } return res.json(); } - -export function useComponentDetail( - params: ComponentDetail, - options?: Omit, "queryKey" | "queryFn"> -) { - return useQuery({ - queryKey: getComponentDetailQueryKey(params), - queryFn: () => fetchComponentDetail(params), - ...options - }); -} diff --git a/src/data/components/components-list.ts b/src/data/components/components-list.ts index 01a698ef..768527c3 100644 --- a/src/data/components/components-list.ts +++ b/src/data/components/components-list.ts @@ -1,10 +1,9 @@ import { FetchError } from "@/lib/fetch-error"; +import { notFound } from "@/lib/not-found-error"; import { objectToSearchParams } from "@/lib/url"; - +import { StackComponentListParams, StackComponentPage } from "@/types/components"; import { apiPaths, createApiPath } from "../api"; import { fetcher } from "../fetch"; -import { notFound } from "@/lib/not-found-error"; -import { StackComponentListParams, StackComponentPage } from "@/types/components"; export async function fetchComponents( queryParams: StackComponentListParams diff --git a/src/data/components/index.ts b/src/data/components/index.ts index e8e2bb77..43a55518 100644 --- a/src/data/components/index.ts +++ b/src/data/components/index.ts @@ -1,12 +1,13 @@ import { infiniteQueryOptions, queryOptions } from "@tanstack/react-query"; import { fetchComponents } from "./components-list"; import { StackComponentListParams } from "@/types/components"; +import { fetchComponentDetail } from "./component-detail-query"; export const componentQueries = { all: ["components"], componentListInfinite: (queryParams: StackComponentListParams) => infiniteQueryOptions({ - queryKey: [...componentQueries.all, queryParams], + queryKey: [...componentQueries.all, queryParams, "infinite"], queryFn: async ({ pageParam }) => fetchComponents({ ...queryParams, page: pageParam }), getNextPageParam: (lastPage) => lastPage.index < lastPage.total_pages ? lastPage.index + 1 : null, @@ -16,5 +17,10 @@ export const componentQueries = { queryOptions({ queryKey: [...componentQueries.all, queryParams], queryFn: async () => fetchComponents(queryParams) + }), + componentDetail: (componentId: string) => + queryOptions({ + queryKey: [...componentQueries.all, componentId], + queryFn: async () => fetchComponentDetail(componentId) }) }; diff --git a/src/layouts/AuthenticatedLayout/Sidebar.tsx b/src/layouts/AuthenticatedLayout/Sidebar.tsx index 33068d2e..1cb00868 100644 --- a/src/layouts/AuthenticatedLayout/Sidebar.tsx +++ b/src/layouts/AuthenticatedLayout/Sidebar.tsx @@ -99,6 +99,7 @@ export function Sidebar() { id="stacks-sidebar-link" routePatterns={[ routes.components.overview, + routes.components.detail(":componentId"), routes.stacks.overview, routes.stacks.create.index, routes.stacks.create.newInfra, diff --git a/src/router/Router.tsx b/src/router/Router.tsx index 190205fc..dc94ced3 100644 --- a/src/router/Router.tsx +++ b/src/router/Router.tsx @@ -36,6 +36,7 @@ const GeneralSettings = lazy(() => import("@/app/settings/general/page")); // Components const Components = lazy(() => import("@/app/components/page")); +const ComponentDetail = lazy(() => import("@/app/components/[componentId]/page")); //Stacks const Stacks = lazy(() => import("@/app/stacks/page")); @@ -216,6 +217,15 @@ export const router = createBrowserRouter( } /> + } + path={routes.components.detail(":componentId")} + element={ + + + + } + /> }> } diff --git a/src/router/routes.tsx b/src/router/routes.tsx index 0734b624..dac92bf7 100644 --- a/src/router/routes.tsx +++ b/src/router/routes.tsx @@ -19,7 +19,8 @@ export const routes = { namespace: (namespace: string) => `/pipelines/${namespace}` }, components: { - overview: "/components" + overview: "/components", + detail: (componentId: string) => `/components/${componentId}` }, stacks: { overview: "/stacks", diff --git a/tailwind.config.js b/tailwind.config.js index 4c6f7c45..0eed910c 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -18,7 +18,8 @@ export default { plugins: [ require("@tailwindcss/forms"), require("tailwindcss-animate"), - require("@tailwindcss/typography") + require("@tailwindcss/typography"), + require("@tailwindcss/container-queries") ], presets: [zenmlPreset] };