Apply test run status to progress donut chart

This commit is contained in:
Takeshi Kimata
2024-04-28 12:26:03 +09:00
parent 88a8c6a50b
commit 7af70399a1
5 changed files with 102 additions and 74 deletions

View File

@@ -1,14 +1,12 @@
const express = require("express"); const express = require("express");
const router = express.Router(); const router = express.Router();
const defineRun = require("../../models/runs"); const defineRun = require("../../models/runs");
// const defineCase = require("../../models/cases"); const defineRunCase = require("../../models/runCases");
const { DataTypes } = require("sequelize"); const { DataTypes, literal } = require("sequelize");
module.exports = function (sequelize) { module.exports = function (sequelize) {
const Run = defineRun(sequelize, DataTypes); const Run = defineRun(sequelize, DataTypes);
// const Case = defineCase(sequelize, DataTypes); const RunCase = defineRunCase(sequelize, DataTypes);
// Run.belongsToMany(Case, { through: "runCases" });
// Case.belongsToMany(Run, { through: "runCases" });
router.get("/:runId", async (req, res) => { router.get("/:runId", async (req, res) => {
const runId = req.params.runId; const runId = req.params.runId;
@@ -18,19 +16,21 @@ module.exports = function (sequelize) {
} }
try { try {
// const project = await Run.findByPk(runId, { const run = await Run.findByPk(runId);
// include: [ if (!run) {
// {
// model: Case,
// through: { attributes: ["status"] },
// },
// ],
// });
const project = await Run.findByPk(runId);
if (!project) {
return res.status(404).send("Run not found"); return res.status(404).send("Run not found");
} }
res.json(project);
// Counts test case status belonging to the run
const statusCounts = await RunCase.findAll({
attributes: ["status", [literal("COUNT(*)"), "count"]],
where: {
runId: run.id,
},
group: ["status"],
});
res.json({ run, statusCounts });
} catch (error) { } catch (error) {
console.error(error); console.error(error);
res.status(500).send("Internal Server Error"); res.status(500).send("Internal Server Error");

View File

@@ -25,11 +25,17 @@ import {
ChevronDown, ChevronDown,
CopyPlus, CopyPlus,
CopyMinus, CopyMinus,
RotateCw,
} from "lucide-react"; } from "lucide-react";
import RunProgressDounut from "./RunPregressDonutChart"; import RunProgressChart from "./RunPregressDonutChart";
import TestCaseSelector from "./TestCaseSelector"; import TestCaseSelector from "./TestCaseSelector";
import { testRunStatus } from "@/config/selection"; import { testRunStatus } from "@/config/selection";
import { RunType, RunCaseType, RunCaseInfoType } from "@/types/run"; import {
RunType,
RunCaseType,
RunCaseInfoType,
RunStatusCountType,
} from "@/types/run";
import { CaseType } from "@/types/case"; import { CaseType } from "@/types/case";
import { FolderType } from "@/types/folder"; import { FolderType } from "@/types/folder";
import { import {
@@ -63,6 +69,8 @@ export default function RunEditor({ projectId, runId }: Props) {
const [testRun, setTestRun] = useState<RunType>(defaultTestRun); const [testRun, setTestRun] = useState<RunType>(defaultTestRun);
const [folders, setFolders] = useState([]); const [folders, setFolders] = useState([]);
const [runCases, setRunCases] = useState<RunCaseType[]>([]); const [runCases, setRunCases] = useState<RunCaseType[]>([]);
const [runStatusCounts, setRunStatusCounts] =
useState<RunStatusCountType[]>();
const [selectedKeys, setSelectedKeys] = useState<Selection>(new Set([])); const [selectedKeys, setSelectedKeys] = useState<Selection>(new Set([]));
const [selectedFolder, setSelectedFolder] = useState<FolderType>({}); const [selectedFolder, setSelectedFolder] = useState<FolderType>({});
const [testcases, setTestCases] = useState<CaseType[]>([]); const [testcases, setTestCases] = useState<CaseType[]>([]);
@@ -70,11 +78,16 @@ export default function RunEditor({ projectId, runId }: Props) {
const [isUpdating, setIsUpdating] = useState<boolean>(false); const [isUpdating, setIsUpdating] = useState<boolean>(false);
const router = useRouter(); const router = useRouter();
const fetchRunAndStatusCount = async () => {
const { run, statusCounts } = await fetchRun(runId);
setTestRun(run);
setRunStatusCounts(statusCounts);
};
useEffect(() => { useEffect(() => {
async function fetchDataEffect() { async function fetchDataEffect() {
try { try {
const runData = await fetchRun(runId); await fetchRunAndStatusCount();
setTestRun(runData);
const foldersData = await fetchFolders(projectId); const foldersData = await fetchFolders(projectId);
setFolders(foldersData); setFolders(foldersData);
setSelectedFolder(foldersData[0]); setSelectedFolder(foldersData[0]);
@@ -232,7 +245,23 @@ export default function RunEditor({ projectId, runId }: Props) {
<div className="container mx-auto max-w-5xl pt-6 px-6 flex-grow"> <div className="container mx-auto max-w-5xl pt-6 px-6 flex-grow">
<div className="flex"> <div className="flex">
<div> <div>
<RunProgressDounut /> <div className="w-96 h-72">
<div className="flex items-center">
<h4 className="font-bold">Progress</h4>
<Tooltip content="Refresh">
<Button
isIconOnly
size="sm"
className="rounded-full bg-transparent ms-1"
onPress={fetchRunAndStatusCount}
>
<RotateCw size={16} />
</Button>
</Tooltip>
</div>
<RunProgressChart statusCounts={runStatusCounts} />
</div>
</div> </div>
<div className="flex-grow"> <div className="flex-grow">
<Input <Input
@@ -290,7 +319,7 @@ export default function RunEditor({ projectId, runId }: Props) {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h6 className="h-8 font-bold">Select test cases</h6> <h6 className="h-8 font-bold">Select test cases</h6>
<div> <div>
{selectedKeys.size > 0 || selectedKeys === "all" ? ( {(selectedKeys.size > 0 || selectedKeys === "all") && (
<Dropdown> <Dropdown>
<DropdownTrigger> <DropdownTrigger>
<Button <Button
@@ -316,8 +345,6 @@ export default function RunEditor({ projectId, runId }: Props) {
</DropdownItem> </DropdownItem>
</DropdownMenu> </DropdownMenu>
</Dropdown> </Dropdown>
) : (
<></>
)} )}
</div> </div>
</div> </div>

View File

@@ -1,55 +1,51 @@
import React from "react"; import React from "react";
import { useState } from "react"; import { useState, useEffect } from "react";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import { Button, Tooltip } from "@nextui-org/react"; import { testRunCaseStatus } from "@/config/selection";
import { RotateCw } from "lucide-react"; import { RunStatusCountType } from "@/types/run";
const Chart = dynamic(() => import("react-apexcharts"), { ssr: false }); const Chart = dynamic(() => import("react-apexcharts"), { ssr: false });
const testCases = [ type Props = {
{ name: "Passed", value: 12, color: "#059669" }, statusCounts: RunStatusCountType[];
{ name: "Failed", value: 3, color: "#f87171" }, };
{ name: "Retest", value: 4, color: "#fbbf24" },
{ name: "Skipped", value: 14, color: "#4b5563" },
{ name: "Untested", value: 32, color: "#e5e7eb" },
];
export default function RunProgressDounut() { export default function RunProgressDounut({ statusCounts }: Props) {
const [chartData, setChartData] = useState({ const [chartData, setChartData] = useState({
series: testCases.map((entry) => { series: [],
return entry.value;
}),
options: { options: {
labels: testCases.map((entry) => { labels: [],
return entry.name; colors: [],
}),
colors: testCases.map((entry) => {
return entry.color;
}),
}, },
}); });
return (
<div className="w-96 h-72">
<div className="flex items-center">
<h4 className="font-bold">Progress</h4>
<Tooltip content="Refresh">
<Button
isIconOnly
size="sm"
className="rounded-full bg-transparent ms-1"
onPress={() => console.log("refresh")}
>
<RotateCw size={16} />
</Button>
</Tooltip>
</div>
<Chart useEffect(() => {
options={chartData.options} const updateChartDate = () => {
series={chartData.series} if (statusCounts) {
type="donut" const series = testRunCaseStatus.map((entry, index) => {
width={"100%"} const found = statusCounts.find((itr) => itr.status === index);
height={"100%"} return found ? found.count : 0;
/> });
</div>
const labels = testRunCaseStatus.map((entry) => entry.name);
const colors = testRunCaseStatus.map((entry) => entry.chartColor);
setChartData({
series,
options: { labels, colors },
});
}
};
updateChartDate();
}, [statusCounts]);
return (
<Chart
options={chartData.options}
series={chartData.series}
type="donut"
width={"100%"}
height={"100%"}
/>
); );
} }

View File

@@ -43,11 +43,11 @@ const testRunStatus = [
]; ];
const testRunCaseStatus = [ const testRunCaseStatus = [
{ name: "Untested", uid: "untested", color: "primary" }, { name: "Untested", uid: "untested", color: "primary", chartColor: "#e5e7eb" },
{ name: "Passed", uid: "passed", color: "success" }, { name: "Passed", uid: "passed", color: "success", chartColor: "#059669" },
{ name: "Retest", uid: "retest", color: "warning" }, { name: "Failed", uid: "failed", color: "danger", chartColor: "#f87171" },
{ name: "Failed", uid: "failed", color: "danger" }, { name: "Retest", uid: "retest", color: "warning", chartColor: "#fbbf24" },
{ name: "Skipped", uid: "skipped", color: "primary" }, { name: "Skipped", uid: "skipped", color: "primary", chartColor: "#4b5563" },
]; ];
export { export {

View File

@@ -19,6 +19,11 @@ type RunCaseType = {
type RunCaseInfoType = { type RunCaseInfoType = {
runId: number; runId: number;
caseId: number; caseId: number;
} };
export { RunType, RunCaseType, RunCaseInfoType }; type RunStatusCountType = {
status: number;
count: number;
};
export { RunType, RunCaseType, RunCaseInfoType, RunStatusCountType };