diff --git a/backend/index.js b/backend/index.js index 2444823..fad0031 100644 --- a/backend/index.js +++ b/backend/index.js @@ -103,6 +103,10 @@ app.use("/runcases", runCaseBuldNewRoute); app.use("/runcases", runCaseDeleteRoute); app.use("/runcases", runCaseBulkDeleteRoute); +// "/home" +const homeIndexRoute = require("./routes/home/index")(sequelize); +app.use("/home", homeIndexRoute); + const PORT = process.env.PORT || 3001; app.listen(PORT, () => { console.log(`Server is running on port ${PORT}`); diff --git a/backend/routes/home/index.js b/backend/routes/home/index.js new file mode 100644 index 0000000..ac41e0f --- /dev/null +++ b/backend/routes/home/index.js @@ -0,0 +1,49 @@ +const express = require("express"); +const router = express.Router(); +const defineProject = require("../../models/projects"); +const defineFolder = require("../../models/folders"); +const defineCase = require("../../models/cases"); +const defineRun = require("../../models/runs"); +const defineRunCase = require("../../models/runCases"); +const { DataTypes } = require("sequelize"); + +module.exports = function (sequelize) { + const Project = defineProject(sequelize, DataTypes); + const Folder = defineFolder(sequelize, DataTypes); + const Case = defineCase(sequelize, DataTypes); + const Run = defineRun(sequelize, DataTypes); + const RunCase = defineRunCase(sequelize, DataTypes); + Project.hasMany(Folder, { foreignKey: "projectId" }); + Folder.hasMany(Case, { foreignKey: "folderId" }); + Project.hasMany(Run, { foreignKey: "projectId" }); + Run.hasMany(RunCase, { foreignKey: "runId" }); + + router.get("/:projectId", async (req, res) => { + const projectId = req.params.projectId; + + if (!projectId) { + return res.status(400).json({ error: "projectId is required" }); + } + + try { + const project = await Project.findByPk(projectId, { + include: [ + { + model: Folder, + include: [{ model: Case }], + }, + { model: Run, include: [{ model: RunCase }] }, + ], + }); + if (!project) { + return res.status(404).send("Project not found"); + } + res.json(project); + } catch (error) { + console.error(error); + res.status(500).send("Internal Server Error"); + } + }); + + return router; +}; diff --git a/frontend/config/selection.ts b/frontend/config/selection.ts index d812141..0706437 100644 --- a/frontend/config/selection.ts +++ b/frontend/config/selection.ts @@ -6,19 +6,19 @@ const priorities = [ ]; const testTypes = [ - { uid: "other" }, - { uid: "security" }, - { uid: "performance" }, - { uid: "accessibility" }, - { uid: "functional" }, - { uid: "acceptance" }, - { uid: "usability" }, - { uid: "smokeSanity" }, - { uid: "compatibility" }, - { uid: "destructive" }, - { uid: "regression" }, - { uid: "automated" }, - { uid: "manual" }, + { uid: "other", chartColor: "#688ae8" }, + { uid: "security", chartColor: "#c33d69" }, + { uid: "performance", chartColor: "#2ea597" }, + { uid: "accessibility", chartColor: "#8456ce" }, + { uid: "functional", chartColor: "#e07941" }, + { uid: "acceptance", chartColor: "#3759ce" }, + { uid: "usability", chartColor: "#962249" }, + { uid: "smokeSanity", chartColor: "#096f64" }, + { uid: "compatibility", chartColor: "#6237a7" }, + { uid: "destructive", chartColor: "#a84401" }, + { uid: "regression", chartColor: "#273ea5" }, + { uid: "automated", chartColor: "#780d35" }, + { uid: "manual", chartColor: "#03524a" }, ]; const automationStatus = [ diff --git a/frontend/messages/en.json b/frontend/messages/en.json index 28adbbc..eec5365 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -1,5 +1,5 @@ { - "Home": { + "Index": { "get_started": "Get Started", "integrate_and_manage": "Integrate and manage all your software testing." }, @@ -24,13 +24,33 @@ "close": "Close", "create": "Create", "update": "Update", - "please_enter": "Please enter project name" + "please_enter": "Please enter project name", + "no_projects_found": "No projects found" }, "Project": { "home": "Home", "test_cases": "Test Cases", "test_run": "Test Runs" }, + "Home": { + "Folders": "Folders", + "test_cases": "Test Cases", + "test_runs": "Test Runs", + "test_types": "Test types", + "other": "Other", + "security": "Security", + "performance": "Performance", + "accessibility": "Accessibility", + "functional": "Functional", + "acceptance": "Acceptance", + "usability": "Usability", + "smoke_sanity": "Smoke&Sanity", + "compatibility": "Compatibility", + "destructive": "Destructive", + "regression": "Regression", + "automated": "Automated", + "manual": "Manual" + }, "Folders": { "folder": "Folder", "new_folder": "New Folder", @@ -56,7 +76,8 @@ "critical": "Critical", "high": "High", "medium": "Medium", - "low": "Low" + "low": "Low", + "no_cases_found": "No test cases found" }, "Case": { "back_to_cases": "Back to test cases", diff --git a/frontend/messages/ja.json b/frontend/messages/ja.json index f5ca2b9..c089d87 100644 --- a/frontend/messages/ja.json +++ b/frontend/messages/ja.json @@ -1,5 +1,5 @@ { - "Home": { + "Index": { "get_started": "テスト管理を始める", "integrate_and_manage": "ソフトウェア開発にかかわるすべてのテストを統合管理" }, @@ -24,13 +24,33 @@ "close": "閉じる", "create": "作成", "update": "更新", - "please_enter": "プロジェクト名を入力してください" + "please_enter": "プロジェクト名を入力してください", + "no_projects_found": "プロジェクトがありません" }, "Project": { "home": "ホーム", "test_cases": "テストケース", "test_runs": "テストの実行" }, + "Home": { + "Folders": "フォルダー", + "test_cases": "テストケース", + "test_runs": "テスト実行", + "test_types": "テスト種別", + "other": "その他", + "security": "セキュリティ", + "performance": "パフォーマンス", + "accessibility": "アクセシビリティ", + "functional": "機能", + "acceptance": "受け入れ", + "usability": "ユーザビリティ", + "smoke_sanity": "スモーク/サニティ", + "compatibility": "互換性", + "destructive": "破壊", + "regression": "回帰", + "automated": "自動", + "manual": "手動" + }, "Folders": { "folder": "フォルダー", "new_folder": "新規フォルダー", @@ -56,7 +76,8 @@ "critical": "致", "high": "高", "medium": "中", - "low": "低" + "low": "低", + "no_cases_found": "テストケースがありません" }, "Case": { "back_to_cases": "テストケース一覧に戻る", diff --git a/frontend/src/app/[locale]/page.tsx b/frontend/src/app/[locale]/page.tsx index 4400fde..0d36fa4 100644 --- a/frontend/src/app/[locale]/page.tsx +++ b/frontend/src/app/[locale]/page.tsx @@ -4,7 +4,7 @@ import { useTranslations } from "next-intl"; import { Link } from "@/src/navigation"; export default function Home() { - const t = useTranslations("Home"); + const t = useTranslations("Index"); return (
diff --git a/frontend/src/app/[locale]/projects/ProjectsTable.tsx b/frontend/src/app/[locale]/projects/ProjectsTable.tsx index b5b83da..1a552dd 100644 --- a/frontend/src/app/[locale]/projects/ProjectsTable.tsx +++ b/frontend/src/app/[locale]/projects/ProjectsTable.tsx @@ -151,7 +151,7 @@ export default function ProjectsTable({ )} - + {(item) => ( {(columnKey) => ( diff --git a/frontend/src/app/[locale]/projects/[projectId]/folders/[folderId]/cases/TestCaseTable.tsx b/frontend/src/app/[locale]/projects/[projectId]/folders/[folderId]/cases/TestCaseTable.tsx index 74a78e7..99e2abc 100644 --- a/frontend/src/app/[locale]/projects/[projectId]/folders/[folderId]/cases/TestCaseTable.tsx +++ b/frontend/src/app/[locale]/projects/[projectId]/folders/[folderId]/cases/TestCaseTable.tsx @@ -211,7 +211,7 @@ export default function TestCaseTable({ )} - + {(item) => ( {(columnKey) => ( diff --git a/frontend/src/app/[locale]/projects/[projectId]/folders/[folderId]/cases/page.tsx b/frontend/src/app/[locale]/projects/[projectId]/folders/[folderId]/cases/page.tsx index 97d1f4a..d30b9bc 100644 --- a/frontend/src/app/[locale]/projects/[projectId]/folders/[folderId]/cases/page.tsx +++ b/frontend/src/app/[locale]/projects/[projectId]/folders/[folderId]/cases/page.tsx @@ -21,6 +21,7 @@ export default function Page({ high: t("high"), medium: t("medium"), low: t("low"), + noCasesFound: t("no_cases_found"), }; return ( diff --git a/frontend/src/app/[locale]/projects/[projectId]/home/TestTypesDonutChart.tsx b/frontend/src/app/[locale]/projects/[projectId]/home/TestTypesDonutChart.tsx new file mode 100644 index 0000000..bcc331a --- /dev/null +++ b/frontend/src/app/[locale]/projects/[projectId]/home/TestTypesDonutChart.tsx @@ -0,0 +1,53 @@ +import React from "react"; +import { useState, useEffect } from "react"; +import dynamic from "next/dynamic"; +import { testTypes } from "@/config/selection"; +import { CaseTypeCountType } from "@/types/case"; +import { HomeMessages } from "./page"; +const Chart = dynamic(() => import("react-apexcharts"), { ssr: false }); + +type Props = { + typesCounts: CaseTypeCountType[]; + messages: HomeMessages; +}; + +export default function TestTypesDonutChart({ typesCounts, messages }: Props) { + const [chartData, setChartData] = useState({ + series: [], + options: { + labels: [], + colors: [], + }, + }); + + useEffect(() => { + const updateChartDate = () => { + if (typesCounts) { + const series = testTypes.map((entry, index) => { + const found = typesCounts.find((itr) => itr.type === index); + return found ? found.count : 0; + }); + + const labels = testTypes.map((entry) => messages[entry.uid]); + const colors = testTypes.map((entry) => entry.chartColor); + + setChartData({ + series, + options: { labels, colors }, + }); + } + }; + + updateChartDate(); + }, [typesCounts]); + + return ( + + ); +} diff --git a/frontend/src/app/[locale]/projects/[projectId]/home/home.tsx b/frontend/src/app/[locale]/projects/[projectId]/home/home.tsx index e64cc8b..c78778c 100644 --- a/frontend/src/app/[locale]/projects/[projectId]/home/home.tsx +++ b/frontend/src/app/[locale]/projects/[projectId]/home/home.tsx @@ -1,7 +1,15 @@ "use client"; +import { useState, useEffect } from "react"; +import { Divider } from "@nextui-org/react"; +import { title } from "@/components/primitives"; +import { Card, CardBody, Chip } from "@nextui-org/react"; +import { Folder, Clipboard, FlaskConical } from "lucide-react"; +import { CaseTypeCountType } from "@/types/case"; +import { HomeMessages } from "./page"; +import { testTypes } from "@/config/selection"; +import TestTypesChart from "./TestTypesDonutChart"; import Config from "@/config/config"; const apiServer = Config.apiServer; -import { useState, useEffect } from "react"; async function fetchProject(url) { try { @@ -25,13 +33,21 @@ async function fetchProject(url) { type Props = { projectId: string; + messages: HomeMessages; }; -export function Home({ projectId }: Props) { +export function Home({ projectId, messages }: Props) { const [project, setProject] = useState({ - Folders: [], + name: "", + detail: "", + Folders: [{ Cases: [] }], + Runs: [{ RunCases: [] }], }); - const url = `${apiServer}/projects/${projectId}`; + const [folderNum, setFolderNum] = useState(0); + const [caseNum, setCaseNum] = useState(0); + const [runNum, setRunNum] = useState(0); + const [typesCounts, setTypesCounts] = useState(); + const url = `${apiServer}/home/${projectId}`; useEffect(() => { async function fetchDataEffect() { @@ -47,13 +63,87 @@ export function Home({ projectId }: Props) { fetchDataEffect(); }, [url]); - return ( - <> -

Home

+ useEffect(() => { + async function aggregate() { + aggregateBasicInfo(); + aggregateTestType(); + } -

- Folder num: {project.Folders.length} -

- + aggregate(); + }, [project]); + + // aggregate folder, case, run mum + function aggregateBasicInfo() { + let num = 0; + setFolderNum(project.Folders.length); + setRunNum(project.Runs.length); + project.Folders.forEach((folder) => { + num += folder.Cases.length; + }); + setCaseNum(num); + } + + // aggregate test types of each case + function aggregateTestType() { + const tempTypesCounts = {}; + project.Folders.forEach((folder) => { + folder.Cases.forEach((testcase) => { + const type = testcase.type; + tempTypesCounts[type] = (tempTypesCounts[type] || 0) + 1; + }); + }); + + const result = []; + for (let type = 0; type <= testTypes.length; type++) { + result.push({ type: type, count: tempTypesCounts[type] || 0 }); + } + + setTypesCounts([...result]); + } + + return ( +
+

{project.name}

+
+ } + className="px-3" + > + {folderNum} {messages.folders} + + } + className="px-3 ms-2" + > + {caseNum} {messages.testCases} + + } + className="px-3 ms-2" + > + {runNum} {messages.testRuns} + +
+ + + {project.detail} + + + + +
+
+

{messages.testTypes}

+ +
+
+

{messages.testTypes}

+ +
+
+
); } diff --git a/frontend/src/app/[locale]/projects/[projectId]/home/page.tsx b/frontend/src/app/[locale]/projects/[projectId]/home/page.tsx index 4513862..591b411 100644 --- a/frontend/src/app/[locale]/projects/[projectId]/home/page.tsx +++ b/frontend/src/app/[locale]/projects/[projectId]/home/page.tsx @@ -1,9 +1,50 @@ import { Home } from "./home"; +import { useTranslations } from "next-intl"; + +export type HomeMessages = { + folders: string; + testCases: string; + testRuns: string; + testTypes: string; + other: string; + security: string; + performance: string; + accessibility: string; + functional: string; + acceptance: string; + usability: string; + smokeSanity: string; + compatibility: string; + destructive: string; + regression: string; + automated: string; + manual: string; +}; export default function Page({ params }: { params: { projectId: string } }) { + const t = useTranslations("Home"); + const messages = { + folders: t("Folders"), + testCases: t("test_cases"), + testRuns: t("test_runs"), + testTypes: t("test_types"), + other: t("other"), + security: t("security"), + performance: t("performance"), + accessibility: t("accessibility"), + functional: t("functional"), + acceptance: t("acceptance"), + usability: t("usability"), + smokeSanity: t("smoke_sanity"), + compatibility: t("compatibility"), + destructive: t("destructive"), + regression: t("regression"), + automated: t("automated"), + manual: t("manual"), + }; return ( <> - + ); } diff --git a/frontend/src/app/[locale]/projects/page.tsx b/frontend/src/app/[locale]/projects/page.tsx index a5e8dea..b74d325 100644 --- a/frontend/src/app/[locale]/projects/page.tsx +++ b/frontend/src/app/[locale]/projects/page.tsx @@ -20,6 +20,7 @@ export default function Page(params: { locale }) { create: t("create"), update: t("update"), pleaseEnter: t("please_enter"), + noProjectsFound: t("no_projects_found"), }; return ( <> diff --git a/frontend/types/case.ts b/frontend/types/case.ts index 07c5a97..bf353a7 100644 --- a/frontend/types/case.ts +++ b/frontend/types/case.ts @@ -49,6 +49,11 @@ type AttachmentType = { caseAttachments: CaseAttachmentType; }; +type CaseTypeCountType = { + type: number; + count: number; +}; + export type CasesMessages = { testCaseList: string; id: string; @@ -63,6 +68,7 @@ export type CasesMessages = { high: string; medium: string; low: string; + noCasesFound: string; }; export type CaseMessages = { @@ -113,4 +119,4 @@ export type CaseMessages = { maxFileSize: string; }; -export { CaseType, StepType, AttachmentType, CasesMessages, CaseMessages }; +export { CaseType, StepType, AttachmentType, CaseTypeCountType, CasesMessages, CaseMessages }; diff --git a/frontend/types/project.ts b/frontend/types/project.ts index aed33d1..45988f0 100644 --- a/frontend/types/project.ts +++ b/frontend/types/project.ts @@ -8,7 +8,6 @@ export type ProjectType = { export type ProjectsMessages = { projectList: string; - projects: string; newProject: string; editProject: string; deleteProject: string; @@ -23,6 +22,7 @@ export type ProjectsMessages = { create: string; update: string; pleaseEnter: string; + noProjectsFound: string; }; export type ProjectMessages = {