diff --git a/package.json b/package.json index a901e6d02..32da6713b 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "prismjs": "^1.29.0", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-error-boundary": "^4.0.13", "react-hook-form": "^7.51.5", "react-joyride": "^2.8.2", "react-markdown": "^9.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 58bac980a..5fcd6c7e6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -59,6 +59,9 @@ importers: react-dom: specifier: ^18.3.1 version: 18.3.1(react@18.3.1) + react-error-boundary: + specifier: ^4.0.13 + version: 4.0.13(react@18.3.1) react-hook-form: specifier: ^7.51.5 version: 7.51.5(react@18.3.1) @@ -2961,6 +2964,11 @@ packages: peerDependencies: react: ^18.3.1 + react-error-boundary@4.0.13: + resolution: {integrity: sha512-b6PwbdSv8XeOSYvjt8LpgpKrZ0yGdtZokYwkwV2wlcZbxgopHX/hgPl5VgpnoVOWd868n1hktM8Qm4b+02MiLQ==} + peerDependencies: + react: '>=16.13.1' + react-floater@0.7.9: resolution: {integrity: sha512-NXqyp9o8FAXOATOEo0ZpyaQ2KPb4cmPMXGWkx377QtJkIXHlHRAGer7ai0r0C1kG5gf+KJ6Gy+gdNIiosvSicg==} peerDependencies: @@ -6452,6 +6460,11 @@ snapshots: react: 18.3.1 scheduler: 0.23.2 + react-error-boundary@4.0.13(react@18.3.1): + dependencies: + '@babel/runtime': 7.24.1 + react: 18.3.1 + react-floater@0.7.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: deepmerge: 4.3.1 diff --git a/src/app/onboarding/Header.tsx b/src/app/onboarding/Header.tsx index 843eac2c0..5d57a492e 100644 --- a/src/app/onboarding/Header.tsx +++ b/src/app/onboarding/Header.tsx @@ -1,16 +1,30 @@ -import { Box } from "@zenml-io/react-component-library"; +import { useCurrentUser } from "@/data/users/current-user-query"; +import { getUsername } from "@/lib/user"; +import { Skeleton } from "@zenml-io/react-component-library"; +import { ProgressIndicatior } from "./ProgressIndicator"; export function HeaderOnboardingBox() { return ( - -
-

Welcome to ZenML

-

- You successfully installed the ZenML dashboard. Now you can get started with your new - ZenML server. +

+
+

+ Welcome to ZenML + +

+

+ You can start by following your quick setup.

- {/*
*/} - + +
); } + +function Username() { + const user = useCurrentUser(); + + if (user.isError) return null; + if (user.isPending) return ; + const name = getUsername(user.data); + return <>{name ? `, ${name}` : ""}; +} diff --git a/src/app/onboarding/ProductionSetup/ArtifactStore.tsx b/src/app/onboarding/ProductionSetup/ArtifactStore.tsx deleted file mode 100644 index 79a61f336..000000000 --- a/src/app/onboarding/ProductionSetup/ArtifactStore.tsx +++ /dev/null @@ -1,207 +0,0 @@ -import { Codesnippet } from "@/components/CodeSnippet"; -import { buttonVariants } from "@zenml-io/react-component-library"; -import { LearnMoreLink } from "./Items"; -import { CloudProvider } from "./ProviderSelect"; - -function AWSStore() { - return ( - <> -
-

- Install the AWS CLI on your machine -

-
- - How to install the AWS CLI - -
-
-
-

- Create a AWS bucket or identify an existing one{" "} - (Optional){" "} - -

- -
-
-

Install the S3 ZenML integration

- -
-
-

Register the artifact store

- -
- - ); -} - -function GCPStore() { - return ( - <> -
-

- Install the Google Cloud CLI on your machine -

-
- - How to install the Google Cloud CLI - -
-
-
-

- Create a GCP bucket or identify an existing one{" "} - (Optional){" "} - -

- -
-
-

Install the GCP ZenML integration

- -
-
-

Register the artifact store

- -
- - ); -} - -function AzureStore() { - return ( - <> -
-

- Install the Azure CLI on your machine -

-
- - How to install the Azure CLI - -
-
-
-

- Create a Azure bucket or identify an existing one{" "} - (Optional){" "} - -

- -
-
-

- Install the Azure ZenML integration{" "} -

- -
-
-

Register the artifact store

- -
- - ); -} - -function OtherStore() { - return ( -
-

- Create a remote artifact in any environment following our guides. -

-
- - How to create a custom artifact store - -
-
- ); -} - -export function getArtifactStoreStep(provider: CloudProvider) { - switch (provider) { - case "gcp": - return ; - case "aws": - return ; - case "azure": - return ; - case "other": - return ; - default: - break; - } -} diff --git a/src/app/onboarding/ProductionSetup/ConnectorContent.tsx b/src/app/onboarding/ProductionSetup/ConnectorContent.tsx deleted file mode 100644 index 45a5227fc..000000000 --- a/src/app/onboarding/ProductionSetup/ConnectorContent.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import { Codesnippet } from "@/components/CodeSnippet"; -import { LearnMoreLink } from "./Items"; -import { CloudProvider } from "./ProviderSelect"; - -function AWSConnector() { - return ( - <> -
-

- Create a AWS service connector using the IAM Method{" "} - -

- -
- - ); -} - -function GCPConnector() { - return ( - <> -
-

- Create a GCP service connector using the Service Account Method{" "} - -

- -
- - ); -} - -function AzureStore() { - return ( - <> -
-

- Create an Azure service connector using the Service Principal Method{" "} - -

- -
- - ); -} - -export function getServiceConnectorStep(provider: CloudProvider) { - switch (provider) { - case "gcp": - return ; - case "aws": - return ; - case "azure": - return ; - default: - break; - } -} diff --git a/src/app/onboarding/ProductionSetup/Items.tsx b/src/app/onboarding/ProductionSetup/Items.tsx deleted file mode 100644 index 0986ece7a..000000000 --- a/src/app/onboarding/ProductionSetup/Items.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import Plus from "@/assets/icons/plus.svg?react"; -import { Codesnippet } from "@/components/CodeSnippet"; -import { HelpBox } from "@/components/fallback-pages/Helpbox"; -import { ChecklistItem } from "@/components/onboarding/ChecklistItem"; -import {} from "@/lib/onboarding"; -import { routes } from "@/router/routes"; -import { OnboardingStep } from "@/types/onboarding"; -import { Button } from "@zenml-io/react-component-library"; -import { Link } from "react-router-dom"; - -export function CreateNewStack({ completed, active, hasDownstreamStep }: OnboardingStep) { - const link = - routes.stacks.create.index + "?" + new URLSearchParams({ origin: "onboarding" }).toString(); - return ( - -

- A stack configures how a pipeline is executed{" "} - -

-
-
-

Connect your Cloud to deploy your ZenML pipelines in a remote stack.

- -
- -
-
- ); -} - -export function RunNewPipeline({ active, completed, hasDownstreamStep }: OnboardingStep) { - return ( - -
-
-

Set the new stack

- -
-
-

Run the pipeline

- -
-
- -
-
-
- ); -} - -export function LearnMoreLink({ href }: { href: string }) { - return ( - - Learn more - - ); -} diff --git a/src/app/onboarding/ProductionSetup/ProviderSelect.tsx b/src/app/onboarding/ProductionSetup/ProviderSelect.tsx deleted file mode 100644 index a0d69ca57..000000000 --- a/src/app/onboarding/ProductionSetup/ProviderSelect.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import Aws from "@/assets/icons/services/aws.svg?react"; -import Gcp from "@/assets/icons/services/gcp.svg?react"; -import Azure from "@/assets/icons/services/azure.svg?react"; -import Cloud from "@/assets/icons/cloud.svg?react"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue -} from "@zenml-io/react-component-library"; - -type Props = { - displayOther?: boolean; - setValue?: (value: CloudProvider) => void; - value?: CloudProvider; - id: string; -}; - -export type CloudProvider = "gcp" | "aws" | "azure" | "other"; - -export function ProviderSelect({ displayOther = false, setValue, value, id }: Props) { - return ( - - ); -} diff --git a/src/app/onboarding/ProductionSetup/index.tsx b/src/app/onboarding/ProductionSetup/index.tsx deleted file mode 100644 index 3d8c60deb..000000000 --- a/src/app/onboarding/ProductionSetup/index.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import ChevronDown from "@/assets/icons/chevron-down.svg?react"; -import { Tick } from "@/components/Tick"; -import { useServerInfo } from "@/data/server/info-query"; -import { useOnboarding } from "@/data/server/onboarding-state"; -import { getProductionSetup, getStarterSetup } from "@/lib/onboarding"; -import { checkIsLocalServer } from "@/lib/server"; -import { - Collapsible, - CollapsibleContent, - CollapsibleTrigger, - RadialProgress, - Skeleton, - cn -} from "@zenml-io/react-component-library"; -import { useState } from "react"; -import { CreateNewStack, RunNewPipeline } from "./Items"; - -export function ProductionSetupChecklist() { - const onboarding = useOnboarding({ refetchInterval: 5000 }); - const serverInfo = useServerInfo(); - const [open, setOpen] = useState(true); - - if (onboarding.isPending || serverInfo.isPending) - return ; - if (onboarding.isError || serverInfo.isError) return null; - - const starterSetup = getStarterSetup( - onboarding.data, - checkIsLocalServer(serverInfo.data.deployment_type || "other") - ); - - const { progress, totalItems, itemsDone, getItem } = getProductionSetup(onboarding.data); - - const stackStep = getItem("stack_with_remote_orchestrator_created"); - const pipelineStep = getItem("pipeline_run_with_remote_orchestrator"); - - return ( - <> - - -
- {progress >= 100 ? ( - - ) : ( - - - {itemsDone}/{totalItems} - - - )} - -
-

- Production Setup{" "} - (10 min) -

-

- {starterSetup.isFinished ? ( - "Level up your skills in a production setting." - ) : ( - - Complete the Starter Setup to enable the production setup. - - )} -

-
-
- -
- -
    -
  • - -
  • -
  • - -
  • -
-
-
- - ); -} diff --git a/src/app/onboarding/ProgressIndicator.tsx b/src/app/onboarding/ProgressIndicator.tsx new file mode 100644 index 000000000..a9964cfe8 --- /dev/null +++ b/src/app/onboarding/ProgressIndicator.tsx @@ -0,0 +1,26 @@ +import { useServerInfo } from "@/data/server/info-query"; +import { useOnboarding } from "@/data/server/onboarding-state"; +import { getOnboardingSetup } from "@/lib/onboarding"; +import { checkIsLocalServer } from "@/lib/server"; +import { Badge, Skeleton } from "@zenml-io/react-component-library"; + +export function ProgressIndicatior() { + const onboarding = useOnboarding(); + const serverInfo = useServerInfo(); + + if (onboarding.isPending || serverInfo.isPending) + return ; + if (onboarding.isError || serverInfo.isError) { + return null; + } + const isLocalServer = checkIsLocalServer(serverInfo.data.deployment_type || "other"); + const onboardingSetup = getOnboardingSetup(onboarding.data, isLocalServer); + const completedItems = onboardingSetup.itemsDone; + const totalItems = onboardingSetup.totalItems; + + return ( + + {completedItems}/{totalItems} steps completed + + ); +} diff --git a/src/app/onboarding/StarterSetup/Items.tsx b/src/app/onboarding/Setup/Items.tsx similarity index 58% rename from src/app/onboarding/StarterSetup/Items.tsx rename to src/app/onboarding/Setup/Items.tsx index cee207441..214bae6c1 100644 --- a/src/app/onboarding/StarterSetup/Items.tsx +++ b/src/app/onboarding/Setup/Items.tsx @@ -1,10 +1,13 @@ import Help from "@/assets/icons/help.svg?react"; +import Plus from "@/assets/icons/plus.svg?react"; import { Codesnippet } from "@/components/CodeSnippet"; import { HelpBox } from "@/components/fallback-pages/Helpbox"; import { ChecklistItem } from "@/components/onboarding/ChecklistItem"; import { useServerInfo } from "@/data/server/info-query"; +import { routes } from "@/router/routes"; import { OnboardingStep } from "@/types/onboarding"; -import { Box, Skeleton, buttonVariants } from "@zenml-io/react-component-library"; +import { Box, Button, Skeleton, buttonVariants } from "@zenml-io/react-component-library"; +import { Link } from "react-router-dom"; export function ConnectZenMLStep({ completed, hasDownstreamStep, active }: OnboardingStep) { const { data } = useServerInfo({ throwOnError: true }); @@ -13,7 +16,7 @@ export function ConnectZenMLStep({ completed, hasDownstreamStep, active }: Onboa active={active} hasDownstream={hasDownstreamStep} completed={completed} - title="Connect to ZenML" + title="Install and Connect ZenML (5 min)" >
@@ -38,7 +41,7 @@ export function RunFirstPipeline({ active, completed, hasDownstreamStep }: Onboa active={active} hasDownstream={hasDownstreamStep} completed={completed} - title="Run your first pipeline" + title="Run a pipeline (2 min)" >
@@ -98,3 +101,71 @@ export function RunFirstPipeline({ active, completed, hasDownstreamStep }: Onboa ); } + +export function CreateNewStack({ completed, active, hasDownstreamStep }: OnboardingStep) { + const link = + routes.stacks.create.index + "?" + new URLSearchParams({ origin: "onboarding" }).toString(); + return ( + +

+ A stack configures how a pipeline is executed{" "} + +

+
+
+

Connect your Cloud to deploy your ZenML pipelines in a remote stack.

+ +
+ +
+
+ ); +} + +export function RunNewPipeline({ active, completed, hasDownstreamStep }: OnboardingStep) { + return ( + +
+
+

Set the new stack

+ +
+
+

Run the pipeline

+ +
+
+ +
+
+
+ ); +} + +function LearnMoreLink({ href }: { href: string }) { + return ( + + Learn more + + ); +} diff --git a/src/app/onboarding/Setup/index.tsx b/src/app/onboarding/Setup/index.tsx new file mode 100644 index 000000000..9a3d02422 --- /dev/null +++ b/src/app/onboarding/Setup/index.tsx @@ -0,0 +1,57 @@ +import { useServerInfo } from "@/data/server/info-query"; +import { useOnboarding } from "@/data/server/onboarding-state"; +import { getOnboardingSetup } from "@/lib/onboarding"; +import { checkIsLocalServer } from "@/lib/server"; +import { Skeleton } from "@zenml-io/react-component-library"; +import { ConnectZenMLStep, CreateNewStack, RunFirstPipeline, RunNewPipeline } from "./Items"; + +export function OnboardingSetupList() { + const onboarding = useOnboarding({ refetchInterval: 5000 }); + const serverInfo = useServerInfo(); + + if (onboarding.isPending || serverInfo.isPending) + return ; + if (onboarding.isError || serverInfo.isError) return null; + + const isLocalServer = checkIsLocalServer(serverInfo.data.deployment_type || "other"); + const { getItem } = getOnboardingSetup(onboarding.data, isLocalServer); + const connectStep = getItem("device_verified"); + const pipelineStep = getItem("pipeline_run"); + const stackStep = getItem("stack_with_remote_orchestrator_created"); + const remotePipelineStep = getItem("pipeline_run_with_remote_orchestrator"); + + return ( +
    + {!isLocalServer && ( +
  • + +
  • + )} +
  • + +
  • +
  • + +
  • +
  • + +
  • +
+ ); +} diff --git a/src/app/onboarding/StarterSetup/index.tsx b/src/app/onboarding/StarterSetup/index.tsx deleted file mode 100644 index 94a143a4d..000000000 --- a/src/app/onboarding/StarterSetup/index.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import ChevronDown from "@/assets/icons/chevron-down.svg?react"; -import { Tick } from "@/components/Tick"; -import { useServerInfo } from "@/data/server/info-query"; -import { useOnboarding } from "@/data/server/onboarding-state"; -import { getStarterSetup } from "@/lib/onboarding"; -import { checkIsLocalServer } from "@/lib/server"; -import { - Collapsible, - CollapsibleContent, - CollapsibleTrigger, - RadialProgress, - Skeleton -} from "@zenml-io/react-component-library"; -import { useState } from "react"; -import { ConnectZenMLStep, RunFirstPipeline } from "./Items"; - -export function StarterSetupList() { - const onboarding = useOnboarding({ refetchInterval: 5000 }); - const serverInfo = useServerInfo(); - const [open, setOpen] = useState(true); - - if (onboarding.isPending || serverInfo.isPending) - return ; - if (onboarding.isError || serverInfo.isError) return null; - - const isLocalServer = checkIsLocalServer(serverInfo.data.deployment_type || "other"); - const { progress, itemsDone, totalItems, getItem } = getStarterSetup( - onboarding.data, - isLocalServer - ); - const connectStep = getItem("device_verified"); - const pipelineStep = getItem("pipeline_run"); - - return ( - - -
- {progress >= 100 ? ( - - ) : ( - - - {itemsDone}/{totalItems} - - - )} - -
-

- Starter Setup{" "} - (2 min) -

-

- Follow these steps to make sure your client is connected and run your first MLOps - pipeline with ZenML -

-
-
- -
- -
    - {!isLocalServer && ( -
  • - -
  • - )} -
  • - -
  • -
-
-
- ); -} diff --git a/src/app/onboarding/page.tsx b/src/app/onboarding/page.tsx index 2b99ab0a4..beada81fe 100644 --- a/src/app/onboarding/page.tsx +++ b/src/app/onboarding/page.tsx @@ -1,7 +1,6 @@ -import { FallbackSupportCard, ResourcesCard } from "@/components/fallback-pages/Cards"; +import { SupportCard, ResourcesCard } from "@/components/fallback-pages/Cards"; import { HeaderOnboardingBox } from "./Header"; -import { ProductionSetupChecklist } from "./ProductionSetup"; -import { StarterSetupList } from "./StarterSetup"; +import { OnboardingSetupList } from "./Setup"; import { useTourContext } from "@/components/tour/TourContext"; import { useEffect } from "react"; @@ -21,11 +20,10 @@ export default function OnboardingPage() {
- - +
- +
diff --git a/src/app/pipelines/[namespace]/RunsTable.tsx b/src/app/pipelines/[namespace]/RunsTable.tsx index 0b467b315..58c357f7f 100644 --- a/src/app/pipelines/[namespace]/RunsTable.tsx +++ b/src/app/pipelines/[namespace]/RunsTable.tsx @@ -6,11 +6,14 @@ import { useAllPipelineRuns } from "@/data/pipeline-runs/all-pipeline-runs-query import { SearchField } from "@/components/SearchField"; import Pagination from "@/components/Pagination"; import { getPipelineDetailColumns } from "./columns"; +import { useRunsSelectorContext } from "../RunsTab/RunsSelectorContext"; +import { RunsButtonGroup } from "../RunsTab/ButtonGroup"; export function PipelineRunsTable() { const { namespace } = useParams() as { namespace: string }; const params = usePipelineRunParams(); const cols = getPipelineDetailColumns(); + const { selectedRuns } = useRunsSelectorContext(); const { data, refetch } = useAllPipelineRuns( { @@ -26,7 +29,7 @@ export function PipelineRunsTable() { return (
- + {selectedRuns.length ? : }
); } diff --git a/src/app/runs/[id]/page.tsx b/src/app/runs/[id]/page.tsx index 7aabb8d3b..7e18fb9c9 100644 --- a/src/app/runs/[id]/page.tsx +++ b/src/app/runs/[id]/page.tsx @@ -1,7 +1,7 @@ import Expand from "@/assets/icons/expand.svg?react"; import { useServerInfo } from "@/data/server/info-query"; import { checkIsLocalServer } from "@/lib/server"; -import { Button } from "@zenml-io/react-component-library"; +import { Button, ScrollArea } from "@zenml-io/react-component-library"; import { Dispatch, SetStateAction, useState } from "react"; import { DAG } from "./Dag"; import { RunsDetailHeader } from "./Header"; @@ -28,13 +28,13 @@ export default function RunDetailPage() {
- - + + + +
diff --git a/src/assets/icons/message-chat-square.svg b/src/assets/icons/message-chat-square.svg new file mode 100644 index 000000000..2f3531457 --- /dev/null +++ b/src/assets/icons/message-chat-square.svg @@ -0,0 +1,4 @@ + + + diff --git a/src/components/artifacts/artifact-node-sheet/SheetContent.tsx b/src/components/artifacts/artifact-node-sheet/SheetContent.tsx index b361af190..eebf3ac99 100644 --- a/src/components/artifacts/artifact-node-sheet/SheetContent.tsx +++ b/src/components/artifacts/artifact-node-sheet/SheetContent.tsx @@ -13,10 +13,11 @@ import { TabsList, TabsTrigger } from "@zenml-io/react-component-library"; +import { ErrorBoundary } from "react-error-boundary"; import { ArtifactIcon } from "../../ArtifactIcon"; import { ArtifactDetailTab } from "./DetailsTab"; import { ArtifactMetadataTab } from "./MetadataTab"; -import { VisualizationTab } from "./VisualizationTab"; +import { VisualizationErrorFallback, VisualizationTab } from "./VisualizationTab"; type Props = { artifactVersionId: string; @@ -85,7 +86,9 @@ export function ArtifactSheetContent({ artifactVersionId }: Props) { - + + + diff --git a/src/components/artifacts/artifact-node-sheet/VisualizationTab.tsx b/src/components/artifacts/artifact-node-sheet/VisualizationTab.tsx index 1599829c0..6cdd62548 100644 --- a/src/components/artifacts/artifact-node-sheet/VisualizationTab.tsx +++ b/src/components/artifacts/artifact-node-sheet/VisualizationTab.tsx @@ -1,12 +1,13 @@ // import { Visualization } from "@/components/tenants-dashboard/artifacts/Visualization"; -import { Button, Skeleton } from "@zenml-io/react-component-library"; -import { useArtifactVersion } from "@/data/artifact-versions/artifact-version-detail-query"; +import { default as Barchart, default as BarChart } from "@/assets/icons/bar-chart.svg?react"; import { EmptyState } from "@/components/EmptyState"; -import BarChart from "@/assets/icons/bar-chart.svg?react"; -import { Visualization } from "../Visualization"; +import { useArtifactVersion } from "@/data/artifact-versions/artifact-version-detail-query"; +import { FIVEMEGABYTES } from "@/lib/constants"; import { MetadataMap } from "@/types/common"; +import { Button, Skeleton } from "@zenml-io/react-component-library"; import { useState } from "react"; -import { FIVEMEGABYTES } from "@/lib/constants"; +import { FallbackProps } from "react-error-boundary"; +import { Visualization } from "../Visualization"; type Props = { artifactVersionId: string; @@ -65,3 +66,14 @@ export function VisualizationTab({ artifactVersionId }: Props) {
); } + +export function VisualizationErrorFallback({ error }: FallbackProps) { + return ( + }> +
+

Something went wrong

+

{error.message}

+
+
+ ); +} diff --git a/src/components/fallback-pages/Cards.tsx b/src/components/fallback-pages/Cards.tsx index e13ae768b..fcf2d688d 100644 --- a/src/components/fallback-pages/Cards.tsx +++ b/src/components/fallback-pages/Cards.tsx @@ -1,111 +1,96 @@ -import { Box } from "@zenml-io/react-component-library"; -import { ReactNode } from "react"; -import ZenMLLogo from "@/assets/icons/zenml-icon.svg?react"; -import Slack from "@/assets/icons/services/slack.svg?react"; -import Mail from "@/assets/icons/mail.svg?react"; -import FileQuestion from "@/assets/icons/file-question.svg?react"; import FileText from "@/assets/icons/file-text.svg?react"; import File from "@/assets/icons/file.svg?react"; -import Terminal from "@/assets/icons/terminal.svg?react"; +import Mail from "@/assets/icons/mail.svg?react"; +import Chat from "@/assets/icons/message-chat-square.svg?react"; import Package from "@/assets/icons/package.svg?react"; +import Terminal from "@/assets/icons/terminal.svg?react"; +import { Button } from "@zenml-io/react-component-library"; -export function FallbackSupportCard() { - const links: CardLinkProps[] = [ - { - text: "Connect with Slack", - href: "https://zenml.io/slack", - icon: - }, +export function SupportCard() { + const links = [ { text: "Send us a message", href: "mailto:cloud@zenml.io", - icon: + icon: + }, + + { + text: "Join our Slack Community", + href: "https://zenml.io/slack", + icon: } ]; return ( - -
- -
-
-

Need support?

-

- Schedule a call with us, connect with our Slack community or send us a message. -

-
    - {links.map((link, i) => ( -
  • - -
  • - ))} -
-
-
+
+

Support

+ +
); } - export function ResourcesCard() { - const links: CardLinkProps[] = [ + const links = [ { text: "Browse our docs", href: "https://docs.zenml.io", - icon: + icon: }, { text: "Discover projects", href: "https://github.com/zenml-io/zenml-projects", - icon: + icon: }, { text: "Navigate our examples", href: "https://github.com/zenml-io/zenml/tree/main/examples/", - icon: + icon: }, { text: "Read our blog", href: "https://zenml.io/blog", - icon: + icon: } ]; return ( - -
- -
-
-

Resources

-

- Need help? Follow our extensive documentation to get started. -

-
    - {links.map((link, i) => ( -
  • - -
  • - ))} -
-
-
- ); -} +
+

Resources

-type CardLinkProps = { - href: string; - text: ReactNode; - icon?: ReactNode; -}; -function CardLink({ href, text, icon }: CardLinkProps) { - return ( - - {icon && icon} - {text} - + +
); } diff --git a/src/components/onboarding/ChecklistItem.tsx b/src/components/onboarding/ChecklistItem.tsx index e6afefeec..d9a3d5727 100644 --- a/src/components/onboarding/ChecklistItem.tsx +++ b/src/components/onboarding/ChecklistItem.tsx @@ -1,12 +1,12 @@ -import ChevronDown from "@/assets/icons/chevron-down.svg?react"; import { - Collapsible, CollapsibleContent, + CollapsiblePanel, CollapsibleTrigger, ProgressOutstanding, cn } from "@zenml-io/react-component-library"; -import { PropsWithChildren, ReactNode, useState } from "react"; +import ChevronDown from "@/assets/icons/chevron-down.svg?react"; +import { PropsWithChildren, ReactNode, useEffect, useState } from "react"; import { Tick } from "../Tick"; import { SkippedStep } from "./SkippedStep"; @@ -21,13 +21,21 @@ export function ChecklistItem({ title, children, hasDownstream, - active + active = false }: PropsWithChildren) { const [open, setOpen] = useState(active); + useEffect(() => { + setOpen(active); + }, [active]); + return ( - -
+ +
{completed ? ( @@ -37,7 +45,12 @@ export function ChecklistItem({ )} - +
@@ -49,16 +62,17 @@ export function ChecklistItem({
- {children && ( - -
-
-
{children}
-
- - )}
- + + {children && ( + +
+
+
{children}
+
+ + )} + ); } @@ -66,17 +80,19 @@ type HeaderProps = { completed: boolean; title: ReactNode; skipped: boolean; + active?: boolean; }; -export function ChecklistHeader({ completed, title, skipped }: HeaderProps) { +export function ChecklistHeader({ completed, title, skipped, active }: HeaderProps) { return (
{title}
diff --git a/src/data/api.ts b/src/data/api.ts index 303e6e99d..caf6cc2ee 100644 --- a/src/data/api.ts +++ b/src/data/api.ts @@ -24,8 +24,7 @@ export const apiPaths = { }, runs: { all: "/runs", - detail: (id: string) => `/runs/${id}`, - graph: (runId: string) => `/runs/${runId}/graph` + detail: (id: string) => `/runs/${id}` }, pipeline_builds: { all: "/pipeline_builds", diff --git a/src/layouts/AuthenticatedLayout/OnboardingItem.tsx b/src/layouts/AuthenticatedLayout/OnboardingItem.tsx index 325d125e3..815874b61 100644 --- a/src/layouts/AuthenticatedLayout/OnboardingItem.tsx +++ b/src/layouts/AuthenticatedLayout/OnboardingItem.tsx @@ -1,7 +1,7 @@ import ChevronRight from "@/assets/icons/chevron-right.svg?react"; import { useServerInfo } from "@/data/server/info-query"; import { useOnboarding } from "@/data/server/onboarding-state"; -import { getProductionSetup, getStarterSetup } from "@/lib/onboarding"; +import { getOnboardingSetup } from "@/lib/onboarding"; import { checkIsLocalServer } from "@/lib/server"; import { routes } from "@/router/routes"; import { Box, ProgressBar, Skeleton, useSidebarContext } from "@zenml-io/react-component-library"; @@ -20,20 +20,17 @@ export function OnboardingItem() { ); } - const starterSetup = getStarterSetup( + const onboardingSetup = getOnboardingSetup( onboarding.data, checkIsLocalServer(serverInfo.data.deployment_type || "other") ); - const productionSetup = getProductionSetup(onboarding.data); - const title = starterSetup.isFinished ? "Production Setup" : "Starter Setup"; - const completedItems = starterSetup.isFinished - ? productionSetup.itemsDone - : starterSetup.itemsDone; - const totalItems = starterSetup.isFinished ? productionSetup.totalItems : starterSetup.totalItems; - const progress = starterSetup.isFinished ? productionSetup.progress : starterSetup.progress; + const title = "Quick Setup"; + const completedItems = onboardingSetup.itemsDone; + const totalItems = onboardingSetup.totalItems; + const progress = onboardingSetup.progress; - if (starterSetup.isFinished && productionSetup.isFinished) return null; + if (onboardingSetup.isFinished) return null; return (
  • diff --git a/src/lib/onboarding.spec.ts b/src/lib/onboarding.spec.ts index 975270076..0e94724b1 100644 --- a/src/lib/onboarding.spec.ts +++ b/src/lib/onboarding.spec.ts @@ -1,55 +1,52 @@ import { OnboardingChecklistItemName, OnboardingResponse } from "@/types/onboarding"; import { describe, expect, test } from "vitest"; -import { - checkDownstreamStep, - getProgress, - getStarterSetupItems, - getOnboardingLength -} from "./onboarding"; +import { checkDownstreamStep, getProgress, getSetupItems, getOnboardingLength } from "./onboarding"; -describe("returns the correct items for the starter setup based on the deployment type", () => { +describe("returns the correct items based on the deployment type", () => { test("doesnt return connect step for local deployment", () => { const isLocal = true; - const items = getStarterSetupItems(isLocal); - expect(items).toEqual(["pipeline_run"]); + const items = getSetupItems(isLocal); + expect(items).toEqual([ + "pipeline_run", + "stack_with_remote_orchestrator_created", + "pipeline_run_with_remote_orchestrator" + ]); }); test("includes the connect step for non-local deployments", () => { const isLocal = false; - const items = getStarterSetupItems(isLocal); - expect(items).toEqual(["device_verified", "pipeline_run"]); + const items = getSetupItems(isLocal); + expect(items).toEqual([ + "device_verified", + "pipeline_run", + "stack_with_remote_orchestrator_created", + "pipeline_run_with_remote_orchestrator" + ]); }); }); describe("returns the correct progress, depending on the current state", () => { test("returns 2 if the first two steps are done", () => { const state: OnboardingResponse = ["device_verified", "pipeline_run"]; - const flow = "starter"; - const progress = getProgress(state, flow, false); + const progress = getProgress(state, false); expect(progress).toBe(2); }); test("returns 0 if no steps are done", () => { const onboardingState: OnboardingChecklistItemName[] = []; - const progress = getProgress(onboardingState, "starter", false); + const progress = getProgress(onboardingState, false); expect(progress).toBe(0); }); test("returns 2 if only the second step is run", () => { const onboardingState: OnboardingChecklistItemName[] = ["pipeline_run"]; - const progress = getProgress(onboardingState, "starter", false); - expect(progress).toBe(2); - }); - - test("returns 2 if only the finalStep is there", () => { - const onboardingState: OnboardingChecklistItemName[] = ["starter_setup_completed"]; - const progress = getProgress(onboardingState, "starter", false); + const progress = getProgress(onboardingState, false); expect(progress).toBe(2); }); test("returns correct value if flow is local", () => { const onboardingState: OnboardingChecklistItemName[] = ["device_verified"]; - const progress = getProgress(onboardingState, "starter", true); + const progress = getProgress(onboardingState, true); expect(progress).toBe(0); }); }); @@ -57,37 +54,28 @@ describe("returns the correct progress, depending on the current state", () => { describe("checks if the item has downstream items", () => { test("returns true if a downstream step is there", () => { const itemName = "device_verified"; - const onboardingState: OnboardingChecklistItemName[] = [ - "device_verified", - "pipeline_run", - "starter_setup_completed" - ]; - const hasDownStreamFinishded = checkDownstreamStep(itemName, onboardingState, "starter", false); + const onboardingState: OnboardingChecklistItemName[] = ["device_verified", "pipeline_run"]; + const hasDownStreamFinishded = checkDownstreamStep(itemName, onboardingState, false); expect(hasDownStreamFinishded).toBe(true); }); test("returns false if there is no downstream step", () => { const itemName = "pipeline_run"; const onboardingState: OnboardingChecklistItemName[] = ["pipeline_run"]; - const hasDownStreamFinishded = checkDownstreamStep(itemName, onboardingState, "starter", false); + const hasDownStreamFinishded = checkDownstreamStep(itemName, onboardingState, false); expect(hasDownStreamFinishded).toBe(false); }); test("returns correct value if starter setup is local", () => { const itemName = "device_verified"; const onboardingState: OnboardingChecklistItemName[] = ["device_verified", "pipeline_run"]; - const hasDownStreamFinishded = checkDownstreamStep(itemName, onboardingState, "starter", true); + const hasDownStreamFinishded = checkDownstreamStep(itemName, onboardingState, true); expect(hasDownStreamFinishded).toBe(false); }); test("only final step is there", () => { const itemName: OnboardingChecklistItemName = "stack_with_remote_orchestrator_created"; const onboarding_state: OnboardingChecklistItemName[] = ["production_setup_completed"]; - const hasDownStreamFinished = checkDownstreamStep( - itemName, - onboarding_state, - "production", - false - ); + const hasDownStreamFinished = checkDownstreamStep(itemName, onboarding_state, false); expect(hasDownStreamFinished).toBe(true); }); }); @@ -95,12 +83,12 @@ describe("checks if the item has downstream items", () => { describe("checks if the correct length is returned", () => { test("returns correct value for local starter setup", () => { const isLocal = true; - const items = getOnboardingLength("starter", isLocal); - expect(items).toBe(1); + const items = getOnboardingLength(isLocal); + expect(items).toBe(3); }); test("returns correct value for non-local starter setup", () => { const isLocal = false; - const items = getOnboardingLength("starter", isLocal); - expect(items).toBe(2); + const items = getOnboardingLength(isLocal); + expect(items).toBe(4); }); }); diff --git a/src/lib/onboarding.ts b/src/lib/onboarding.ts index 778f479fe..658c50215 100644 --- a/src/lib/onboarding.ts +++ b/src/lib/onboarding.ts @@ -1,67 +1,36 @@ import { OnboardingChecklistItemName, OnboardingResponse } from "@/types/onboarding"; -export type Flow = "starter" | "production"; - -export function getStarterSetupItems(isLocal: boolean): OnboardingChecklistItemName[] { - return [...(isLocal ? [] : ["device_verified" as OnboardingChecklistItemName]), "pipeline_run"]; -} - -function getProductionSetupItems(): OnboardingChecklistItemName[] { - return ["stack_with_remote_orchestrator_created", "pipeline_run_with_remote_orchestrator"]; +export function getSetupItems(isLocal: boolean): OnboardingChecklistItemName[] { + return [ + ...(isLocal ? [] : ["device_verified" as OnboardingChecklistItemName]), + "pipeline_run", + "stack_with_remote_orchestrator_created", + "pipeline_run_with_remote_orchestrator" + ]; } -const finalSteps: { - starter: OnboardingChecklistItemName; - production: OnboardingChecklistItemName; -} = { - starter: "starter_setup_completed", - production: "production_setup_completed" -}; - -export function getStarterSetup(data: OnboardingResponse, isLocal: boolean) { - const flowType: Flow = "starter"; - const finalStep = finalSteps[flowType]; - const itemsDone = getProgress(data, flowType, isLocal); - const totalItems = getOnboardingLength(flowType, isLocal); - return { - itemsDone, - totalItems, - items: getStarterSetupItems(isLocal), - isFinished: data.includes(finalStep), - progress: (itemsDone / totalItems) * 100, - finalStep: finalSteps.starter, - hasItem: (item: OnboardingChecklistItemName) => hasOnboardingItem(item, data), - getItem: (item: OnboardingChecklistItemName) => getItem(item, data, flowType) - }; -} +const finalStep: OnboardingChecklistItemName = "production_setup_completed"; -export function getProductionSetup(data: OnboardingResponse) { - const flowType: Flow = "production"; - const finalStep = finalSteps[flowType]; - const itemsDone = getProgress(data, flowType); - const totalItems = getOnboardingLength(flowType); +export function getOnboardingSetup(data: OnboardingResponse, isLocal: boolean) { + const itemsDone = getProgress(data, isLocal); + const totalItems = getOnboardingLength(isLocal); return { itemsDone, totalItems, - items: getProductionSetupItems(), + items: getSetupItems(isLocal), isFinished: data.includes(finalStep), progress: (itemsDone / totalItems) * 100, - finalStep: finalSteps.starter, + finalStep: finalStep, hasItem: (item: OnboardingChecklistItemName) => hasOnboardingItem(item, data), - getItem: (item: OnboardingChecklistItemName) => getItem(item, data, flowType) + getItem: (item: OnboardingChecklistItemName) => getItem(item, data) }; } -function getItem( - item: OnboardingChecklistItemName, - state: OnboardingResponse, - flow: Flow, - isLocal?: boolean -) { +function getItem(item: OnboardingChecklistItemName, state: OnboardingResponse, isLocal?: boolean) { return { isCompleted: state.includes(item), - hasDownStreamStep: checkDownstreamStep(item, state, flow, isLocal || false), - isActive: isStepActive(item, state, flow, isLocal || false) + hasDownStreamStep: checkDownstreamStep(item, state, isLocal || false), + isActive: isStepActive(item, state, isLocal || false) }; } @@ -72,12 +41,10 @@ function hasOnboardingItem(item: OnboardingChecklistItemName, state: OnboardingR export function checkDownstreamStep( item: OnboardingChecklistItemName, state: OnboardingResponse, - flow: Flow, - isLocal?: boolean + isLocal: boolean ) { - const order = - flow === "starter" ? getStarterSetupItems(isLocal || false) : getProductionSetupItems(); - const withFinalStep = [...order, finalSteps[flow]]; + const order = getSetupItems(isLocal); + const withFinalStep = [...order, finalStep]; const currentIndex = withFinalStep.indexOf(item); if (currentIndex === -1) { return false; // If the item is not found in the order array, return false @@ -89,10 +56,9 @@ export function checkDownstreamStep( function isStepActive( step: OnboardingChecklistItemName, state: OnboardingResponse, - flowType: Flow, isLocal: boolean ): boolean { - const flow = flowType === "starter" ? getStarterSetupItems(isLocal) : getProductionSetupItems(); + const flow = getSetupItems(isLocal); if (flow.length === 0) { return false; } @@ -119,10 +85,9 @@ function isStepActive( return state.includes(previousStep); } -export function getProgress(onboardingState: OnboardingResponse, flow: Flow, isLocal?: boolean) { - const items = - flow === "starter" ? getStarterSetupItems(isLocal || false) : getProductionSetupItems(); - const finalStep = finalSteps[flow]; +export function getProgress(onboardingState: OnboardingResponse, isLocal: boolean) { + const items = getSetupItems(isLocal); + // Filter out the finalStep from the checklist items const filteredItems = items.filter((item) => item !== finalStep); @@ -143,8 +108,7 @@ export function getProgress(onboardingState: OnboardingResponse, flow: Flow, isL return highestIndex + 1; } -export function getOnboardingLength(flow: Flow, isLocal?: boolean) { - const items = - flow === "starter" ? getStarterSetupItems(isLocal || false) : getProductionSetupItems(); +export function getOnboardingLength(isLocal: boolean) { + const items = getSetupItems(isLocal); return items.length; } diff --git a/src/types/onboarding.ts b/src/types/onboarding.ts index e90d1c9c7..d7b2777f2 100644 --- a/src/types/onboarding.ts +++ b/src/types/onboarding.ts @@ -1,7 +1,6 @@ export type OnboardingChecklistItemName = | "device_verified" | "pipeline_run" - | "starter_setup_completed" | "stack_with_remote_orchestrator_created" | "pipeline_run_with_remote_orchestrator" | "production_setup_completed";