diff --git a/frontend/app/projects/[projectId]/runs/RunDialog.tsx b/frontend/app/projects/[projectId]/runs/RunDialog.tsx new file mode 100644 index 0000000..ff48d58 --- /dev/null +++ b/frontend/app/projects/[projectId]/runs/RunDialog.tsx @@ -0,0 +1,140 @@ +"use client"; +import React from "react"; +import { useState, useEffect } from "react"; +import { + Button, + Input, + Textarea, + Modal, + ModalContent, + ModalHeader, + ModalBody, + ModalFooter, +} from "@nextui-org/react"; +import { RunType } from "@/types/run"; + +type Props = { + isOpen: boolean; + editingRun: RunType; + onCancel: () => void; + onSubmit: (name: string, detail: string) => void; +}; + +export default function RunDialog({ + isOpen, + editingRun, + onCancel, + onSubmit, +}: Props) { + const [runName, setRunName] = useState({ + text: editingRun ? editingRun.name : "", + isValid: false, + errorMessage: "", + }); + + const [runDetail, setRunDetail] = useState({ + text: editingRun ? editingRun.detail : "", + isValid: false, + errorMessage: "", + }); + + useEffect(() => { + if (editingRun) { + setRunName({ + ...runName, + text: editingRun.name, + }); + + setRunDetail({ + ...runDetail, + text: editingRun.detail ? editingRun.detail : "", + }); + } else { + setRunName({ + ...runName, + text: "", + }); + + setRunDetail({ + ...runDetail, + text: "", + }); + } + }, [editingRun]); + + const clear = () => { + setRunName({ + isValid: false, + text: "", + errorMessage: "", + }); + setRunDetail({ + isValid: false, + text: "", + errorMessage: "", + }); + }; + + const validate = () => { + if (!runName.text) { + setRunName({ + text: "", + isValid: false, + errorMessage: "Please enter run name", + }); + + return; + } + + onSubmit(runName.text, runDetail.text); + clear(); + }; + + return ( + { + onCancel(); + }} + > + + Run + + { + setRunName({ + ...runName, + text: e.target.value, + }); + }} + /> + { + setRunDetail({ + ...runDetail, + text: e.target.value, + }); + }} + /> + + + + Close + + + {editingRun ? "Update" : "Create"} + + + + + ); +} diff --git a/frontend/app/projects/[projectId]/runs/RunsPage.tsx b/frontend/app/projects/[projectId]/runs/RunsPage.tsx new file mode 100644 index 0000000..f578bf3 --- /dev/null +++ b/frontend/app/projects/[projectId]/runs/RunsPage.tsx @@ -0,0 +1,112 @@ +"use client"; +import { useEffect, useState } from "react"; +import { Button } from "@nextui-org/react"; +import { Plus } from "lucide-react"; +import { RunType } from "@/types/run"; +import RunsTable from "./RunsTable"; +import RunDialog from "./RunDialog"; +import { + fetchRuns, + createRun, + updateRun, + deleteRun, +} from "./runsControl"; + +type Props = { + projectId: string; +}; + +export default function RunsPage({ projectId }: Props) { + const [runs, setRuns] = useState([]); + + useEffect(() => { + async function fetchDataEffect() { + try { + const data = await fetchRuns(projectId); + setRuns(data); + } catch (error) { + console.error("Error in effect:", error.message); + } + } + + fetchDataEffect(); + }, []); + + // dialog + const [isRunDialogOpen, setIsRunDialogOpen] = useState(false); + const [editingRun, setEditingRun] = useState( + null + ); + const openDialogForCreate = () => { + setIsRunDialogOpen(true); + setEditingRun(null); + }; + + const closeDialog = () => { + setIsRunDialogOpen(false); + setEditingRun(null); + }; + + const onSubmit = async (name: string, detail: string) => { + if (editingRun) { + const updatedRun = await updateRun( + editingRun.id, + name, + detail + ); + const updatedRuns = runs.map((run) => + run.id === updatedRun.id ? updatedRun : run + ); + setRuns(updatedRuns); + } else { + const newRun = await createRun(name, detail); + setRuns([...runs, newRun]); + } + closeDialog(); + }; + + const onEditClick = (run: RunType) => { + setEditingRun(run); + setIsRunDialogOpen(true); + }; + + const onDeleteClick = async (runId: number) => { + try { + await deleteRun(runId); + setRuns(runs.filter((run) => run.id !== runId)); + } catch (error) { + console.error("Error deleting run:", error); + } + }; + + return ( + + + Runs + + } + size="sm" + color="primary" + onClick={openDialogForCreate} + > + New Run + + + + + + + + + ); +} diff --git a/frontend/app/projects/[projectId]/runs/RunsTable.tsx b/frontend/app/projects/[projectId]/runs/RunsTable.tsx new file mode 100644 index 0000000..781a8c2 --- /dev/null +++ b/frontend/app/projects/[projectId]/runs/RunsTable.tsx @@ -0,0 +1,158 @@ +import { useState, useMemo, useCallback } from "react"; +import { + Table, + TableHeader, + TableColumn, + TableBody, + TableRow, + TableCell, + Button, + DropdownTrigger, + Dropdown, + DropdownMenu, + DropdownItem, + SortDescriptor, + Link, +} from "@nextui-org/react"; +import { MoreVertical } from "lucide-react"; +import { RunType } from "@/types/run"; +import dayjs from "dayjs"; + +const headerColumns = [ + { name: "ID", uid: "id", sortable: true }, + { name: "Name", uid: "name", sortable: true }, + { name: "Description", uid: "description", sortable: true }, + { name: "Last update", uid: "updatedAt", sortable: true }, + { name: "Actions", uid: "actions" }, +]; + +type Props = { + runs: RunType[]; + onEditRun: (run: RunType) => void; + onDeleteRun: (runId: number) => void; +}; + +export default function RunsTable({ runs, onEditRun, onDeleteRun }: Props) { + const [sortDescriptor, setSortDescriptor] = useState({ + column: "id", + direction: "ascending", + }); + + const sortedItems = useMemo(() => { + return [...runs].sort((a: RunType, b: RunType) => { + const first = a[sortDescriptor.column as keyof RunType] as number; + const second = b[sortDescriptor.column as keyof RunType] as number; + const cmp = first < second ? -1 : first > second ? 1 : 0; + + return sortDescriptor.direction === "descending" ? -cmp : cmp; + }); + }, [sortDescriptor, runs]); + + const truncateText = (text: string, maxLength: number) => { + return text.length > maxLength ? text.slice(0, maxLength) + "..." : text; + }; + + const renderCell = useCallback((run: RunType, columnKey: Key) => { + const cellValue = run[columnKey as keyof RunType]; + + switch (columnKey) { + case "id": + return {cellValue}; + case "name": + return ( + + {cellValue} + + ); + case "detail": + const maxLength = 20; + const truncatedValue = truncateText(cellValue, maxLength); + return ( + + {truncatedValue} + + ); + case "updatedAt": + return {dayjs(cellValue).format("YYYY/MM/DD HH:mm")}; + case "actions": + return ( + + + + + + + + onEditRun(run)}> + Edit run + + onDeleteRun(run.id)} + > + Delete run + + + + ); + default: + return cellValue; + } + }, []); + + const classNames = useMemo( + () => ({ + wrapper: ["max-w-3xl"], + th: ["bg-transparent", "text-default-500", "border-b", "border-divider"], + td: [ + // changing the rows border radius + // first + "group-data-[first=true]:first:before:rounded-none", + "group-data-[first=true]:last:before:rounded-none", + // middle + "group-data-[middle=true]:before:rounded-none", + // last + "group-data-[last=true]:first:before:rounded-none", + "group-data-[last=true]:last:before:rounded-none", + ], + }), + [] + ); + + return ( + <> + + + {(column) => ( + + {column.name} + + )} + + + {(item) => ( + + {(columnKey) => ( + {renderCell(item, columnKey)} + )} + + )} + + + > + ); +} diff --git a/frontend/app/projects/[projectId]/runs/page.tsx b/frontend/app/projects/[projectId]/runs/page.tsx index 9624f12..2ac2a97 100644 --- a/frontend/app/projects/[projectId]/runs/page.tsx +++ b/frontend/app/projects/[projectId]/runs/page.tsx @@ -1,3 +1,9 @@ -export default function Page() { - return <>This is Runs tab>; +import RunsPage from "./RunsPage"; + +export default function Page({ params }: { params: { projectId: string } }) { + return ( + <> + + > + ); } diff --git a/frontend/app/projects/[projectId]/runs/runsControl.ts b/frontend/app/projects/[projectId]/runs/runsControl.ts new file mode 100644 index 0000000..54f9f82 --- /dev/null +++ b/frontend/app/projects/[projectId]/runs/runsControl.ts @@ -0,0 +1,105 @@ +import Config from "@/config/config"; +const apiServer = Config.apiServer; + +async function fetchRuns(projectId: string) { + const url = `${apiServer}/runs?projectId=${projectId}`; + + try { + const response = await fetch(url, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + + const data = await response.json(); + return data; + } catch (error) { + console.error("Error fetching data:", error.message); + } +} + +async function createRun(name: string, detail: string) { + const newProjectData = { + name: name, + detail: detail, + }; + + const fetchOptions = { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(newProjectData), + }; + + const url = `${apiServer}/runs`; + + try { + const response = await fetch(url, fetchOptions); + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + const data = await response.json(); + return data; + } catch (error) { + console.error("Error creating new project:", error); + throw error; + } +} + +async function updateRun(runId: number, name: string, detail: string) { + const updatedProjectData = { + name: name, + detail: detail, + }; + + const fetchOptions = { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(updatedProjectData), + }; + + const url = `${apiServer}/runs/${runId}`; + + try { + const response = await fetch(url, fetchOptions); + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + const data = await response.json(); + return data; + } catch (error) { + console.error("Error updating project:", error); + throw error; + } +} + +async function deleteRun(runId: number) { + const fetchOptions = { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, + }; + + const url = `${apiServer}/runs/${runId}`; + + try { + const response = await fetch(url, fetchOptions); + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + } catch (error) { + console.error("Error deleting project:", error); + throw error; + } +} + +export { fetchRuns, createRun, updateRun, deleteRun }; diff --git a/frontend/types/run.ts b/frontend/types/run.ts new file mode 100644 index 0000000..c09d99c --- /dev/null +++ b/frontend/types/run.ts @@ -0,0 +1,10 @@ +export type RunType = { + id: number; + name: string; + configurations: number; + description: string; + state: number; + projectId: number; + createdAt: string; + updatedAt: string; +}; \ No newline at end of file