From ddadc0b2f00e1da25c6658ec2ca9bdf959441573 Mon Sep 17 00:00:00 2001 From: Ankur Kumar Date: Sat, 24 Aug 2024 23:44:30 -0400 Subject: [PATCH] Feat(store): Add global store and project refactor --- canvas/app/dashboard/project/id/[id]/job.tsx | 4 +- .../project/id/[id]/jobs-actions.tsx | 65 +++++++++ canvas/app/dashboard/project/id/[id]/jobs.tsx | 39 +++--- .../project/id/[id]/main-content.tsx | 48 ------- canvas/app/dashboard/project/id/[id]/page.tsx | 2 +- .../project/id/[id]/project-actions.tsx | 65 +++++++++ .../app/dashboard/project/id/[id]/project.tsx | 40 ++++++ canvas/app/dashboard/project/id/[id]/run.tsx | 106 ++------------- .../project/id/[id]/runs-actions.tsx | 107 +++++++++++++++ .../project/id/[id]/runs-container.tsx | 36 +++++ canvas/app/dashboard/project/id/[id]/runs.tsx | 35 +++++ .../project/id/[id]/steps-container.tsx | 58 ++++++++ .../app/dashboard/project/id/[id]/steps.tsx | 60 +++------ canvas/app/providers.tsx | 3 +- canvas/components/custom/action-buttons.tsx | 127 ++++++++++++++++++ canvas/components/ui/toast.tsx | 48 +++---- canvas/components/ui/toaster.tsx | 12 +- canvas/components/ui/tooltip.tsx | 30 +++++ canvas/package-lock.json | 64 ++++++++- canvas/package.json | 4 +- canvas/services/run.ts | 4 +- canvas/stores/project/index.ts | 2 + canvas/stores/project/provider.tsx | 45 +++++++ canvas/stores/project/store.ts | 36 +++++ 24 files changed, 803 insertions(+), 237 deletions(-) create mode 100644 canvas/app/dashboard/project/id/[id]/jobs-actions.tsx delete mode 100644 canvas/app/dashboard/project/id/[id]/main-content.tsx create mode 100644 canvas/app/dashboard/project/id/[id]/project-actions.tsx create mode 100644 canvas/app/dashboard/project/id/[id]/project.tsx create mode 100644 canvas/app/dashboard/project/id/[id]/runs-actions.tsx create mode 100644 canvas/app/dashboard/project/id/[id]/runs-container.tsx create mode 100644 canvas/app/dashboard/project/id/[id]/runs.tsx create mode 100644 canvas/app/dashboard/project/id/[id]/steps-container.tsx create mode 100644 canvas/components/custom/action-buttons.tsx create mode 100644 canvas/components/ui/tooltip.tsx create mode 100644 canvas/stores/project/index.ts create mode 100644 canvas/stores/project/provider.tsx create mode 100644 canvas/stores/project/store.ts diff --git a/canvas/app/dashboard/project/id/[id]/job.tsx b/canvas/app/dashboard/project/id/[id]/job.tsx index d0f161b..78600a2 100644 --- a/canvas/app/dashboard/project/id/[id]/job.tsx +++ b/canvas/app/dashboard/project/id/[id]/job.tsx @@ -5,7 +5,7 @@ import { AccordionTrigger, } from "@/components/ui/accordion"; import { TProjectStepJob } from "@/services/job"; -import { ProjectStepJobRun } from "./run"; +import { ProjectStepJobRunsContainer } from "./runs-container"; import React from "react"; export type TProjectStepJobProps = { @@ -22,7 +22,7 @@ export function ProjectStepJob(props: TProjectStepJobProps) { {job.name} - + diff --git a/canvas/app/dashboard/project/id/[id]/jobs-actions.tsx b/canvas/app/dashboard/project/id/[id]/jobs-actions.tsx new file mode 100644 index 0000000..627fc7d --- /dev/null +++ b/canvas/app/dashboard/project/id/[id]/jobs-actions.tsx @@ -0,0 +1,65 @@ +import { ActionsButtons } from "@/components/custom/action-buttons"; +import { TRunStatus } from "@/services/run"; +import React from "react"; + +export function JobsActions() { + const [currentState, setCurrentState] = React.useState("init"); + + return ( + { + setCurrentState("failed"); + }, + tooltipText: "To cancel all the jobs running.", + }} + playButton={{ + isDisabled: false, + tooltipText: "To run all the jobs in the current step.", + onClick: () => { + setCurrentState("pending"); + setTimeout(() => { + setCurrentState("running"); + }, 3000); + setTimeout(() => { + setCurrentState("failed"); + }, 5000); + }, + }} + pendingButton={{ + isDisabled: false, + tooltipText: "All jobs execution is in pending.", + }} + runningButton={{ + isDisabled: false, + tooltipText: "Executing all the jobs in the current steps.", + }} + successButton={{ + isDisabled: false, + onClick: () => { + setCurrentState("pending"); + }, + tooltipText: "Click to run all the jobs again.", + }} + failedButton={{ + tooltipText: + "Last execution was failed. Press this button to retry the execution.", + onClick: () => { + setCurrentState("pending"); + setTimeout(() => { + setCurrentState("running"); + }, 3000); + + setTimeout(() => { + setCurrentState("success"); + }, 6000); + }, + }} + /> + ); +} diff --git a/canvas/app/dashboard/project/id/[id]/jobs.tsx b/canvas/app/dashboard/project/id/[id]/jobs.tsx index e9c7ac6..aa78d96 100644 --- a/canvas/app/dashboard/project/id/[id]/jobs.tsx +++ b/canvas/app/dashboard/project/id/[id]/jobs.tsx @@ -6,17 +6,19 @@ import useSWR from "swr"; import { Accordion } from "@/components/ui/accordion"; import { ProjectStepJob } from "./job"; import { JobsSkeleton } from "./skeleton/jobs-skeleton"; -import { Button } from "@/components/ui/button"; +import { JobsActions } from "./jobs-actions"; +import React from "react"; +import { useProjectStore } from "@/stores/project"; -export type TProjectStepJobProps = { - step: TProjectStep; -}; +export function ProjectStepJobs() { + const { step, updateJobs } = useProjectStore((state) => ({ + step: state.currentStep, + updateJobs: state.updateCurrentStepJobs, + })); -export function ProjectStepJobs(props: TProjectStepJobProps) { - const { step } = props; const { data, error, isLoading } = useSWR( - `/job/?step_id=${step.id}`, - () => getJobs({ stepId: step.id, canThrowOnError: true }), + `/job/?step_id=${step!.id}`, + () => getJobs({ stepId: step!.id, canThrowOnError: true }), { revalidateIfStale: false, revalidateOnFocus: false, @@ -25,6 +27,12 @@ export function ProjectStepJobs(props: TProjectStepJobProps) { } ); + React.useEffect(() => { + if (data && data.response) { + updateJobs(data.response); + } + }, [updateJobs, data]); + if (isLoading && !data) { return ; } @@ -35,15 +43,12 @@ export function ProjectStepJobs(props: TProjectStepJobProps) { return (
-

{step.name}

-
-

- This step includes {data.response.length} job(s). You can run each job - individually or use the "Execute Step" button to run all - jobs at once. -

-
- +

{step!.name}

+
+

{step!.description}

+
+ +
diff --git a/canvas/app/dashboard/project/id/[id]/main-content.tsx b/canvas/app/dashboard/project/id/[id]/main-content.tsx deleted file mode 100644 index 5b40391..0000000 --- a/canvas/app/dashboard/project/id/[id]/main-content.tsx +++ /dev/null @@ -1,48 +0,0 @@ -"use client"; -import useSWR from "swr"; -import { Breadcrumb } from "@/components/custom/breadcrumb"; -import { PageHeading } from "@/components/custom/page-heading"; -import { PROJECTS_LISTING_URL } from "@/constants/routes"; -import { Steps } from "./steps"; -import { TProject } from "@/services/projects"; -import { ProjectError } from "./project-error"; -import { getSteps } from "@/services/step"; -import { StepAndJobsSkeleton } from "./skeleton/step-and-jobs-skeleton"; - -export type TProjectMainContentProps = { - project: TProject; -}; - -export function ProjectMainContent(props: TProjectMainContentProps) { - const { project } = props; - - const { data, error, isLoading } = useSWR( - `/step/?project_id=${project.id}`, - () => getSteps({ projectId: project.id, canThrowOnError: true }), - { - revalidateIfStale: false, - revalidateOnFocus: false, - revalidateOnReconnect: false, - refreshInterval: 1000 * 60, - } - ); - - return ( -
- - - - {!data?.ok && isLoading && } - {!data?.ok && error && !isLoading && } - - {data && data.ok && data.response && ( - - )} -
- ); -} diff --git a/canvas/app/dashboard/project/id/[id]/page.tsx b/canvas/app/dashboard/project/id/[id]/page.tsx index 6f95deb..a118496 100644 --- a/canvas/app/dashboard/project/id/[id]/page.tsx +++ b/canvas/app/dashboard/project/id/[id]/page.tsx @@ -1,5 +1,5 @@ import { PageContainer } from "../../../_layout/page-container"; -import { ProjectMainContent } from "./main-content"; +import { ProjectMainContent } from "./project"; import { getProject } from "@/services/projects"; import { ProjectError } from "./project-error"; diff --git a/canvas/app/dashboard/project/id/[id]/project-actions.tsx b/canvas/app/dashboard/project/id/[id]/project-actions.tsx new file mode 100644 index 0000000..29ba782 --- /dev/null +++ b/canvas/app/dashboard/project/id/[id]/project-actions.tsx @@ -0,0 +1,65 @@ +import { ActionsButtons } from "@/components/custom/action-buttons"; +import { TRunStatus } from "@/services/run"; +import React from "react"; + +export function ProjectActions() { + const [currentState, setCurrentState] = React.useState("init"); + + return ( + { + setCurrentState("failed"); + }, + tooltipText: "To cancel all the steps running in this Project.", + }} + playButton={{ + isDisabled: false, + tooltipText: "To run all the steps in the current project.", + onClick: () => { + setCurrentState("pending"); + setTimeout(() => { + setCurrentState("running"); + }, 3000); + setTimeout(() => { + setCurrentState("failed"); + }, 5000); + }, + }} + pendingButton={{ + isDisabled: false, + tooltipText: "Project execution is in pending.", + }} + runningButton={{ + isDisabled: false, + tooltipText: "Executing all the steps in the current project.", + }} + successButton={{ + isDisabled: false, + onClick: () => { + setCurrentState("pending"); + }, + tooltipText: "Click to run all the steps in the project again.", + }} + failedButton={{ + tooltipText: + "Last execution was failed. Press this button to retry the execution.", + onClick: () => { + setCurrentState("pending"); + setTimeout(() => { + setCurrentState("running"); + }, 3000); + + setTimeout(() => { + setCurrentState("success"); + }, 6000); + }, + }} + /> + ); +} diff --git a/canvas/app/dashboard/project/id/[id]/project.tsx b/canvas/app/dashboard/project/id/[id]/project.tsx new file mode 100644 index 0000000..b6f1677 --- /dev/null +++ b/canvas/app/dashboard/project/id/[id]/project.tsx @@ -0,0 +1,40 @@ +"use client"; + +import { Breadcrumb } from "@/components/custom/breadcrumb"; +import { PageHeading } from "@/components/custom/page-heading"; +import { PROJECTS_LISTING_URL } from "@/constants/routes"; +import { TProject } from "@/services/projects"; +import { ProjectStoreProvider } from "@/stores/project"; +import { StepsContainer } from "./steps-container"; +import { ProjectActions } from "./project-actions"; + +export type TProjectMainContentProps = { + project: TProject; +}; + +export function ProjectMainContent(props: TProjectMainContentProps) { + const { project } = props; + + return ( + +
+ + +
+
+

{project.description}

+
+ +
+
+ +
+
+
+ ); +} diff --git a/canvas/app/dashboard/project/id/[id]/run.tsx b/canvas/app/dashboard/project/id/[id]/run.tsx index 2eb9e46..dee8958 100644 --- a/canvas/app/dashboard/project/id/[id]/run.tsx +++ b/canvas/app/dashboard/project/id/[id]/run.tsx @@ -1,114 +1,30 @@ "use client"; -import { PageSpinner } from "@/components/custom/page-spinner"; import { Button } from "@/components/ui/button"; -import { toast } from "@/components/ui/use-toast"; -import { executeJob, TProjectStepJob } from "@/services/job"; -import { getRun } from "@/services/run"; -import { useMutation } from "@tanstack/react-query"; +import { TProjectStepJobRun } from "@/services/run"; import { Loader2 } from "lucide-react"; import React from "react"; -import useSWR, { mutate } from "swr"; import { JobBadge } from "./badge"; import { FileViewer } from "@/components/custom/file-viewer"; export type TProjectStepJobRunProps = { - job: TProjectStepJob; + run: TProjectStepJobRun; }; export function ProjectStepJobRun(props: TProjectStepJobRunProps) { - const { job } = props; - const key = `/job/execute?job_id=${job.id}`; - const { data, error, isLoading } = useSWR( - key, - () => getRun({ job_id: job.id, canThrowOnError: true }), - { - revalidateIfStale: false, - revalidateOnFocus: false, - revalidateOnReconnect: false, - refreshInterval: 1000 * 30, - } - ); - - const { - mutate: executeJobMutation, - isPending: isSubmittingJob, - isSuccess: isSubmittedSuccessfully, - error: isFailedToExecute, - } = useMutation({ - mutationFn: executeJob, - }); - - function submitJobRequest() { - executeJobMutation({ - jobId: job.id, - canThrowOnError: true, - }); - } - - React.useEffect(() => { - if (isSubmittedSuccessfully) { - // will tell swr to refetch data - mutate(key); - toast({ - title: "Job started successfully", - }); - } else if (isFailedToExecute) { - toast({ - title: "Failed to start the Job", - }); - } - }, [isFailedToExecute, isSubmittedSuccessfully, key]); - - if (isLoading || !data || !data.response) { - return ; - } + const { run } = props; - if (error || !data || !data.response) { - return
Something went wrong!
; - } - - if (data.response.length === 0) { - return ( + return ( +
+
Name: {run.name}
-

No job is running.

- + Status:
- ); - } - return ( -
-
Jobs:
- - {data.response.map((run, idx) => ( -
-
-
Name: {run.name}
-
- Status: -
- {run.run_status === "success" && ( -
- -
- )} -
- {idx !== data.response!.length - 1 &&
} + {run.run_status === "success" && ( +
+
- ))} - - + )}
); } diff --git a/canvas/app/dashboard/project/id/[id]/runs-actions.tsx b/canvas/app/dashboard/project/id/[id]/runs-actions.tsx new file mode 100644 index 0000000..f6a5496 --- /dev/null +++ b/canvas/app/dashboard/project/id/[id]/runs-actions.tsx @@ -0,0 +1,107 @@ +import { ActionsButtons } from "@/components/custom/action-buttons"; +import { toast } from "@/components/ui/use-toast"; +import { executeJob, TProjectStepJob } from "@/services/job"; +import { TRunStatus } from "@/services/run"; +import { useMutation } from "@tanstack/react-query"; +import React from "react"; +import { mutate } from "swr"; + +export type TRunsActionsProps = { + job: TProjectStepJob; + mutateKey: string; +}; + +export function RunsActions(props: TRunsActionsProps) { + const { job, mutateKey } = props; + + const { + mutate: executeJobMutation, + isPending, + isSuccess, + error, + } = useMutation({ + mutationFn: executeJob, + }); + + function submitJobRequest() { + executeJobMutation({ + jobId: job.id, + canThrowOnError: true, + }); + } + + React.useEffect(() => { + if (isSuccess) { + // will tell swr to refetch data + mutate(mutateKey); + toast({ + title: "Job completed successfully", + className: "bg-black text-white", + }); + } else if (error) { + mutate(mutateKey); + toast({ + title: "Failed to start the Job", + variant: "destructive", + }); + } + }, [error, isSuccess, mutateKey]); + const [currentState, setCurrentState] = React.useState("init"); + + React.useEffect(() => { + if (isSuccess) { + setCurrentState("running"); + } else if (error) { + setCurrentState("failed"); + } else if (isPending) { + setCurrentState("pending"); + } + }, [error, isPending, isSuccess]); + + return ( + { + console.log("Cancel this job"); + }, + tooltipText: "To cancel the job.", + }} + playButton={{ + isDisabled: false, + tooltipText: "To execute this job.", + onClick: submitJobRequest, + }} + pendingButton={{ + isDisabled: false, + tooltipText: "This job is pending.", + }} + runningButton={{ + isDisabled: false, + tooltipText: "Executing this job.", + }} + successButton={{ + isDisabled: false, + onClick: () => { + setCurrentState("pending"); + }, + tooltipText: "Click to run this job again.", + }} + failedButton={{ + tooltipText: + "Last execution was failed. Press this button to retry this job.", + onClick: () => { + setCurrentState("pending"); + setTimeout(() => { + setCurrentState("running"); + }, 3000); + + setTimeout(() => { + setCurrentState("success"); + }, 6000); + }, + }} + /> + ); +} diff --git a/canvas/app/dashboard/project/id/[id]/runs-container.tsx b/canvas/app/dashboard/project/id/[id]/runs-container.tsx new file mode 100644 index 0000000..a751332 --- /dev/null +++ b/canvas/app/dashboard/project/id/[id]/runs-container.tsx @@ -0,0 +1,36 @@ +"use client"; + +import { PageSpinner } from "@/components/custom/page-spinner"; +import { TProjectStepJob } from "@/services/job"; +import { getRun } from "@/services/run"; +import React from "react"; +import useSWR from "swr"; +import { ProjectStepJobRuns } from "./runs"; +export type TProjectStepJobRunProps = { + job: TProjectStepJob; +}; + +export function ProjectStepJobRunsContainer(props: TProjectStepJobRunProps) { + const { job } = props; + const key = `/job/execute?job_id=${job.id}`; + const { data, error, isLoading } = useSWR( + key, + () => getRun({ job_id: job.id, canThrowOnError: true }), + { + revalidateIfStale: false, + revalidateOnFocus: false, + revalidateOnReconnect: false, + refreshInterval: 1000 * 30, + } + ); + + if (isLoading || !data || !data.response) { + return ; + } + + if (error || !data || !data.response) { + return
Something went wrong!
; + } + + return ; +} diff --git a/canvas/app/dashboard/project/id/[id]/runs.tsx b/canvas/app/dashboard/project/id/[id]/runs.tsx new file mode 100644 index 0000000..e8df5ee --- /dev/null +++ b/canvas/app/dashboard/project/id/[id]/runs.tsx @@ -0,0 +1,35 @@ +"use client"; + +import { TProjectStepJob } from "@/services/job"; +import { TProjectStepJobRun } from "@/services/run"; +import React from "react"; +import { ProjectStepJobRun } from "./run"; +import { RunsActions } from "./runs-actions"; + +export type TProjectStepJobRunProps = { + runs: TProjectStepJobRun[]; + job: TProjectStepJob; + mutateKey: string; +}; + +export function ProjectStepJobRuns(props: TProjectStepJobRunProps) { + const { runs, job, mutateKey } = props; + + return ( +
+
+

{job.description}

+
+ +
+
+ {runs.map((run, idx) => ( +
+ + {idx !== runs.length - 1 &&
} +
+ ))} + {runs.length === 0 &&

No job is running.

} +
+ ); +} diff --git a/canvas/app/dashboard/project/id/[id]/steps-container.tsx b/canvas/app/dashboard/project/id/[id]/steps-container.tsx new file mode 100644 index 0000000..0b891f9 --- /dev/null +++ b/canvas/app/dashboard/project/id/[id]/steps-container.tsx @@ -0,0 +1,58 @@ +"use client"; +import React from "react"; +import { Sidebar } from "@/components/custom/sidebar"; +import { TProjectStep } from "@/services/step"; +import { ProjectStepJobs } from "./jobs"; +import { Button } from "@/components/ui/button"; +import { Loader2, PlayIcon } from "lucide-react"; +import { ActionsButtons } from "@/components/custom/action-buttons"; +import { useProjectStore } from "@/stores/project"; +import useSWR from "swr"; +import { ProjectError } from "./project-error"; +import { getSteps } from "@/services/step"; +import { StepAndJobsSkeleton } from "./skeleton/step-and-jobs-skeleton"; +import { Steps } from "./steps"; + +export function StepsContainer() { + const { project, updateSteps, updateCurrentStep } = useProjectStore( + (state) => ({ + project: state.project, + updateSteps: state.updateSteps, + updateCurrentStep: state.updateCurrentStep, + }) + ); + + const { data, error, isLoading } = useSWR( + `/step/?project_id=${project.id}`, + () => getSteps({ projectId: project.id, canThrowOnError: true }), + { + revalidateIfStale: false, + revalidateOnFocus: false, + revalidateOnReconnect: false, + refreshInterval: 1000 * 60, + } + ); + + React.useEffect(() => { + if (data && data.ok && data.response) { + updateSteps(data.response); + updateCurrentStep(data.response[0]); + } + }, [data, updateSteps, updateCurrentStep]); + + if (isLoading && !data?.ok) { + return ; + } + + if (!data || !data.response || error) { + return ( +
+

+ Failed to fetch all the steps. Please try again after sometime. +

+
+ ); + } + + return ; +} diff --git a/canvas/app/dashboard/project/id/[id]/steps.tsx b/canvas/app/dashboard/project/id/[id]/steps.tsx index fd93678..8c89325 100644 --- a/canvas/app/dashboard/project/id/[id]/steps.tsx +++ b/canvas/app/dashboard/project/id/[id]/steps.tsx @@ -1,57 +1,37 @@ "use client"; import React from "react"; import { Sidebar } from "@/components/custom/sidebar"; -import { TProjectStep } from "@/services/step"; import { ProjectStepJobs } from "./jobs"; -import { Button } from "@/components/ui/button"; -import { Loader2 } from "lucide-react"; +import { useProjectStore } from "@/stores/project"; -export type TStepsProps = { - steps: TProjectStep[]; - projectDescription: string; -}; - -export function Steps(props: TStepsProps) { - const { steps, projectDescription } = props; - const [activeStep, setActiveStep] = React.useState(steps[0]); - const [isExecuting, setIsExecuting] = React.useState(false); +export function Steps() { + const { steps, currentStep, updateCurrentStep } = useProjectStore( + (state) => ({ + steps: state.steps, + currentStep: state.currentStep, + updateCurrentStep: state.updateCurrentStep, + }) + ); if (steps.length === 0) { return (
-

No steps to show! Add some steps to your pipeline.

+

No steps to show! Add some steps to your pipeline.

); } return ( -
-
-

{projectDescription}

-
-

- This project consists of {steps.length} step(s). You can either - execute the steps individually, run individual jobs one by one, or use - the "Execute Project" button to run all the steps and jobs - at once. -

-
- -
-
- ({ - title: step.name, - id: step.id, - onClick: () => setActiveStep(step), - }))} - /> - -
+
+ ({ + title: step.name, + id: step.id, + onClick: () => updateCurrentStep(step), + }))} + /> + {currentStep && }
); } diff --git a/canvas/app/providers.tsx b/canvas/app/providers.tsx index 63757e6..4e52079 100644 --- a/canvas/app/providers.tsx +++ b/canvas/app/providers.tsx @@ -8,6 +8,7 @@ import { } from "@tanstack/react-query"; import * as React from "react"; import { ReactQueryStreamedHydration } from "@tanstack/react-query-next-experimental"; +import { TooltipProvider } from "@/components/ui/tooltip"; function makeQueryClient() { return new QueryClient({ @@ -47,7 +48,7 @@ export function Providers(props: { children: React.ReactNode }) { return ( - {props.children} + {props.children} ); diff --git a/canvas/components/custom/action-buttons.tsx b/canvas/components/custom/action-buttons.tsx new file mode 100644 index 0000000..6eea0ad --- /dev/null +++ b/canvas/components/custom/action-buttons.tsx @@ -0,0 +1,127 @@ +import { + CheckIcon, + CircleDotDashed, + Icon, + Loader2, + PlayIcon, + RotateCw, + XIcon, +} from "lucide-react"; +import { Button } from "../ui/button"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { TRunStatus } from "@/services/run"; + +export type TActionsButtonProps = { + isDisabled?: boolean; + onClick?: () => void; + tooltipText?: string; +}; + +export type TActionsButtonsProps = { + playButton: TActionsButtonProps; + pendingButton: TActionsButtonProps; + runningButton: TActionsButtonProps; + failedButton: TActionsButtonProps; + successButton: TActionsButtonProps; + cancelButton: TActionsButtonProps; + currentState: TRunStatus; +}; + +export function ActionsButtons(props: TActionsButtonsProps) { + const { + currentState, + pendingButton, + playButton, + runningButton, + failedButton, + cancelButton, + successButton, + } = props; + + function getPrimaryAction() { + switch (currentState) { + case "init": + return { + icon: , + ...playButton, + }; + case "pending": + return { + icon: , + ...pendingButton, + }; + + case "running": + return { + icon: , + ...runningButton, + }; + + case "failed": + return { + icon: , + ...failedButton, + }; + case "success": + return { + icon: , + ...successButton, + }; + default: + return null; + } + } + + const action = getPrimaryAction(); + + if (!action) { + return null; + } + + return ( +
+ + + + + {action.tooltipText && ( + +

{action.tooltipText}

+
+ )} +
+ + + + + {cancelButton.tooltipText && ( + +

{cancelButton.tooltipText}

+
+ )} +
+
+ ); +} diff --git a/canvas/components/ui/toast.tsx b/canvas/components/ui/toast.tsx index fa65d11..7b230fa 100644 --- a/canvas/components/ui/toast.tsx +++ b/canvas/components/ui/toast.tsx @@ -1,13 +1,13 @@ -"use client" +"use client"; -import * as React from "react" -import { Cross2Icon } from "@radix-ui/react-icons" -import * as ToastPrimitives from "@radix-ui/react-toast" -import { cva, type VariantProps } from "class-variance-authority" +import * as React from "react"; +import { Cross2Icon } from "@radix-ui/react-icons"; +import * as ToastPrimitives from "@radix-ui/react-toast"; +import { cva, type VariantProps } from "class-variance-authority"; -import { cn } from "@/shadcn/utils" +import { cn } from "@/shadcn/utils"; -const ToastProvider = ToastPrimitives.Provider +const ToastProvider = ToastPrimitives.Provider; const ToastViewport = React.forwardRef< React.ElementRef, @@ -21,8 +21,8 @@ const ToastViewport = React.forwardRef< )} {...props} /> -)) -ToastViewport.displayName = ToastPrimitives.Viewport.displayName +)); +ToastViewport.displayName = ToastPrimitives.Viewport.displayName; const toastVariants = cva( "group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full", @@ -38,7 +38,7 @@ const toastVariants = cva( variant: "default", }, } -) +); const Toast = React.forwardRef< React.ElementRef, @@ -51,9 +51,9 @@ const Toast = React.forwardRef< className={cn(toastVariants({ variant }), className)} {...props} /> - ) -}) -Toast.displayName = ToastPrimitives.Root.displayName + ); +}); +Toast.displayName = ToastPrimitives.Root.displayName; const ToastAction = React.forwardRef< React.ElementRef, @@ -67,8 +67,8 @@ const ToastAction = React.forwardRef< )} {...props} /> -)) -ToastAction.displayName = ToastPrimitives.Action.displayName +)); +ToastAction.displayName = ToastPrimitives.Action.displayName; const ToastClose = React.forwardRef< React.ElementRef, @@ -85,8 +85,8 @@ const ToastClose = React.forwardRef< > -)) -ToastClose.displayName = ToastPrimitives.Close.displayName +)); +ToastClose.displayName = ToastPrimitives.Close.displayName; const ToastTitle = React.forwardRef< React.ElementRef, @@ -97,8 +97,8 @@ const ToastTitle = React.forwardRef< className={cn("text-sm font-semibold [&+div]:text-xs", className)} {...props} /> -)) -ToastTitle.displayName = ToastPrimitives.Title.displayName +)); +ToastTitle.displayName = ToastPrimitives.Title.displayName; const ToastDescription = React.forwardRef< React.ElementRef, @@ -109,12 +109,12 @@ const ToastDescription = React.forwardRef< className={cn("text-sm opacity-90", className)} {...props} /> -)) -ToastDescription.displayName = ToastPrimitives.Description.displayName +)); +ToastDescription.displayName = ToastPrimitives.Description.displayName; -type ToastProps = React.ComponentPropsWithoutRef +type ToastProps = React.ComponentPropsWithoutRef; -type ToastActionElement = React.ReactElement +type ToastActionElement = React.ReactElement; export { type ToastProps, @@ -126,4 +126,4 @@ export { ToastDescription, ToastClose, ToastAction, -} +}; diff --git a/canvas/components/ui/toaster.tsx b/canvas/components/ui/toaster.tsx index e223385..7d82ed5 100644 --- a/canvas/components/ui/toaster.tsx +++ b/canvas/components/ui/toaster.tsx @@ -1,4 +1,4 @@ -"use client" +"use client"; import { Toast, @@ -7,11 +7,11 @@ import { ToastProvider, ToastTitle, ToastViewport, -} from "@/components/ui/toast" -import { useToast } from "@/components/ui/use-toast" +} from "@/components/ui/toast"; +import { useToast } from "@/components/ui/use-toast"; export function Toaster() { - const { toasts } = useToast() + const { toasts } = useToast(); return ( @@ -27,9 +27,9 @@ export function Toaster() { {action} - ) + ); })} - ) + ); } diff --git a/canvas/components/ui/tooltip.tsx b/canvas/components/ui/tooltip.tsx new file mode 100644 index 0000000..38d14f0 --- /dev/null +++ b/canvas/components/ui/tooltip.tsx @@ -0,0 +1,30 @@ +"use client" + +import * as React from "react" +import * as TooltipPrimitive from "@radix-ui/react-tooltip" + +import { cn } from "@/shadcn/utils" + +const TooltipProvider = TooltipPrimitive.Provider + +const Tooltip = TooltipPrimitive.Root + +const TooltipTrigger = TooltipPrimitive.Trigger + +const TooltipContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + +)) +TooltipContent.displayName = TooltipPrimitive.Content.displayName + +export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } diff --git a/canvas/package-lock.json b/canvas/package-lock.json index 7ed6486..95cd018 100644 --- a/canvas/package-lock.json +++ b/canvas/package-lock.json @@ -19,6 +19,7 @@ "@radix-ui/react-select": "^2.1.1", "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-toast": "^1.2.1", + "@radix-ui/react-tooltip": "^1.1.2", "@tanstack/react-query": "^5.51.23", "@tanstack/react-query-next-experimental": "^5.51.23", "@types/d3-dsv": "^3.0.7", @@ -35,7 +36,8 @@ "tailwind-merge": "^2.5.2", "tailwindcss-animate": "^1.0.7", "wretch": "^2.9.0", - "zod": "^3.23.8" + "zod": "^3.23.8", + "zustand": "^4.5.5" }, "devDependencies": { "@types/node": "^20", @@ -1861,6 +1863,39 @@ } } }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.1.2.tgz", + "integrity": "sha512-9XRsLwe6Yb9B/tlnYCPVUd/TFS4J7HuOZW345DCeC6vKIxQGMZdx21RK4VoZauPD5frgkXTYVS5y90L+3YBn4w==", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.0", + "@radix-ui/react-dismissable-layer": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-popper": "1.2.0", + "@radix-ui/react-portal": "1.1.1", + "@radix-ui/react-presence": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-slot": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-visually-hidden": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-use-callback-ref": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz", @@ -7458,6 +7493,33 @@ "funding": { "url": "https://github.com/sponsors/colinhacks" } + }, + "node_modules/zustand": { + "version": "4.5.5", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.5.tgz", + "integrity": "sha512-+0PALYNJNgK6hldkgDq2vLrw5f6g/jCInz52n9RTpropGgeAf/ioFUCdtsjCqu4gNhW9D01rUQBROoRjdzyn2Q==", + "dependencies": { + "use-sync-external-store": "1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } } } } diff --git a/canvas/package.json b/canvas/package.json index 3d31690..1c125a4 100644 --- a/canvas/package.json +++ b/canvas/package.json @@ -20,6 +20,7 @@ "@radix-ui/react-select": "^2.1.1", "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-toast": "^1.2.1", + "@radix-ui/react-tooltip": "^1.1.2", "@tanstack/react-query": "^5.51.23", "@tanstack/react-query-next-experimental": "^5.51.23", "@types/d3-dsv": "^3.0.7", @@ -36,7 +37,8 @@ "tailwind-merge": "^2.5.2", "tailwindcss-animate": "^1.0.7", "wretch": "^2.9.0", - "zod": "^3.23.8" + "zod": "^3.23.8", + "zustand": "^4.5.5" }, "devDependencies": { "@types/node": "^20", diff --git a/canvas/services/run.ts b/canvas/services/run.ts index 3d21327..de6a48c 100644 --- a/canvas/services/run.ts +++ b/canvas/services/run.ts @@ -1,10 +1,12 @@ import { api, TResponse } from "./api"; +export type TRunStatus = "pending" | "running" | "success" | "failed" | "init"; + export type TProjectStepJobRun = { id: string | number; job_id: string | number; name: string; - run_status: "pending" | "running" | "success" | "failed"; + run_status: TRunStatus; }; export type TGetStepJobsRunOptions = { diff --git a/canvas/stores/project/index.ts b/canvas/stores/project/index.ts new file mode 100644 index 0000000..ea7695e --- /dev/null +++ b/canvas/stores/project/index.ts @@ -0,0 +1,2 @@ +export * from "./store"; +export * from "./provider"; diff --git a/canvas/stores/project/provider.tsx b/canvas/stores/project/provider.tsx new file mode 100644 index 0000000..e446c41 --- /dev/null +++ b/canvas/stores/project/provider.tsx @@ -0,0 +1,45 @@ +import { type ReactNode, createContext, useRef, useContext } from "react"; +import { useStore } from "zustand"; + +import { type TProjectStore, createProjectStore } from "./store"; +import { TProject } from "@/services/projects"; + +export type TProjectStoreApi = ReturnType; + +export const ProjectStoreContext = createContext( + undefined +); + +export type TProjectStoreProviderProps = { + children: ReactNode; + project: TProject; +}; + +export function ProjectStoreProvider(props: TProjectStoreProviderProps) { + const { children, project } = props; + + const storeRef = useRef(); + if (!storeRef.current) { + storeRef.current = createProjectStore({ + project, + }); + } + + return ( + + {children} + + ); +} + +export const useProjectStore = ( + selector: (store: TProjectStore) => T +): T => { + const projectStoreContext = useContext(ProjectStoreContext); + + if (!projectStoreContext) { + throw new Error(`useCounterStore must be used within CounterStoreProvider`); + } + + return useStore(projectStoreContext, selector); +}; diff --git a/canvas/stores/project/store.ts b/canvas/stores/project/store.ts new file mode 100644 index 0000000..3ba461f --- /dev/null +++ b/canvas/stores/project/store.ts @@ -0,0 +1,36 @@ +import { TProjectStepJob } from "@/services/job"; +import { TProject } from "@/services/projects"; +import { TProjectStep } from "@/services/step"; +import { createStore } from "zustand/vanilla"; + +export type TProjectState = { + project: TProject; + steps: TProjectStep[]; + currentStep: TProjectStep | null; + currentActiveStepJobs: TProjectStepJob[]; +}; + +export type TProjectStoreActions = { + updateSteps: (steps: TProjectStep[]) => void; + updateCurrentStep: (step: TProjectStep) => void; + updateCurrentStepJobs: (jobs: TProjectStepJob[]) => void; +}; +export type TProjectStore = TProjectState & TProjectStoreActions; + +export type TCreateProjectStoreOptions = { + project: TProject; +}; + +export function createProjectStore(options: TCreateProjectStoreOptions) { + const { project } = options; + + return createStore()((set) => ({ + project, + steps: [], + currentStep: null, + currentActiveStepJobs: [], + updateSteps: (steps) => set({ steps }), + updateCurrentStep: (step) => set({ currentStep: step }), + updateCurrentStepJobs: (jobs) => set({ currentActiveStepJobs: jobs }), + })); +}