blob: cdefad26a421a72ecc60f443e3ea47ce48aa365d [file]
import React, { FC, useState } from "react";
import { Link, useNavigate } from "react-router-dom";
import { RunStatus } from "./Status";
import ReactSelect from "react-select";
import ReactTimeAgo from "react-time-ago";
import {
DAGRun,
DAGTemplateWithoutData,
RunStatusType,
} from "../../../state/api/friendlyApi";
import { RunLink, VersionLink } from "../../common/CommonLinks";
import { DurationDisplay } from "../../common/Datetime";
import { adjustStatusForDuration } from "../../../utils";
import VisibilitySensor from "react-visibility-sensor";
const MAX_COMPARE_RUNS = 5;
const TableRow: React.FC<{
run: DAGRun;
version: DAGTemplateWithoutData | undefined;
projectId: number;
setVersion: (version: number) => void;
cols: {
display: string | JSX.Element;
render: (props: RenderProps) => JSX.Element;
}[];
toggleIncludeVersionInCompare: (include: boolean) => void;
includedInCompare: boolean;
}> = (props) => {
return (
<tr className="hover:bg-slate-100 h-12">
<td key={"checkbox"} className="py-2 px-3 text-sm max-w-sm text-gray-500">
<div className="flex h-6 items-center">
<input
id="comments"
aria-describedby="comments-description"
name="comments"
type="checkbox"
checked={props.includedInCompare}
onChange={(e) => {
props.toggleIncludeVersionInCompare(e.target.checked);
}}
className="h-4 w-4 rounded border-gray-300 text-dwdarkblue focus:ring-dwdarkblue"
/>
</div>
</td>
{props.cols.map((col, index) => {
const ToRender = col.render;
return (
<td key={index} className="py-2 px-3 text-sm max-w-sm text-gray-500">
{
<ToRender
projectId={props.projectId}
run={props.run}
status={props.run.run_status}
version={props.version}
setVersion={props.setVersion}
/>
}
</td>
);
})}
</tr>
);
};
type RenderProps = {
projectId: number;
run: DAGRun;
status: string;
version: DAGTemplateWithoutData | undefined;
setVersion: (version: number) => void;
};
export const RunsTable: FC<{
projectId: number;
runs: DAGRun[];
projectVersions: DAGTemplateWithoutData[];
selectedTags: string[];
// selfFilter?: boolean;
}> = (props) => {
const { projectId } = props;
const navigate = useNavigate();
// const [selectedTags, setSelectedTags] = useState([] as string[]);
const [tagFilters, setTagFilters] = useState(new Map<string, string[]>());
const [runsToCompare, setRunsToCompare] = useState([] as number[]);
const selectedTags = props.selectedTags;
const BASE_COLS = [
{
display: (
<button
onClick={() => {
navigate(
`/dashboard/project/${projectId}/runs/${runsToCompare.join(",")}`
);
}}
type="button"
disabled={runsToCompare.length < 1}
className={`runs-compare rounded bg-dwlightblue px-2 py-1 text-sm font-semibold w-20
text-white shadow-sm hover:bg-dwlightblue focus-visible:outline
focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-dwlightblue ${
runsToCompare.length < 1 ? "opacity-70" : ""
}`}
>
{runsToCompare.length >= 2
? "Compare"
: runsToCompare.length == 1
? "View"
: "Select..."}
</button>
),
render: (props: RenderProps) => (
// This is a little awkward -- we need to think through the best way to model this (UUIDs? Or just IDs?)
<div className="w-12">
<Link
to={`/dashboard/project/${props.projectId}/runs/${props.run.id}`}
className="text-dwlightblue flex flex-row gap-2 items-center hover:scale-110 run-link"
>
<RunLink
projectId={props.projectId}
runId={props.run.id as number}
setHighlightedRun={() => void 0}
highlightedRun={null}
></RunLink>
</Link>
</div>
),
},
{
display: "DAG Version",
render: (props: RenderProps) => {
return (
<div className="">
<Link
className="text-dwlightblue cursor-pointer truncate flex gap-2 items-center hover:underline"
to={`/dashboard/project/${props.projectId}/version/${props.run.dag_template}/visualize`}
// onClick={
// // This is a little sloppy -- we should really be ensuring it isn't undefined...
// () => props.setVersion(props.run.project_version as number)
// }
>
<VersionLink
projectId={props.projectId}
versionId={props.run.dag_template_id as number}
nodeName={undefined}
/>
{props.version?.name ? (
<span>{`(${props.version?.name})`}</span>
) : (
<></>
)}
</Link>
</div>
);
},
},
{
display: "Status",
render: (props: RenderProps) => {
const status = adjustStatusForDuration(
props.run.run_status as RunStatusType,
props.run.run_start_time || undefined,
props.run.run_end_time || undefined,
new Date()
);
return <RunStatus status={status as RunStatusType} />;
},
},
{
display: "Duration",
render: (props: RenderProps) => {
return (
<DurationDisplay
startTime={props.run.run_start_time || undefined}
endTime={props.run.run_end_time || undefined}
currentTime={new Date()}
/>
);
},
},
// render: (props: RenderProps) => {
// const {
// formattedHours,
// formattedMinutes,
// formattedSeconds,
// formattedMilliseconds,
// } = durationFormat(
// props.run.run_start_time || undefined,
// props.run.run_end_time || undefined,
// new Date()
// );
// // const out = `${formattedHours}:${formattedMinutes}:${formattedSeconds}.${formattedMilliseconds}`;
// const getTextColor = (durationFormat: string) => {
// if (durationFormat === "00") {
// return "text-gray-200";
// }
// return ""; // default to standrd
// };
// const highlightMilliseconds =
// formattedMinutes === "00" && formattedHours === "00";
// return (
// <div className="font-semibold flex gap-0">
// <span className={`${getTextColor(formattedHours)}`}>
// {formattedHours}:
// </span>
// <span className={`${getTextColor(formattedMinutes)}`}>
// {formattedMinutes}:
// </span>
// <span className={`${getTextColor(formattedSeconds)}`}>
// {formattedSeconds}
// </span>
// <span
// className={`${
// highlightMilliseconds
// ? getTextColor(formattedMilliseconds)
// : "text-gray-200"
// }`}
// >
// .{formattedMilliseconds}
// </span>
// </div>
// );
// },
// },
{
display: "Ran",
render: (props: RenderProps) => {
// return <span>{parseTime(props.run.start_time)}</span>;
if (!props.run.run_start_time) {
return <></>;
}
return <ReactTimeAgo date={new Date(props.run.run_start_time)} />;
},
},
{
display: "Run by",
render: (props: RenderProps) => (
// This is a little awkward -- we need to think through the best way to model this (UUIDs? Or just IDs?)
<span className="font-semibold">
{props.run.username_resolved || ""}
</span>
),
},
];
const projectVersionMap = new Map<number, DAGTemplateWithoutData>();
props.projectVersions.forEach((version) => {
projectVersionMap.set(version.id as number, version);
});
const possibleTagValues = new Map<string, Set<string>>();
props.runs.forEach((run) => {
Object.keys(run.tags || {}).forEach((tag) => {
if (!possibleTagValues.has(tag)) {
possibleTagValues.set(tag, new Set());
}
const tagKey = tag as keyof typeof run.tags;
possibleTagValues.get(tag)?.add(run.tags?.[tagKey] || "");
});
});
const filteredRuns = props.runs.filter((run) => {
if (runsToCompare.indexOf(run.id as number) !== -1) {
return true;
}
const tagMatches = Array.from(tagFilters.entries()).every(([tag, vals]) => {
const tagKey = tag as keyof typeof run.tags;
return vals.length === 0 || vals.includes(run.tags?.[tagKey] || ""); // VAlue length is zero if no filter is selected
});
return tagMatches;
});
const tagCols = Array.from(selectedTags).map((tag) => {
return {
display: (
<ReactSelect
onChange={(selected) => {
setTagFilters((prev) => {
const newMap = new Map(prev);
newMap.set(
tag,
selected.map((opt) => opt.value)
);
return newMap;
});
}}
className={"w-48"}
placeholder={tag}
isMulti
options={Array.from(possibleTagValues.get(tag) || []).map((tag) => {
return { label: tag, value: tag };
})}
/>
),
render: (props: RenderProps) => {
const tagKey = tag as keyof typeof props.run.tags;
return (
<span className="font-semibold">
{props.run.tags?.[tagKey] || ""}
</span>
);
},
};
});
const allCols = [...BASE_COLS, ...tagCols];
return (
<div className="px-4 sm:px-6 lg:px-8 py-2 max-w-full">
<div className="flex flex-row gap-5 pr-10">
{/* {props.selfFilter && (
<TagSelector setSelectedTags={setSelectedTags} allTags={allTags} />
)} */}
</div>
<div className="mt-3 flex flex-col">
<div className="-my-2 -mx-4 sm:-mx-6 lg:-mx-8">
<div className="inline-block min-w-full py-2 align-middle md:px-6 lg:px-8">
<table className="min-w-full divide-y divide-gray-300">
<thead>
<tr className="">
<th className="py-3.5 pl-3 pr-3 text-left text-lg font-semibold text-gray-900">
{/* <TbSortDescending className="hover:scale-125 cursor-pointer" /> */}
</th>
{allCols.map((col, index) => (
<th
key={index}
scope="col"
className="py-3.5 pl-3 pr-3 text-left text-sm font-semibold text-gray-900"
>
{col.display}
</th>
))}
{<th className="w-6"></th>}
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
{[...filteredRuns]
.sort((runA, runB) => {
// TODO -- include if we want them "above the fold"
// Right now its messy, as they move order when you click on them
// We should really use pagination here...
// if (tagFilters.size > 0) {
// if (runACompare && !runBCompare) {
// return -1;
// }
// if (runBCompare && !runACompare) {
// return 1;
// }
// }
return (
(runB.run_start_time
? new Date(runB.run_start_time).getTime()
: new Date().getTime()) -
(runA.run_start_time
? new Date(runA.run_start_time).getTime()
: new Date().getTime())
);
})
.map((run, index) => {
const runID = run.id as number;
return (
<VisibilitySensor
key={index}
offset={{ top: -1000, bottom: -1000 }}
partialVisibility={true}
>
<TableRow
projectId={projectId as number}
run={run}
key={index}
version={projectVersionMap.get(
run.dag_template_id as number
)}
cols={allCols}
setVersion={(version: number) => {
navigate(
`/dashboard/project/${projectId}/version/${version}`
);
}}
includedInCompare={runsToCompare.indexOf(runID) > -1}
toggleIncludeVersionInCompare={(include) => {
if (include) {
runsToCompare.push(runID);
if (runsToCompare.length > MAX_COMPARE_RUNS) {
runsToCompare.shift();
}
} else {
const index = runsToCompare.indexOf(runID, 0);
if (index > -1) {
runsToCompare.splice(index, 1);
}
}
setRunsToCompare(Array.from(runsToCompare));
}}
/>
</VisibilitySensor>
);
})}
</tbody>
</table>
</div>
</div>
</div>
</div>
);
};