Skip to content

Commit

Permalink
Improve SR table jobs overview (#923)
Browse files Browse the repository at this point in the history
  • Loading branch information
AndrewThien authored Dec 17, 2024
1 parent 95f0f05 commit 1d3b3a4
Show file tree
Hide file tree
Showing 10 changed files with 181 additions and 48 deletions.
12 changes: 12 additions & 0 deletions app/api/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,18 @@ def patch(self, request: Any, *args: Any, **kwargs: Any) -> Response:
trigger = (
f"/api/orchestrators/{settings.AZ_RULES_NAME}?code={settings.AZ_RULES_KEY}"
)
# Prevent double-updating from backend
if Job.objects.filter(
scan_report_table=instance,
status=StageStatus.objects.get(value="IN_PROGRESS"),
):
return Response(
{
"detail": "There is a job running for this table. Please wait until it complete before updating."
},
status=status.HTTP_400_BAD_REQUEST,
)

try:
# Create Job records
# For the first stage, default status is IN_PROGRESS
Expand Down
36 changes: 11 additions & 25 deletions app/next-client-app/app/(protected)/scanreports/[id]/columns.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { ColumnDef } from "@tanstack/react-table";
import { DataTableColumnHeader } from "@/components/data-table/DataTableColumnHeader";
import { EditButton } from "@/components/scanreports/EditButton";
import JobDialog from "@/components/jobs/JobDialog";
import { FindGeneralStatus } from "@/components/jobs/JobUtils";
import { FindGeneralStatus, DivideJobs } from "@/components/jobs/JobUtils";

export const columns: ColumnDef<ScanReportTable>[] = [
{
Expand Down Expand Up @@ -57,28 +57,10 @@ export const columns: ColumnDef<ScanReportTable>[] = [
const { id, name, jobs } = row.original;
// Filter the jobs based on the scanReportTable ID
const jobsData: Job[] = jobs.filter((job) => job.scan_report_table == id);

// Get the general status of the table
const generalStatus = FindGeneralStatus(jobsData);

// Divide the jobs of each table to group of three (each group demonstrates each run)
let jobGroups: Job[][] = [];
if (jobsData.length > 0) {
let jobs: Job[] = [];
jobsData.forEach((job) => {
jobs.push(job);
if (jobs.length === 3) {
// Sort jobs based on the "created_at" field
jobs.sort(
(a, b) =>
new Date(a.created_at).getTime() -
new Date(b.created_at).getTime()
);
jobGroups.push(jobs);
jobs = [];
}
});
}
// Divide jobs into jobs groups
const jobGroups = DivideJobs(jobsData);
// Get the general status of the table for the lastest run
const generalStatus = FindGeneralStatus(jobGroups[0]);

return (
<div className="flex justify-center">
Expand All @@ -96,8 +78,12 @@ export const columns: ColumnDef<ScanReportTable>[] = [
header: ({ column }) => <DataTableColumnHeader column={column} title="" />,
cell: ({ row }) => {
const { id, scan_report, permissions, jobs } = row.original;
// Get the general status of the whole scan report
const generalStatus = FindGeneralStatus(jobs);
// Filter the jobs based on the scanReportTable ID
const jobsData: Job[] = jobs.filter((job) => job.scan_report_table == id);
// Divide jobs into jobs groups
const jobGroups = DivideJobs(jobsData);
// Get the general status of the table for the lastest run
const generalStatus = FindGeneralStatus(jobGroups[0]);
return (
<EditButton
scanreportId={scan_report}
Expand Down
13 changes: 5 additions & 8 deletions app/next-client-app/app/(protected)/scanreports/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { columns } from "./columns";
import {
getScanReportPermissions,
getJobs,
getScanReportTables,
} from "@/api/scanreports";
import { DataTable } from "@/components/data-table";
import { objToQuery } from "@/lib/client-utils";
import { FilterParameters } from "@/types/filter";
import ScanReportsTableClient from "@/components/scanreports/ScanReportsTableClient";
import { DataTableFilter } from "@/components/data-table/DataTableFilter";

interface ScanReportsTableProps {
Expand All @@ -25,7 +24,6 @@ export default async function ScanReportsTable({
const combinedParams = { ...defaultParams, ...searchParams };
const query = objToQuery(combinedParams);
const filter = <DataTableFilter filter="name" />;

const scanReportsTables = await getScanReportTables(id, query);
const permissions = await getScanReportPermissions(id);
// Get data about jobs then inject it to the SR table data
Expand All @@ -42,12 +40,11 @@ export default async function ScanReportsTable({
return (
<div>
<div>
<DataTable
columns={columns}
data={scanReportsResult}
count={scanReportsTables.count}
<ScanReportsTableClient
scanReportId={id}
Filter={filter}
linkPrefix="tables/"
initialScanReportsResult={scanReportsResult}
count={scanReportsTables.count}
/>
</div>
</div>
Expand Down
9 changes: 7 additions & 2 deletions app/next-client-app/components/data-table/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ interface DataTableProps<TData, TValue> {
clickableRow?: boolean;
viewColumns?: boolean;
paginated?: boolean;
overflow?: boolean;
RefreshButton?: JSX.Element;
defaultPageSize?: 10 | 20 | 30 | 40 | 50;
}

Expand All @@ -55,6 +57,8 @@ export function DataTable<TData, TValue>({
clickableRow = true,
viewColumns = true,
paginated = true,
overflow = true,
RefreshButton,
defaultPageSize,
}: DataTableProps<TData, TValue>) {
const [columnVisibility, setColumnVisibility] =
Expand Down Expand Up @@ -92,8 +96,9 @@ export function DataTable<TData, TValue>({

return (
<div>
<div className="flex justify-between mb-3">
<div className="flex justify-between items-center mb-3">
{Filter}
{RefreshButton}
{/* Views Columns Menu */}
{viewColumns && (
<DropdownMenu>
Expand Down Expand Up @@ -132,7 +137,7 @@ export function DataTable<TData, TValue>({
)}
</div>
<div className="rounded-md border">
<Table>
<Table overflow={overflow}>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
Expand Down
1 change: 1 addition & 0 deletions app/next-client-app/components/jobs/JobDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export default function JobDialog({
clickableRow={false}
paginated={false}
viewColumns={false}
overflow={false}
/>
))
: "Retrieving Jobs"}
Expand Down
21 changes: 21 additions & 0 deletions app/next-client-app/components/jobs/JobUtils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,24 @@ export const FindGeneralStatus = (jobsData: Job[]) => {
}
return generalStatus;
};

export const DivideJobs = (jobsData: Job[]) => {
let jobGroups: Job[][] = [];
// Divide the jobs of each table to group of three (each group demonstrates each run)
if (jobsData.length > 0) {
let jobs: Job[] = [];
jobsData.forEach((job) => {
jobs.push(job);
if (jobs.length === 3) {
// Sort jobs based on the "created_at" field
jobs.sort(
(a, b) =>
new Date(a.created_at).getTime() - new Date(b.created_at).getTime()
);
jobGroups.push(jobs);
jobs = [];
}
});
}
return jobGroups;
};
52 changes: 52 additions & 0 deletions app/next-client-app/components/jobs/RefreshButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
"use client";

import { useState } from "react";
import { Button } from "@/components/ui/button";
import { RefreshCw } from "lucide-react";
import { getJobs } from "@/api/scanreports";
import { toast } from "sonner";

interface RefreshButtonProps {
scanReportId: string;
onJobsRefresh: (updatedJobs: Job[]) => void;
}

export function RefreshButton({
scanReportId,
onJobsRefresh,
}: RefreshButtonProps) {
const [isRefreshing, setIsRefreshing] = useState(false);

const handleRefresh = async () => {
try {
setIsRefreshing(true);
// Fetch jobs data
const updatedJobs = await getJobs(scanReportId);
if (updatedJobs) {
onJobsRefresh(updatedJobs);
}
} catch (error: any) {
toast.error("Failed to refresh jobs:", error);
} finally {
setIsRefreshing(false);
}
};

return (
<Button
variant="outline"
onClick={handleRefresh}
disabled={isRefreshing}
className="ml-2"
>
{isRefreshing ? (
<>Refreshing...</>
) : (
<>
<RefreshCw className="mr-2 h-4 w-4" />
Refresh Jobs Progress
</>
)}
</Button>
);
}
2 changes: 1 addition & 1 deletion app/next-client-app/components/scanreports/EditButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export function EditButton({
<Pencil className="ml-2 size-4" />
</Button>
{generalStatus == "IN_PROGRESS" && (
<Tooltips content="You can update the table after jobs running in this scan report finished." />
<Tooltips content="You can update the table fields after jobs running in this table finished." />
)}
</Link>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"use client";

import { useEffect, useState } from "react";

import { DataTable } from "@/components/data-table";
import { RefreshButton } from "../jobs/RefreshButton";
import { columns } from "@/app/(protected)/scanreports/[id]/columns";

export default function ScanReportsTableClient({
initialScanReportsResult,
scanReportId,
Filter,
count,
}: {
initialScanReportsResult: ScanReportTable[];
scanReportId: string;
Filter: JSX.Element;
count: number;
}) {
const [scanReportsResult, setScanReportsResult] = useState(
initialScanReportsResult
);

useEffect(() => {
setScanReportsResult(initialScanReportsResult);
}, [initialScanReportsResult]);

const handleJobsRefresh = (updatedJobs: Job[]) => {
// Update the jobs for each table row
const updatedScanReportsResult = scanReportsResult.map((table) => ({
...table,
jobs: updatedJobs,
}));

setScanReportsResult(updatedScanReportsResult);
};

return (
<div>
<DataTable
columns={columns}
data={scanReportsResult}
count={count}
RefreshButton={
<RefreshButton
scanReportId={scanReportId}
onJobsRefresh={handleJobsRefresh}
/>
}
linkPrefix="tables/"
Filter={Filter}
/>
</div>
);
}
28 changes: 16 additions & 12 deletions app/next-client-app/components/ui/table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,22 @@ import * as React from "react";

import { cn } from "@/lib/utils";

const Table = React.forwardRef<
HTMLTableElement,
React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto">
<table
ref={ref}
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
));
interface TableProps extends React.HTMLAttributes<HTMLTableElement> {
overflow?: boolean;
}

const Table = React.forwardRef<HTMLTableElement, TableProps>(
({ className, overflow, ...props }, ref) => (
<div className={cn("relative w-full", overflow && "overflow-auto")}>
<table
ref={ref}
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
)
);

Table.displayName = "Table";

const TableHeader = React.forwardRef<
Expand Down

0 comments on commit 1d3b3a4

Please sign in to comment.