diff --git a/src/app/onboarding/ProductionSetup/Items.tsx b/src/app/onboarding/ProductionSetup/Items.tsx index bffb02d35..652424742 100644 --- a/src/app/onboarding/ProductionSetup/Items.tsx +++ b/src/app/onboarding/ProductionSetup/Items.tsx @@ -1,106 +1,20 @@ -import { ChecklistItem } from "@/components/onboarding/ChecklistItem"; -import { CloudProvider, ProviderSelect } from "./ProviderSelect"; -import { HelpBox } from "@/components/fallback-pages/Helpbox"; -import { useState } from "react"; -import { getOnboardingItem } from "@/lib/onboarding"; -import { OnboardingChecklistItemName, OnboardingState } from "@/types/onboarding"; -import { getServiceConnectorStep } from "./ConnectorContent"; -import { getArtifactStoreStep } from "./ArtifactStore"; +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"; -type Props = { - onboardingState?: OnboardingState; - active?: boolean; -}; - -export function CreateServiceConnector({ onboardingState, active }: Props) { - const [selectedProvider, setSelectedProvider] = useState("aws"); - const itemName: OnboardingChecklistItemName = "create_service_connector"; - const item = getOnboardingItem(onboardingState || {}, itemName); - return ( - -

- A service connector grants users of your ZenML server the ability to access components like - your artifact store{" "} - -

-
-
- - -
- {getServiceConnectorStep(selectedProvider)} -
- -
-
-
- ); -} - -export function CreateArtifactStore({ onboardingState, active }: Props) { - const [selectedProvider, setSelectedProvider] = useState("aws"); - const itemName: OnboardingChecklistItemName = "create_remote_artifact_store"; - const item = getOnboardingItem(onboardingState || {}, itemName); - - return ( - -

- Configuring a remote artifact store will version your pipeline's data directly in your cloud - provider{" "} - -

-
-
- - -
- {getArtifactStoreStep(selectedProvider)} -
- -
-
-
- ); -} - -export function CreateNewStack({ onboardingState, active }: Props) { - const itemName: OnboardingChecklistItemName = "create_remote_stack"; - const item = getOnboardingItem(onboardingState || {}, itemName); - +export function CreateNewStack({ completed, active, hasDownstreamStep }: OnboardingStep) { + const link = + routes.stacks.create.index + "?" + new URLSearchParams({ origin: "onboarding" }).toString(); return ( @@ -109,14 +23,14 @@ export function CreateNewStack({ onboardingState, active }: Props) {

-
-

- Download the quickstart example to your local machine -

- +
+

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

+
@@ -124,25 +38,18 @@ export function CreateNewStack({ onboardingState, active }: Props) { ); } -export function RunNewPipeline({ active, onboardingState }: Props) { - const itemName: OnboardingChecklistItemName = "run_remote_pipeline"; - const item = getOnboardingItem(onboardingState || {}, itemName); - +export function RunNewPipeline({ active, completed, hasDownstreamStep }: OnboardingStep) { return (

Set the new stack

- +

Run the pipeline

diff --git a/src/app/onboarding/ProductionSetup/index.tsx b/src/app/onboarding/ProductionSetup/index.tsx index 8c712a00f..3d8c60deb 100644 --- a/src/app/onboarding/ProductionSetup/index.tsx +++ b/src/app/onboarding/ProductionSetup/index.tsx @@ -1,9 +1,8 @@ import ChevronDown from "@/assets/icons/chevron-down.svg?react"; import { Tick } from "@/components/Tick"; -import { useServerSettings } from "@/data/server/get-server-settings"; import { useServerInfo } from "@/data/server/info-query"; -import { PRODUCTION_SETUP_ITEMS } from "@/lib/constants"; -import { getOnboardingState, getProgress, getStarterSetupItems } from "@/lib/onboarding"; +import { useOnboarding } from "@/data/server/onboarding-state"; +import { getProductionSetup, getStarterSetup } from "@/lib/onboarding"; import { checkIsLocalServer } from "@/lib/server"; import { Collapsible, @@ -14,30 +13,26 @@ import { cn } from "@zenml-io/react-component-library"; import { useState } from "react"; -import { - CreateArtifactStore, - CreateNewStack, - CreateServiceConnector, - RunNewPipeline -} from "./Items"; +import { CreateNewStack, RunNewPipeline } from "./Items"; export function ProductionSetupChecklist() { - const { isError, isPending, data } = useServerSettings({ throwOnError: true }); + const onboarding = useOnboarding({ refetchInterval: 5000 }); const serverInfo = useServerInfo(); const [open, setOpen] = useState(true); - if (isPending || serverInfo.isPending) return ; - if (isError || serverInfo.isError) return null; + if (onboarding.isPending || serverInfo.isPending) + return ; + if (onboarding.isError || serverInfo.isError) return null; - const STARTER_SETUP_ITEMS = getStarterSetupItems( + const starterSetup = getStarterSetup( + onboarding.data, checkIsLocalServer(serverInfo.data.deployment_type || "other") ); - const onboardingState = getOnboardingState(data); - const isStarterSetupFinished = - getProgress(onboardingState, STARTER_SETUP_ITEMS) === STARTER_SETUP_ITEMS.length; - const doneItems = getProgress(onboardingState, PRODUCTION_SETUP_ITEMS); - const progress = (doneItems / PRODUCTION_SETUP_ITEMS.length) * 100; + 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 ( <> @@ -53,7 +48,7 @@ export function ProductionSetupChecklist() { ) : ( - {doneItems}/{PRODUCTION_SETUP_ITEMS.length} + {itemsDone}/{totalItems} )} @@ -64,7 +59,7 @@ export function ProductionSetupChecklist() { (10 min)

- {isStarterSetupFinished ? ( + {starterSetup.isFinished ? ( "Level up your skills in a production setting." ) : ( @@ -83,23 +78,19 @@ export function ProductionSetupChecklist() {

  • -
  • -
  • -
  • - -
  • -
  • - -
diff --git a/src/app/onboarding/StarterSetup/Items.tsx b/src/app/onboarding/StarterSetup/Items.tsx index 8ec294c8f..76b692898 100644 --- a/src/app/onboarding/StarterSetup/Items.tsx +++ b/src/app/onboarding/StarterSetup/Items.tsx @@ -1,22 +1,20 @@ -import { ChecklistItem } from "@/components/onboarding/ChecklistItem"; +import Help from "@/assets/icons/help.svg?react"; import { Codesnippet } from "@/components/CodeSnippet"; import { HelpBox } from "@/components/fallback-pages/Helpbox"; -import { Box, Skeleton, buttonVariants } from "@zenml-io/react-component-library"; -import Help from "@/assets/icons/help.svg?react"; -import { OnboardingChecklistItemName, OnboardingState } from "@/types/onboarding"; -import { getOnboardingItem } from "@/lib/onboarding"; +import { ChecklistItem } from "@/components/onboarding/ChecklistItem"; import { useServerInfo } from "@/data/server/info-query"; +import { OnboardingStep } from "@/types/onboarding"; +import { Box, Skeleton, buttonVariants } from "@zenml-io/react-component-library"; -type Props = { - onboardingState?: OnboardingState; -}; -export function ConnectZenMLStep({ onboardingState }: Props) { +export function ConnectZenMLStep({ completed, hasDownstreamStep, active }: OnboardingStep) { const { data } = useServerInfo({ throwOnError: true }); - - const itemName = "connect_zenml"; - const item = getOnboardingItem(onboardingState || {}, itemName); return ( - +

Install ZenML

@@ -34,11 +32,14 @@ export function ConnectZenMLStep({ onboardingState }: Props) { ); } -export function RunFirstPipeline({ onboardingState }: Props) { - const itemName: OnboardingChecklistItemName = "run_first_pipeline"; - const item = getOnboardingItem(onboardingState || {}, itemName); +export function RunFirstPipeline({ active, completed, hasDownstreamStep }: OnboardingStep) { return ( - +

diff --git a/src/app/onboarding/StarterSetup/index.tsx b/src/app/onboarding/StarterSetup/index.tsx index 4851664b9..94a143a4d 100644 --- a/src/app/onboarding/StarterSetup/index.tsx +++ b/src/app/onboarding/StarterSetup/index.tsx @@ -1,8 +1,8 @@ import ChevronDown from "@/assets/icons/chevron-down.svg?react"; import { Tick } from "@/components/Tick"; -import { useServerSettings } from "@/data/server/get-server-settings"; import { useServerInfo } from "@/data/server/info-query"; -import { getOnboardingState, getProgress, getStarterSetupItems } from "@/lib/onboarding"; +import { useOnboarding } from "@/data/server/onboarding-state"; +import { getStarterSetup } from "@/lib/onboarding"; import { checkIsLocalServer } from "@/lib/server"; import { Collapsible, @@ -15,18 +15,21 @@ import { useState } from "react"; import { ConnectZenMLStep, RunFirstPipeline } from "./Items"; export function StarterSetupList() { - const { isError, isPending, data } = useServerSettings({ throwOnError: true }); + const onboarding = useOnboarding({ refetchInterval: 5000 }); const serverInfo = useServerInfo(); const [open, setOpen] = useState(true); - if (isPending || serverInfo.isPending) return ; - if (isError || serverInfo.isError) return null; + if (onboarding.isPending || serverInfo.isPending) + return ; + if (onboarding.isError || serverInfo.isError) return null; const isLocalServer = checkIsLocalServer(serverInfo.data.deployment_type || "other"); - const STARTER_SETUP_ITEMS = getStarterSetupItems(isLocalServer); - - const doneItems = getProgress(getOnboardingState(data), STARTER_SETUP_ITEMS); - const progress = (doneItems / STARTER_SETUP_ITEMS.length) * 100; + const { progress, itemsDone, totalItems, getItem } = getStarterSetup( + onboarding.data, + isLocalServer + ); + const connectStep = getItem("device_verified"); + const pipelineStep = getItem("pipeline_run"); return ( - {doneItems}/{STARTER_SETUP_ITEMS.length} + {itemsDone}/{totalItems} )} @@ -67,11 +70,19 @@ export function StarterSetupList() {

    {!isLocalServer && (
  • - +
  • )}
  • - +
diff --git a/src/app/stacks/create/SmartSetup.tsx b/src/app/stacks/create/SmartSetup.tsx index 744151cd8..37c5de9d2 100644 --- a/src/app/stacks/create/SmartSetup.tsx +++ b/src/app/stacks/create/SmartSetup.tsx @@ -4,7 +4,7 @@ import { useServerInfo } from "@/data/server/info-query"; import { checkIsLocalServer } from "@/lib/server"; import { routes } from "@/router/routes"; import { Skeleton } from "@zenml-io/react-component-library"; -import { Link } from "react-router-dom"; +import { Link, useSearchParams } from "react-router-dom"; import { CreateStackOptionCard } from "./OptionCard"; export function SmartSetup() { @@ -34,10 +34,13 @@ type Props = { isLocalDeployment: boolean; }; function NewInfrastructure({ isLocalDeployment }: Props) { + const [searchParams] = useSearchParams(); + const link = + routes.stacks.create.newInfra + (searchParams.size >= 1 ? `?${searchParams.toString()}` : ""); return (
{isLocalDeployment && } - + ) { - const [open, setOpen] = useState(false); - const queryClient = useQueryClient(); - const { data } = useServerSettings({ throwOnError: true }); - - const { mutate } = useUpdateServerSettings({ - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: getServerSettingsKey() }); - } - }); - - function toggleItem(isDone: boolean) { - const newOnboardingState: OnboardingState = { - ...data?.metadata?.onboarding_state, - [itemName]: isDone - }; - mutate({ onboarding_state: newOnboardingState }); - } + const [open, setOpen] = useState(active); return (
{completed ? ( - + + ) : hasDownstream ? ( + ) : ( )} - +
- {!completed && active && ( - - )}
{title} diff --git a/src/components/onboarding/SkippedStep.tsx b/src/components/onboarding/SkippedStep.tsx new file mode 100644 index 000000000..a60a5c0d5 --- /dev/null +++ b/src/components/onboarding/SkippedStep.tsx @@ -0,0 +1,17 @@ +import { cn } from "@zenml-io/react-component-library"; +import { HTMLAttributes } from "react"; +import Forward from "@/assets/icons/chevron-right-double.svg?react"; + +export function SkippedStep({ className, ...rest }: HTMLAttributes) { + return ( +
+ +
+ ); +} diff --git a/src/data/api.ts b/src/data/api.ts index 82c77b8ef..1bbe10d40 100644 --- a/src/data/api.ts +++ b/src/data/api.ts @@ -5,6 +5,7 @@ export const apiPaths = { currentUser: "/current-user", info: "/info", settings: "/settings", + onboarding: "/onboarding_state", pipelines: { namespaces: "/pipeline_namespaces" }, diff --git a/src/data/server/activate-server-mutation.ts b/src/data/server/activate-server-mutation.ts index dd08ff008..02959d13d 100644 --- a/src/data/server/activate-server-mutation.ts +++ b/src/data/server/activate-server-mutation.ts @@ -20,7 +20,7 @@ export async function updateServerSettings(body: ServerActivationPayload) { const errorData: string = await res .json() .then((data) => { - if (data.detail instanceof Array) { + if (Array.isArray(data.detail)) { return data.detail[1]; } return data.detail; diff --git a/src/data/server/get-server-settings.ts b/src/data/server/get-server-settings.ts index ce41bbe99..8693543cc 100644 --- a/src/data/server/get-server-settings.ts +++ b/src/data/server/get-server-settings.ts @@ -21,7 +21,7 @@ export async function fetchServerSettings(): Promise { const errorData: string = await res .json() .then((data) => { - if (data.detail instanceof Array) { + if (Array.isArray(data.detail)) { return data.detail[1]; } return data.detail; diff --git a/src/data/server/onboarding-state.ts b/src/data/server/onboarding-state.ts new file mode 100644 index 000000000..353bdcf0d --- /dev/null +++ b/src/data/server/onboarding-state.ts @@ -0,0 +1,49 @@ +import { FetchError } from "@/lib/fetch-error"; +import { OnboardingResponse } from "@/types/onboarding"; +import { UseQueryOptions, useQuery } from "@tanstack/react-query"; +import { apiPaths, createApiPath } from "../api"; +import { fetcher } from "../fetch"; + +export function getOnboardingKey() { + return ["onboarding_state"]; +} + +export async function fetchOnboarding() { + const url = createApiPath(apiPaths.onboarding); + const res = await fetcher(url, { + method: "GET", + headers: { + "Content-Type": "application/json" + } + }); + + if (!res.ok) { + const errorData: string = await res + .json() + .then((data) => { + if (Array.isArray(data.detail)) { + return data.detail[1]; + } + return data.detail; + }) + .catch(() => "Failed to update activate user"); + + throw new FetchError({ + status: res.status, + statusText: res.statusText, + message: errorData + }); + } + + return res.json(); +} + +export function useOnboarding( + options?: Omit, "queryKey" | "queryFn"> +) { + return useQuery({ + queryKey: getOnboardingKey(), + queryFn: async () => fetchOnboarding(), + ...options + }); +} diff --git a/src/data/server/update-server-settings-mutation.ts b/src/data/server/update-server-settings-mutation.ts index dab0e2849..bc03d9677 100644 --- a/src/data/server/update-server-settings-mutation.ts +++ b/src/data/server/update-server-settings-mutation.ts @@ -19,7 +19,7 @@ export async function updateServerSettings(body: ServerSettigsUpdate) { const errorData: string = await res .json() .then((data) => { - if (data.detail instanceof Array) { + if (Array.isArray(data.detail)) { return data.detail[1]; } return data.detail; diff --git a/src/data/users/activate-user-mutation.ts b/src/data/users/activate-user-mutation.ts index 8a8661cd1..d69dac83f 100644 --- a/src/data/users/activate-user-mutation.ts +++ b/src/data/users/activate-user-mutation.ts @@ -24,7 +24,7 @@ export async function activateUser({ userId, body }: ActivateUserArgs) { const errorData: string = await res .json() .then((data) => { - if (data.detail instanceof Array) { + if (Array.isArray(data.detail)) { return data.detail[1]; } return data.detail; diff --git a/src/data/users/update-current-user-mutation.ts b/src/data/users/update-current-user-mutation.ts index 49162ae0d..fb910a64d 100644 --- a/src/data/users/update-current-user-mutation.ts +++ b/src/data/users/update-current-user-mutation.ts @@ -19,7 +19,7 @@ export async function updateUser(body: UpdateUser) { const errorData: string = await res .json() .then((data) => { - if (data.detail instanceof Array) { + if (Array.isArray(data.detail)) { return data.detail[1]; } return data.detail; diff --git a/src/layouts/AuthenticatedLayout/OnboardingItem.tsx b/src/layouts/AuthenticatedLayout/OnboardingItem.tsx index 538f79710..325d125e3 100644 --- a/src/layouts/AuthenticatedLayout/OnboardingItem.tsx +++ b/src/layouts/AuthenticatedLayout/OnboardingItem.tsx @@ -1,19 +1,18 @@ import ChevronRight from "@/assets/icons/chevron-right.svg?react"; -import { useServerSettings } from "@/data/server/get-server-settings"; import { useServerInfo } from "@/data/server/info-query"; -import { PRODUCTION_SETUP_ITEMS } from "@/lib/constants"; -import { getOnboardingState, getProgress, getStarterSetupItems } from "@/lib/onboarding"; +import { useOnboarding } from "@/data/server/onboarding-state"; +import { getProductionSetup, getStarterSetup } from "@/lib/onboarding"; import { checkIsLocalServer } from "@/lib/server"; import { routes } from "@/router/routes"; import { Box, ProgressBar, Skeleton, useSidebarContext } from "@zenml-io/react-component-library"; import { Link } from "react-router-dom"; export function OnboardingItem() { - const { isPending, isError, data } = useServerSettings({ throwOnError: true }); + const onboarding = useOnboarding({ refetchInterval: 3600 * 1000 }); const serverInfo = useServerInfo(); const { isOpen } = useSidebarContext(); - if (isError || serverInfo.isError) return null; - if (isPending || serverInfo.isPending) { + if (onboarding.isError || serverInfo.isError) return null; + if (onboarding.isPending || serverInfo.isPending) { return ( @@ -21,32 +20,20 @@ export function OnboardingItem() { ); } - const STARTER_SETUP_ITEMS = getStarterSetupItems( + const starterSetup = getStarterSetup( + onboarding.data, checkIsLocalServer(serverInfo.data.deployment_type || "other") ); + const productionSetup = getProductionSetup(onboarding.data); - const onboardingState = getOnboardingState(data || {}); - const isStarterSetupFinished = - getProgress(onboardingState, STARTER_SETUP_ITEMS) === STARTER_SETUP_ITEMS.length; + 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 isProductionSetupFinished = - getProgress(onboardingState, PRODUCTION_SETUP_ITEMS) === PRODUCTION_SETUP_ITEMS.length; - - const doneItems = getProgress( - onboardingState, - isStarterSetupFinished ? PRODUCTION_SETUP_ITEMS : STARTER_SETUP_ITEMS - ); - - const activeFlow = { - title: isStarterSetupFinished ? "Production Setup" : "Starter Setup", - doneItems, - total: isStarterSetupFinished ? PRODUCTION_SETUP_ITEMS.length : STARTER_SETUP_ITEMS.length - }; - - if (isProductionSetupFinished && isStarterSetupFinished) { - return null; - } - const progress = (activeFlow.doneItems / activeFlow.total) * 100; + if (starterSetup.isFinished && productionSetup.isFinished) return null; return (
  • @@ -55,12 +42,12 @@ export function OnboardingItem() {
    - {activeFlow.title} + {title}
    - {activeFlow.doneItems}/{activeFlow.total} + {completedItems}/{totalItems}
    diff --git a/src/layouts/AuthenticatedLayout/Sidebar.tsx b/src/layouts/AuthenticatedLayout/Sidebar.tsx index a7f467424..46c15156a 100644 --- a/src/layouts/AuthenticatedLayout/Sidebar.tsx +++ b/src/layouts/AuthenticatedLayout/Sidebar.tsx @@ -54,7 +54,7 @@ export function Sidebar() { - {} +
  • { test("doesnt return connect step for local deployment", () => { const isLocal = true; const items = getStarterSetupItems(isLocal); - expect(items).toEqual(["run_first_pipeline"]); + expect(items).toEqual(["pipeline_run"]); }); test("includes the connect step for non-local deployments", () => { const isLocal = false; const items = getStarterSetupItems(isLocal); - expect(items).toEqual(["connect_zenml", "run_first_pipeline"]); + expect(items).toEqual(["device_verified", "pipeline_run"]); + }); +}); + +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); + expect(progress).toBe(2); + }); + + test("returns 0 if no steps are done", () => { + const onboardingState: OnboardingChecklistItemName[] = []; + const progress = getProgress(onboardingState, "starter", 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); + expect(progress).toBe(2); + }); + + test("returns correct value if flow is local", () => { + const onboardingState: OnboardingChecklistItemName[] = ["device_verified"]; + const progress = getProgress(onboardingState, "starter", true); + expect(progress).toBe(0); + }); +}); + +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); + 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); + 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); + 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 + ); + expect(hasDownStreamFinished).toBe(true); + }); +}); + +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); + }); + test("returns correct value for non-local starter setup", () => { + const isLocal = false; + const items = getOnboardingLength("starter", isLocal); + expect(items).toBe(2); }); }); diff --git a/src/lib/onboarding.ts b/src/lib/onboarding.ts index 0600bb763..778f479fe 100644 --- a/src/lib/onboarding.ts +++ b/src/lib/onboarding.ts @@ -1,27 +1,150 @@ -import { OnboardingChecklistItemName, OnboardingState } from "@/types/onboarding"; -import { ServerSettings } from "@/types/server"; +import { OnboardingChecklistItemName, OnboardingResponse } from "@/types/onboarding"; -export function getOnboardingState(data: ServerSettings) { - return data.metadata?.onboarding_state as OnboardingState; +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"]; +} + +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) + }; +} + +export function getProductionSetup(data: OnboardingResponse) { + const flowType: Flow = "production"; + const finalStep = finalSteps[flowType]; + const itemsDone = getProgress(data, flowType); + const totalItems = getOnboardingLength(flowType); + return { + itemsDone, + totalItems, + items: getProductionSetupItems(), + isFinished: data.includes(finalStep), + progress: (itemsDone / totalItems) * 100, + finalStep: finalSteps.starter, + hasItem: (item: OnboardingChecklistItemName) => hasOnboardingItem(item, data), + getItem: (item: OnboardingChecklistItemName) => getItem(item, data, flowType) + }; } -export function getOnboardingItem( - onboardingState: OnboardingState, - item: OnboardingChecklistItemName +function getItem( + item: OnboardingChecklistItemName, + state: OnboardingResponse, + flow: Flow, + isLocal?: boolean ) { - return onboardingState[item]; + return { + isCompleted: state.includes(item), + hasDownStreamStep: checkDownstreamStep(item, state, flow, isLocal || false), + isActive: isStepActive(item, state, flow, isLocal || false) + }; } -export function getProgress( - onboardingState: OnboardingState, - checklistItems: OnboardingChecklistItemName[] +function hasOnboardingItem(item: OnboardingChecklistItemName, state: OnboardingResponse): boolean { + return state.includes(item); +} + +export function checkDownstreamStep( + item: OnboardingChecklistItemName, + state: OnboardingResponse, + flow: Flow, + isLocal?: boolean ) { - return checklistItems.filter((item) => onboardingState[item]).length; + const order = + flow === "starter" ? getStarterSetupItems(isLocal || false) : getProductionSetupItems(); + const withFinalStep = [...order, finalSteps[flow]]; + const currentIndex = withFinalStep.indexOf(item); + if (currentIndex === -1) { + return false; // If the item is not found in the order array, return false + } + const downstreamSteps = withFinalStep.slice(currentIndex + 1); + return downstreamSteps.some((step) => state.includes(step)); } -export function getStarterSetupItems(isLocal: boolean): OnboardingChecklistItemName[] { - return [ - ...(isLocal ? [] : ["connect_zenml" as OnboardingChecklistItemName]), - "run_first_pipeline" - ]; +function isStepActive( + step: OnboardingChecklistItemName, + state: OnboardingResponse, + flowType: Flow, + isLocal: boolean +): boolean { + const flow = flowType === "starter" ? getStarterSetupItems(isLocal) : getProductionSetupItems(); + if (flow.length === 0) { + return false; + } + + const stepIndex = flow.indexOf(step); + + if (stepIndex === -1) { + // Step is not in the setup list + return false; + } + + if (state.includes(step)) { + // Step is already done + return false; + } + + if (stepIndex === 0) { + // First step is active if not done + return true; + } + + // Check if the previous step is done + const previousStep = flow[stepIndex - 1]; + 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]; + // Filter out the finalStep from the checklist items + const filteredItems = items.filter((item) => item !== finalStep); + + // If the final step is present in the onboarding state, return the length of the filtered items + if (onboardingState.includes(finalStep)) { + return filteredItems.length; + } + + // Find the highest index of any present item in the onboarding state + let highestIndex = -1; + filteredItems.forEach((item, index) => { + if (onboardingState.includes(item)) { + highestIndex = index; + } + }); + + // If any item is found, return the highest index + 1 as the completed count + return highestIndex + 1; +} + +export function getOnboardingLength(flow: Flow, isLocal?: boolean) { + const items = + flow === "starter" ? getStarterSetupItems(isLocal || false) : getProductionSetupItems(); + return items.length; } diff --git a/src/types/core.ts b/src/types/core.ts index e7b1d2502..a1cb73fb6 100644 --- a/src/types/core.ts +++ b/src/types/core.ts @@ -1301,6 +1301,16 @@ export type paths = { */ get: operations["server_info_api_v1_info_get"]; }; + "/api/v1/onboarding_state": { + /** + * Get Onboarding State + * @description Get the onboarding state of the server. + * + * Returns: + * The onboarding state of the server. + */ + get: operations["get_onboarding_state_api_v1_onboarding_state_get"]; + }; "/api/v1/settings": { /** * Get Settings @@ -7005,10 +7015,6 @@ export type components = { display_announcements?: boolean | null; /** Whether to display notifications about ZenML updates in the dashboard. */ display_updates?: boolean | null; - /** The server's onboarding state. */ - onboarding_state?: { - [key: string]: unknown; - } | null; /** The username of the default admin account to create. Leave empty to skip creating the default admin account. */ admin_username?: string | null; /** The password of the default admin account to create. Leave empty to skip creating the default admin account. */ @@ -7148,15 +7154,7 @@ export type components = { * ServerSettingsResponseMetadata * @description Response metadata for server settings. */ - ServerSettingsResponseMetadata: { - /** - * The server's onboarding state. - * @default {} - */ - onboarding_state?: { - [key: string]: unknown; - }; - }; + ServerSettingsResponseMetadata: Record; /** * ServerSettingsResponseResources * @description Response resources for server settings. @@ -7179,10 +7177,6 @@ export type components = { display_announcements?: boolean | null; /** Whether to display notifications about ZenML updates in the dashboard. */ display_updates?: boolean | null; - /** The server's onboarding state. */ - onboarding_state?: { - [key: string]: unknown; - } | null; }; /** * ServiceAccountRequest @@ -14158,6 +14152,41 @@ export type operations = { }; }; }; + /** + * Get Onboarding State + * @description Get the onboarding state of the server. + * + * Returns: + * The onboarding state of the server. + */ + get_onboarding_state_api_v1_onboarding_state_get: { + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": string[]; + }; + }; + /** @description Unauthorized */ + 401: { + content: { + "application/json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Not Found */ + 404: { + content: { + "application/json": components["schemas"]["ErrorModel"]; + }; + }; + /** @description Unprocessable Entity */ + 422: { + content: { + "application/json": components["schemas"]["ErrorModel"]; + }; + }; + }; + }; /** * Get Settings * @description Get settings of the server. diff --git a/src/types/onboarding.ts b/src/types/onboarding.ts index 6ef9e63a9..e90d1c9c7 100644 --- a/src/types/onboarding.ts +++ b/src/types/onboarding.ts @@ -1,11 +1,15 @@ export type OnboardingChecklistItemName = - | "connect_zenml" - | "run_first_pipeline" - | "create_service_connector" - | "create_remote_artifact_store" - | "create_remote_stack" - | "run_remote_pipeline"; + | "device_verified" + | "pipeline_run" + | "starter_setup_completed" + | "stack_with_remote_orchestrator_created" + | "pipeline_run_with_remote_orchestrator" + | "production_setup_completed"; -export type OnboardingState = { - [key in OnboardingChecklistItemName]?: boolean; +export type OnboardingResponse = OnboardingChecklistItemName[]; + +export type OnboardingStep = { + completed: boolean; + active: boolean; + hasDownstreamStep: boolean; };