diff --git a/torchci/components/HudGroupingSettings/MainPageSettings.module.css b/torchci/components/HudGroupingSettings/MainPageSettings.module.css new file mode 100644 index 0000000000..5735c77145 --- /dev/null +++ b/torchci/components/HudGroupingSettings/MainPageSettings.module.css @@ -0,0 +1,40 @@ +.draggingSource { + opacity: 0.3; +} + +.dropTarget { + background-color: #e8f0fe; +} + +.expandIconWrapper.isOpen { + transform: rotate(90deg); +} + +.expandIconWrapper { + align-items: center; + font-size: 0; + cursor: pointer; + display: flex; + justify-content: center; + transform: rotate(0deg); + margin: 0; + padding: 0; +} + +.node { + height: 32px; + padding-inline-end: 8px; +} +.root { + list-style: none; +} +.root ul { + list-style: none; +} +.labelGridItem { + padding-inline-start: 8px; +} + +.root li:hover { + background-color: #f0f0f0; +} diff --git a/torchci/components/HudGroupingSettings/MainPageSettings.tsx b/torchci/components/HudGroupingSettings/MainPageSettings.tsx new file mode 100644 index 0000000000..889c755df5 --- /dev/null +++ b/torchci/components/HudGroupingSettings/MainPageSettings.tsx @@ -0,0 +1,532 @@ +import { + closestCenter, + DndContext, + PointerSensor, + useSensor, + useSensors, +} from "@dnd-kit/core"; +import { + SortableContext, + useSortable, + verticalListSortingStrategy, +} from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; +import { Check, CopyAll, DragHandle } from "@mui/icons-material"; +import { + Button, + Dialog, + DialogActions, + DialogContent, + List, + ListItem, + ListItemButton, + Stack, + Typography, +} from "@mui/material"; +import { ValidatedTextField } from "components/common/ValidatedTextField"; +import { getDefaultGroupSettings } from "components/HudGroupingSettings/defaults"; +import * as React from "react"; +import { useState } from "react"; +import { + getNonDupNewName, + getStoredTreeData, + Group, + isDupName, + parseTreeData, + saveTreeData, + serializeTreeData, +} from "./mainPageSettingsUtils"; + +function validRegex(value: string) { + try { + new RegExp(value); + return true; + } catch (e) { + return false; + } +} + +// MARK: Default Components + +function FormStack({ + children, + onSubmit, +}: { + children: React.ReactNode; + onSubmit: (e: React.FormEvent) => void; +}) { + return ( + + {children} + + ); +} + +function DefaultOpenDialogButton({ + text, + setOpen, +}: { + text: string; + setOpen: (open: boolean) => void; +}) { + return ( + + ); +} + +function DefaultDialog({ + open, + setOpen, + children, +}: { + open: boolean; + setOpen: (open: boolean) => void; + children: React.ReactNode; +}) { + return ( + setOpen(false)} + aria-modal + > + {children} + + ); +} + +// MARK: Specific Dialogs + +function ImportExportDialog({ + treeData, + setTreeData, +}: { + treeData: Group[]; + setTreeData: (data: Group[]) => void; +}) { + const [open, setOpen] = useState(false); + const [justClicked, setJustClicked] = useState(false); + + return ( + <> + + + + {/* Copy current */} + + { + e.preventDefault(); + const data = new FormData(e.currentTarget); + const value = data.get("Import"); + const revived = parseTreeData(value as string); + if (revived === undefined) { + return; + } + setTreeData(revived); + setOpen(false); + }} + > + parseTreeData(v) !== undefined} + initialValue="" + errorMessage="Invalid import" + /> + + + + + + + + ); +} + +function EditSectionDialog({ + treeData, + name, + setGroup, +}: { + treeData: Group[]; + name: string; + setGroup: (name: string, newName: string, regex: string) => void; +}) { + const [open, setOpen] = useState(false); + + function isGoodName(value: string) { + return value == name || !isDupName(treeData, value); + } + + return ( + <> + + + { + e.preventDefault(); + const formData = new FormData(e.currentTarget); + const regex = formData.get("Filter") as string; + const newName = formData.get("Section name") as string; + if (!validRegex(regex) || !isGoodName(newName)) { + return; + } + setGroup(name, newName, regex); + setOpen(false); + }} + > + + node.name === name)?.regex.source ?? "" + } + errorMessage="Invalid regex" + /> + + + + + + + ); +} + +function ResetButton({ onConfirm }: { onConfirm: () => void }) { + const [open, setOpen] = useState(false); + + return ( + <> + + + + + Are you sure you want to reset to default group settings? + + + + + + + + + ); +} + +// MARK: Main Component + +export default function SettingsModal({ + repositoryFullName, + branchName, + visible, + handleClose, +}: { + repositoryFullName: string; + branchName: string; + visible: boolean; + handleClose: () => void; +}) { + const getStoredTreeDataCustom = () => { + return getStoredTreeData(repositoryFullName, branchName); + }; + const saveTreeDataCustom = (treeData: Group[]) => { + saveTreeData(repositoryFullName, branchName, treeData); + }; + const [treeData, setTreeData] = useState(getStoredTreeDataCustom()); + const [orderBy, setOrderBy] = useState<"display" | "filter">("display"); + const treeDataOrdered = treeData.sort((a, b) => { + if (orderBy === "display") { + return a.displayPriority - b.displayPriority; + } + return a.filterPriority - b.filterPriority; + }); + + const sensors = useSensors(useSensor(PointerSensor)); + + React.useEffect(() => { + setTreeData(getStoredTreeDataCustom()); + }, [repositoryFullName, branchName]); + + function addSection() { + setTreeData([ + { + regex: new RegExp(""), + name: getNonDupNewName(treeData), + filterPriority: 0, + displayPriority: 0, + persistent: false, + hide: false, + }, + ...treeData.map((node) => { + return { + ...node, + filterPriority: node.filterPriority + 1, + displayPriority: node.displayPriority + 1, + }; + }), + ]); + } + + function removeSection(name: string) { + const removedNode = treeData.find((node) => node.name === name); + const filterPriority = removedNode!.filterPriority; + const displayPriority = removedNode!.displayPriority; + const newTreeData = treeData + .filter((node) => node.name !== name) + .map((node) => { + return { + ...node, + filterPriority: + node.filterPriority > filterPriority + ? node.filterPriority - 1 + : node.filterPriority, + displayPriority: + node.displayPriority > displayPriority + ? node.displayPriority - 1 + : node.displayPriority, + }; + }); + setTreeData(newTreeData); + } + + function setItem(name: string, newName: string, regex: string) { + setTreeData( + treeData.map((node) => { + if (node.name === name) { + return { + ...node, + regex: new RegExp(regex), + name: newName, + }; + } + return node; + }) + ); + } + + const Node = React.memo(function Node({ data }: { data: Group }) { + const { attributes, listeners, setNodeRef, transform, transition } = + useSortable({ id: data.name }); + + const style = { + transform: CSS.Transform.toString(transform), + transition, + opacity: data.hide ? 0.4 : 1, + }; + + return ( + + + + + + + {data.name} + + {data.regex.source} + + + + + + + + + + + ); + }); + + function handleDragEnd(event: any) { + const { active, over } = event; + if (active == null || over == null) { + return; + } + const priority = + orderBy === "display" ? "displayPriority" : "filterPriority"; + const oldIndex = treeData.find((node) => node.name === active.id)![ + priority + ]; + const newIndex = treeData.find((node) => node.name === over.id)![priority]; + if (oldIndex < newIndex) { + setTreeData( + treeData.map((node) => { + if (node[priority] === oldIndex) { + return { + ...node, + [priority]: newIndex, + }; + } else if (oldIndex <= node[priority] && node[priority] <= newIndex) { + return { + ...node, + [priority]: node[priority] - 1, + }; + } + return node; + }) + ); + } + if (newIndex < oldIndex) { + setTreeData( + treeData.map((node) => { + if (node[priority] === oldIndex) { + return { + ...node, + [priority]: newIndex, + }; + } else if (newIndex <= node[priority] && node[priority] <= oldIndex) { + return { + ...node, + [priority]: node[priority] + 1, + }; + } + return node; + }) + ); + } + } + return ( + { + saveTreeDataCustom(treeData); + handleClose(); + }} + onClick={(e) => e.stopPropagation()} + > + + + + + { + saveTreeDataCustom(getDefaultGroupSettings()); + setTreeData(getDefaultGroupSettings()); + }} + /> + + + + + + + + + node.name)} + strategy={verticalListSortingStrategy} + > + {treeDataOrdered.map((id) => ( + + ))} + + + + + ); +} diff --git a/torchci/components/HudGroupingSettings/defaults.ts b/torchci/components/HudGroupingSettings/defaults.ts new file mode 100644 index 0000000000..ec93301320 --- /dev/null +++ b/torchci/components/HudGroupingSettings/defaults.ts @@ -0,0 +1,204 @@ +import { Group } from "components/HudGroupingSettings/mainPageSettingsUtils"; + +const GROUP_MEMORY_LEAK_CHECK = "Memory Leak Check"; +const GROUP_RERUN_DISABLED_TESTS = "Rerun Disabled Tests"; +export const GROUP_UNSTABLE = "Unstable"; +const GROUP_PERIODIC = "Periodic"; +const GROUP_INDUCTOR_PERIODIC = "Inductor Periodic"; +const GROUP_SLOW = "Slow"; +const GROUP_LINT = "Lint"; +const GROUP_INDUCTOR = "Inductor"; +const GROUP_ANDROID = "Android"; +const GROUP_ROCM = "ROCm"; +const GROUP_XLA = "XLA"; +const GROUP_LINUX = "Linux"; +const GROUP_BINARY_LINUX = "Binary Linux"; +const GROUP_BINARY_WINDOWS = "Binary Windows"; +const GROUP_ANNOTATIONS_AND_LABELING = "Annotations and labeling"; +const GROUP_DOCKER = "Docker"; +const GROUP_WINDOWS = "Windows"; +const GROUP_CALC_DOCKER_IMAGE = "GitHub calculate-docker-image"; +const GROUP_CI_DOCKER_IMAGE_BUILDS = "CI Docker Image Builds"; +const GROUP_CI_CIRCLECI_PYTORCH_IOS = "ci/circleci: pytorch_ios"; +const GROUP_IOS = "iOS"; +const GROUP_MAC = "Mac"; +const GROUP_PARALLEL = "Parallel"; +const GROUP_DOCS = "Docs"; +const GROUP_LIBTORCH = "Libtorch"; +const GROUP_OTHER_VIABLE_STRICT_BLOCKING = "Other viable/strict blocking"; +const GROUP_XPU = "XPU"; +const GROUP_VLLM = "vLLM"; +export const GROUP_OTHER = "other"; + +// Jobs will be grouped with the first regex they match in this list +export const groups = [ + { + regex: /vllm/, + name: GROUP_VLLM, + }, + { + // Weird regex because some names are too long and getting cut off + // TODO: figure out a better way to name the job or filter them + regex: /, mem_leak/, + name: GROUP_MEMORY_LEAK_CHECK, + persistent: true, + }, + { + regex: /, rerun_/, + name: GROUP_RERUN_DISABLED_TESTS, + persistent: true, + }, + { + regex: /unstable/, + name: GROUP_UNSTABLE, + }, + { + regex: /^xpu/, + name: GROUP_XPU, + }, + { + regex: /inductor-periodic/, + name: GROUP_INDUCTOR_PERIODIC, + }, + { + regex: /periodic/, + name: GROUP_PERIODIC, + }, + { + regex: /slow/, + name: GROUP_SLOW, + }, + { + regex: /Lint/, + name: GROUP_LINT, + }, + { + regex: /inductor/, + name: GROUP_INDUCTOR, + }, + { + regex: /android/, + name: GROUP_ANDROID, + }, + { + regex: /rocm/, + name: GROUP_ROCM, + }, + { + regex: /-xla/, + name: GROUP_XLA, + }, + { + regex: /(\slinux-|sm86)/, + name: GROUP_LINUX, + }, + { + regex: /linux-binary/, + name: GROUP_BINARY_LINUX, + }, + { + regex: /windows-binary/, + name: GROUP_BINARY_WINDOWS, + }, + { + regex: + /(Add annotations )|(Close stale pull requests)|(Label PRs & Issues)|(Triage )|(Update S3 HTML indices)|(is-properly-labeled)|(Facebook CLA Check)|(auto-label-rocm)/, + name: GROUP_ANNOTATIONS_AND_LABELING, + }, + { + regex: + /(ci\/circleci: docker-pytorch-)|(ci\/circleci: ecr_gc_job_)|(ci\/circleci: docker_for_ecr_gc_build_job)|(Garbage Collect ECR Images)/, + name: GROUP_DOCKER, + }, + { + regex: /\swin-/, + name: GROUP_WINDOWS, + }, + { + regex: / \/ calculate-docker-image/, + name: GROUP_CALC_DOCKER_IMAGE, + }, + { + regex: /docker-builds/, + name: GROUP_CI_DOCKER_IMAGE_BUILDS, + }, + { + regex: /ci\/circleci: pytorch_ios_/, + name: GROUP_CI_CIRCLECI_PYTORCH_IOS, + }, + { + regex: /ios-/, + name: GROUP_IOS, + }, + { + regex: /\smacos-/, + name: GROUP_MAC, + }, + { + regex: + /(ci\/circleci: pytorch_parallelnative_)|(ci\/circleci: pytorch_paralleltbb_)|(paralleltbb-linux-)|(parallelnative-linux-)/, + name: GROUP_PARALLEL, + }, + { + regex: /(docs push)|(docs build)/, + name: GROUP_DOCS, + }, + { + regex: /libtorch/, + name: GROUP_LIBTORCH, + }, + { + // This is a catch-all for jobs that are viable but strict blocking + // Excluding linux-binary-* jobs because they are already grouped further up + regex: /(pull)|(trunk)/, + name: GROUP_OTHER_VIABLE_STRICT_BLOCKING, + }, +]; + +// Jobs on HUD home page will be sorted according to this list, with anything left off at the end +// Reorder elements in this list to reorder the groups on the HUD +const HUD_GROUP_SORTING = [ + GROUP_LINT, + GROUP_LINUX, + GROUP_WINDOWS, + GROUP_IOS, + GROUP_MAC, + GROUP_ROCM, + GROUP_XPU, + GROUP_XLA, + GROUP_OTHER_VIABLE_STRICT_BLOCKING, // placed after the last group that tends to have viable/strict blocking jobs + GROUP_VLLM, + GROUP_PARALLEL, + GROUP_LIBTORCH, + GROUP_ANDROID, + GROUP_BINARY_LINUX, + GROUP_DOCKER, + GROUP_CALC_DOCKER_IMAGE, + GROUP_CI_DOCKER_IMAGE_BUILDS, + GROUP_CI_CIRCLECI_PYTORCH_IOS, + GROUP_PERIODIC, + GROUP_SLOW, + GROUP_DOCS, + GROUP_INDUCTOR, + GROUP_INDUCTOR_PERIODIC, + GROUP_ANNOTATIONS_AND_LABELING, + GROUP_BINARY_WINDOWS, + GROUP_MEMORY_LEAK_CHECK, + GROUP_RERUN_DISABLED_TESTS, + // These two groups should always be at the end + GROUP_OTHER, + GROUP_UNSTABLE, +]; + +export function getDefaultGroupSettings(): Group[] { + return groups.map((g, i) => { + return { + name: g.name, + regex: g.regex, + filterPriority: i, + displayPriority: HUD_GROUP_SORTING.indexOf(g.name), + persistent: g.persistent ?? false, + hide: false, + }; + }); +} diff --git a/torchci/components/HudGroupingSettings/hudGroupingSettings.ts b/torchci/components/HudGroupingSettings/hudGroupingSettings.ts new file mode 100644 index 0000000000..5e07aba151 --- /dev/null +++ b/torchci/components/HudGroupingSettings/hudGroupingSettings.ts @@ -0,0 +1,142 @@ +import { Group } from "components/HudGroupingSettings/mainPageSettingsUtils"; +import { isFailure } from "lib/JobClassifierUtil"; +import { getOpenUnstableIssues } from "lib/jobUtils"; +import { IssueData, RowData } from "lib/types"; +import { + getDefaultGroupSettings, + GROUP_OTHER, + GROUP_UNSTABLE, +} from "./defaults"; +import { getStoredTreeData } from "./mainPageSettingsUtils"; + +function getGroupSettings(repositoryFullName: string, branchName: string) { + const groups = + getStoredTreeData(repositoryFullName, branchName) ?? + getDefaultGroupSettings(); + groups.push({ + name: GROUP_OTHER, + regex: /.*/, + filterPriority: groups.length, + displayPriority: groups.length, + persistent: false, + hide: false, + }); + return groups; +} + +export function getGroupingData( + repositoryFullName: string, + branchName: string, + shaGrid: RowData[], + jobNames: Set, + showUnstableGroup: boolean, + unstableIssues?: IssueData[] +) { + // Construct Job Groupping Mapping + const groupSettings = getGroupSettings(repositoryFullName, branchName); + + const groupNameMapping = new Map>(); // group -> [job names] + + // Track which jobs have failures + const jobsWithFailures = new Set(); + + // First pass: check failures for each job across all commits + for (const name of jobNames) { + // Check if this job has failures in any commit + const hasFailure = shaGrid.some((row) => { + const job = row.nameToJobs.get(name); + return job && isFailure(job.conclusion); + }); + + if (hasFailure) { + jobsWithFailures.add(name); + } + } + + for (const name of jobNames) { + const groupName = classifyGroup( + name, + showUnstableGroup, + groupSettings, + unstableIssues + ); + const jobsInGroup = groupNameMapping.get(groupName) ?? []; + jobsInGroup.push(name); + groupNameMapping.set(groupName, jobsInGroup); + } + + // Remove hidden groups + for (const group of groupSettings) { + if (group.hide && groupNameMapping.has(group.name)) { + groupNameMapping.delete(group.name); + } + } + + // Calculate which groups have failures + const groupsWithFailures = new Set(); + for (const [groupName, jobs] of groupNameMapping.entries()) { + if (jobs.some((jobName) => jobsWithFailures.has(jobName))) { + groupsWithFailures.add(groupName); + } + } + + return { + shaGrid, + groupNameMapping, + jobsWithFailures, + groupsWithFailures, + groupSettings, + }; +} + +// Accepts a list of group names and returns that list sorted according to +// the order defined in HUD_GROUP_SORTING +export function sortGroupNamesForHUD( + groupNames: string[], + groups: Group[] +): string[] { + let result = groupNames.sort((a, b) => { + return ( + groups.find((g) => g.name === a)!.displayPriority - + groups.find((g) => g.name === b)!.displayPriority + ); + }); + + // Be flexible in case against any groups were left out of HUD_GROUP_SORTING + let remaining = groupNames.filter((x) => !result.includes(x)); + + result = result.concat(remaining); + return result; +} + +export function classifyGroup( + jobName: string, + showUnstableGroup: boolean, + groups: Group[], + unstableIssues?: IssueData[] +): string { + const openUnstableIssues = getOpenUnstableIssues(jobName, unstableIssues); + let assignedGroup = undefined; + for (const group of groups.sort( + (a, b) => a.filterPriority - b.filterPriority + )) { + if (jobName.match(group.regex)) { + assignedGroup = group; + break; + } + } + + // Check if the job has been marked as unstable but doesn't include the + // unstable keyword. + if (!showUnstableGroup && assignedGroup?.persistent) { + // If the unstable group is not being shown, then persistent groups (mem + // leak check, rerun disabled tests) should not be overwritten + return assignedGroup.name; + } + + if (openUnstableIssues !== undefined && openUnstableIssues.length !== 0) { + return GROUP_UNSTABLE; + } + + return assignedGroup === undefined ? GROUP_OTHER : assignedGroup.name; +} diff --git a/torchci/components/HudGroupingSettings/mainPageSettingsUtils.ts b/torchci/components/HudGroupingSettings/mainPageSettingsUtils.ts new file mode 100644 index 0000000000..286e22adbd --- /dev/null +++ b/torchci/components/HudGroupingSettings/mainPageSettingsUtils.ts @@ -0,0 +1,114 @@ +import _ from "lodash"; +import { getDefaultGroupSettings } from "./defaults"; + +export type Group = { + name: string; + regex: RegExp; + filterPriority: number; + displayPriority: number; + persistent: boolean; + hide: boolean; +}; + +export function serializeTreeData(treeData: Group[]): string { + // Convert RegExp objects to a format that can be seralized + const serializable = treeData.map((group) => ({ + ...group, + regex: group.regex.source, // Store only the regex pattern as a string + })); + + return JSON.stringify(serializable); +} + +export function parseTreeData(input: string): Group[] | undefined { + try { + const parsed = JSON.parse(input); + + // Convert the stored string patterns back to RegExp objects + return parsed.map((item: any) => ({ + ...item, + regex: new RegExp(item.regex || ""), + })); + } catch (error) { + return undefined; + } +} + +const LOCAL_STORAGE_KEY = "hud_group_settings"; +export function saveTreeData( + repositoryFullName: string, + branchName: string, + treeData: Group[] +) { + const localStorageContents = JSON.parse( + localStorage.getItem(LOCAL_STORAGE_KEY) || "{}" + ); + const repoBranchKey = `${repositoryFullName}::${branchName}`; + + if ( + _.isEqual( + treeData.sort((a, b) => a.name.localeCompare(b.name)), + getDefaultGroupSettings().sort((a, b) => a.name.localeCompare(b.name)) + ) + ) { + // If the current settings are the same as the default, remove from + // localStorage + localStorageContents[repoBranchKey] = undefined; + localStorage.setItem( + LOCAL_STORAGE_KEY, + JSON.stringify(localStorageContents) + ); + return; + } + + const setting = serializeTreeData(treeData); + localStorageContents[repoBranchKey] = setting; + localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(localStorageContents)); +} + +export function getStoredTreeData( + repositoryFullName: string, + branchName: string +): Group[] { + try { + // Try to load saved tree data from localStorage + const stored = JSON.parse( + localStorage.getItem("hud_group_settings") || "{}" + ); + + if (!stored) return getDefaultGroupSettings(); + const repoBranchKey = `${repositoryFullName}::${branchName}`; + const storedSetting = stored[repoBranchKey]; + if (storedSetting) { + const parsed = parseTreeData(storedSetting); + if (parsed === undefined) { + return getDefaultGroupSettings(); + } + return parsed; + } + const backUpKey = `${repositoryFullName}::main`; + const backUpSetting = stored[backUpKey]; + if (backUpSetting) { + const parsed = parseTreeData(backUpSetting); + if (parsed !== undefined) { + return parsed; + } + } + return getDefaultGroupSettings(); + } catch (error) { + console.error("Error loading stored group settings:", error); + return getDefaultGroupSettings(); + } +} + +export function getNonDupNewName(treeData: Group[]) { + let i = 0; + while (isDupName(treeData, `NEW GROUP ${i}`)) { + i++; + } + return `NEW GROUP ${i}`; +} + +export function isDupName(treeData: Group[], name: string): boolean { + return treeData.some((node) => node.name === name); +} diff --git a/torchci/components/common/ValidatedTextField.tsx b/torchci/components/common/ValidatedTextField.tsx new file mode 100644 index 0000000000..06bd3f8211 --- /dev/null +++ b/torchci/components/common/ValidatedTextField.tsx @@ -0,0 +1,34 @@ +import { TextField } from "@mui/material"; +import { useState } from "react"; + +// Text field but with validation +export function ValidatedTextField({ + name, + isValid, + initialValue, + errorMessage = "Invalid", +}: { + name: string; + isValid: (value: string) => boolean; + initialValue: string; + errorMessage?: string; +}) { + const [value, setValue] = useState(initialValue); + const [valid, setValid] = useState(true); + function onChangeWrapper(e: React.ChangeEvent) { + const newValue = e.target.value; + setValue(newValue); + setValid(isValid(newValue)); + } + + return ( + + ); +} diff --git a/torchci/lib/JobClassifierUtil.ts b/torchci/lib/JobClassifierUtil.ts index 75aaca3ff6..37651cd88d 100644 --- a/torchci/lib/JobClassifierUtil.ts +++ b/torchci/lib/JobClassifierUtil.ts @@ -1,204 +1,20 @@ +import { Group } from "components/HudGroupingSettings/mainPageSettingsUtils"; import { GroupedJobStatus, JobStatus } from "components/job/GroupJobConclusion"; import { getOpenUnstableIssues } from "lib/jobUtils"; -import { IssueData, RowData } from "./types"; - -const GROUP_MEMORY_LEAK_CHECK = "Memory Leak Check"; -const GROUP_RERUN_DISABLED_TESTS = "Rerun Disabled Tests"; -const GROUP_UNSTABLE = "Unstable"; -const GROUP_PERIODIC = "Periodic"; -const GROUP_INDUCTOR_PERIODIC = "Inductor Periodic"; -const GROUP_SLOW = "Slow"; -const GROUP_LINT = "Lint"; -const GROUP_INDUCTOR = "Inductor"; -const GROUP_ANDROID = "Android"; -const GROUP_ROCM = "ROCm"; -const GROUP_XLA = "XLA"; -const GROUP_LINUX = "Linux"; -const GROUP_BINARY_LINUX = "Binary Linux"; -const GROUP_BINARY_WINDOWS = "Binary Windows"; -const GROUP_ANNOTATIONS_AND_LABELING = "Annotations and labeling"; -const GROUP_DOCKER = "Docker"; -const GROUP_WINDOWS = "Windows"; -const GROUP_CALC_DOCKER_IMAGE = "GitHub calculate-docker-image"; -const GROUP_CI_DOCKER_IMAGE_BUILDS = "CI Docker Image Builds"; -const GROUP_CI_CIRCLECI_PYTORCH_IOS = "ci/circleci: pytorch_ios"; -const GROUP_IOS = "iOS"; -const GROUP_MAC = "Mac"; -const GROUP_PARALLEL = "Parallel"; -const GROUP_DOCS = "Docs"; -const GROUP_LIBTORCH = "Libtorch"; -const GROUP_OTHER_VIABLE_STRICT_BLOCKING = "Other viable/strict blocking"; -const GROUP_XPU = "XPU"; -const GROUP_VLLM = "vLLM"; -const GROUP_OTHER = "other"; - -// Jobs will be grouped with the first regex they match in this list -export const groups = [ - { - regex: /vllm/, - name: GROUP_VLLM, - }, - { - // Weird regex because some names are too long and getting cut off - // TODO: figure out a better way to name the job or filter them - regex: /, mem_leak/, - name: GROUP_MEMORY_LEAK_CHECK, - persistent: true, - }, - { - regex: /, rerun_/, - name: GROUP_RERUN_DISABLED_TESTS, - persistent: true, - }, - { - regex: /unstable/, - name: GROUP_UNSTABLE, - }, - { - regex: /^xpu/, - name: GROUP_XPU, - }, - { - regex: /inductor-periodic/, - name: GROUP_INDUCTOR_PERIODIC, - }, - { - regex: /periodic/, - name: GROUP_PERIODIC, - }, - { - regex: /slow/, - name: GROUP_SLOW, - }, - { - regex: /Lint/, - name: GROUP_LINT, - }, - { - regex: /inductor/, - name: GROUP_INDUCTOR, - }, - { - regex: /android/, - name: GROUP_ANDROID, - }, - { - regex: /rocm/, - name: GROUP_ROCM, - }, - { - regex: /-xla/, - name: GROUP_XLA, - }, - { - regex: /(\slinux-|sm86)/, - name: GROUP_LINUX, - }, - { - regex: /linux-binary/, - name: GROUP_BINARY_LINUX, - }, - { - regex: /windows-binary/, - name: GROUP_BINARY_WINDOWS, - }, - { - regex: - /(Add annotations )|(Close stale pull requests)|(Label PRs & Issues)|(Triage )|(Update S3 HTML indices)|(is-properly-labeled)|(Facebook CLA Check)|(auto-label-rocm)/, - name: GROUP_ANNOTATIONS_AND_LABELING, - }, - { - regex: - /(ci\/circleci: docker-pytorch-)|(ci\/circleci: ecr_gc_job_)|(ci\/circleci: docker_for_ecr_gc_build_job)|(Garbage Collect ECR Images)/, - name: GROUP_DOCKER, - }, - { - regex: /\swin-/, - name: GROUP_WINDOWS, - }, - { - regex: / \/ calculate-docker-image/, - name: GROUP_CALC_DOCKER_IMAGE, - }, - { - regex: /docker-builds/, - name: GROUP_CI_DOCKER_IMAGE_BUILDS, - }, - { - regex: /ci\/circleci: pytorch_ios_/, - name: GROUP_CI_CIRCLECI_PYTORCH_IOS, - }, - { - regex: /ios-/, - name: GROUP_IOS, - }, - { - regex: /\smacos-/, - name: GROUP_MAC, - }, - { - regex: - /(ci\/circleci: pytorch_parallelnative_)|(ci\/circleci: pytorch_paralleltbb_)|(paralleltbb-linux-)|(parallelnative-linux-)/, - name: GROUP_PARALLEL, - }, - { - regex: /(docs push)|(docs build)/, - name: GROUP_DOCS, - }, - { - regex: /libtorch/, - name: GROUP_LIBTORCH, - }, - { - // This is a catch-all for jobs that are viable but strict blocking - // Excluding linux-binary-* jobs because they are already grouped further up - regex: /(pull)|(trunk)/, - name: GROUP_OTHER_VIABLE_STRICT_BLOCKING, - }, -]; - -// Jobs on HUD home page will be sorted according to this list, with anything left off at the end -// Reorder elements in this list to reorder the groups on the HUD -const HUD_GROUP_SORTING = [ - GROUP_LINT, - GROUP_LINUX, - GROUP_WINDOWS, - GROUP_IOS, - GROUP_MAC, - GROUP_ROCM, - GROUP_XPU, - GROUP_XLA, - GROUP_OTHER_VIABLE_STRICT_BLOCKING, // placed after the last group that tends to have viable/strict blocking jobs - GROUP_VLLM, - GROUP_PARALLEL, - GROUP_LIBTORCH, - GROUP_ANDROID, - GROUP_BINARY_LINUX, - GROUP_DOCKER, - GROUP_CALC_DOCKER_IMAGE, - GROUP_CI_DOCKER_IMAGE_BUILDS, - GROUP_CI_CIRCLECI_PYTORCH_IOS, - GROUP_PERIODIC, - GROUP_SLOW, - GROUP_DOCS, - GROUP_INDUCTOR, - GROUP_INDUCTOR_PERIODIC, - GROUP_ANNOTATIONS_AND_LABELING, - GROUP_BINARY_WINDOWS, - GROUP_MEMORY_LEAK_CHECK, - GROUP_RERUN_DISABLED_TESTS, - // These two groups should always be at the end - GROUP_OTHER, - GROUP_UNSTABLE, -]; +import { IssueData } from "./types"; // Accepts a list of group names and returns that list sorted according to // the order defined in HUD_GROUP_SORTING -export function sortGroupNamesForHUD(groupNames: string[]): string[] { +export function sortGroupNamesForHUD( + groupNames: string[], + groupSettings: Group[] +): string[] { let result: string[] = []; - for (const group of HUD_GROUP_SORTING) { - if (groupNames.includes(group)) { - result.push(group); + for (const group of groupSettings.sort( + (a, b) => a.displayPriority - b.displayPriority + )) { + if (groupNames.includes(group.name)) { + result.push(group.name); } } @@ -209,35 +25,6 @@ export function sortGroupNamesForHUD(groupNames: string[]): string[] { return result; } -export function classifyGroup( - jobName: string, - showUnstableGroup: boolean, - unstableIssues?: IssueData[] -): string { - const openUnstableIssues = getOpenUnstableIssues(jobName, unstableIssues); - let assignedGroup = undefined; - for (const group of groups) { - if (jobName.match(group.regex)) { - assignedGroup = group; - break; - } - } - - // Check if the job has been marked as unstable but doesn't include the - // unstable keyword. - if (!showUnstableGroup && assignedGroup?.persistent) { - // If the unstable group is not being shown, then persistent groups (mem - // leak check, rerun disabled tests) should not be overwritten - return assignedGroup.name; - } - - if (openUnstableIssues !== undefined && openUnstableIssues.length !== 0) { - return GROUP_UNSTABLE; - } - - return assignedGroup === undefined ? GROUP_OTHER : assignedGroup.name; -} - export function getGroupConclusionChar(conclusion?: GroupedJobStatus): string { switch (conclusion) { case GroupedJobStatus.Success: @@ -351,56 +138,7 @@ export function getConclusionSeverityForSorting(conclusion?: string): number { } } -export function getGroupingData( - shaGrid: RowData[], - jobNames: Set, - showUnstableGroup: boolean, - unstableIssues?: IssueData[] -) { - // Construct Job Groupping Mapping - const groupNameMapping = new Map>(); // group -> [job names] - - // Track which jobs have failures - const jobsWithFailures = new Set(); - - // First pass: check failures for each job across all commits - for (const name of jobNames) { - // Check if this job has failures in any commit - const hasFailure = shaGrid.some((row) => { - const job = row.nameToJobs.get(name); - return job && isFailure(job.conclusion); - }); - - if (hasFailure) { - jobsWithFailures.add(name); - } - } - - // Second pass: group jobs - for (const name of jobNames) { - const groupName = classifyGroup(name, showUnstableGroup, unstableIssues); - const jobsInGroup = groupNameMapping.get(groupName) ?? []; - jobsInGroup.push(name); - groupNameMapping.set(groupName, jobsInGroup); - } - - // Calculate which groups have failures - const groupsWithFailures = new Set(); - for (const [groupName, jobs] of groupNameMapping.entries()) { - if (jobs.some((jobName) => jobsWithFailures.has(jobName))) { - groupsWithFailures.add(groupName); - } - } - - return { - shaGrid, - groupNameMapping, - jobsWithFailures, - groupsWithFailures, - }; -} - -export function isPersistentGroup(name: string) { +export function isPersistentGroup(groups: Group[], name: string) { return ( groups.filter((group) => group.name == name && group.persistent).length !== 0 diff --git a/torchci/package.json b/torchci/package.json index 891d35c0d7..cde01d2086 100644 --- a/torchci/package.json +++ b/torchci/package.json @@ -22,6 +22,8 @@ "@codemirror/state": "^0.20.0", "@codemirror/theme-one-dark": "^0.20.0", "@codemirror/view": "^0.20.7", + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", "@mui/icons-material": "^7.3.1", @@ -55,6 +57,7 @@ "pino-std-serializers": "^7.0.0", "probot": "^12.3.3", "react": "^18.3.1", + "react-dnd": "^16.0.1", "react-dom": "^18.3.1", "react-ga4": "^2.1.0", "react-icons": "^4.3.1", @@ -62,6 +65,8 @@ "react-markdown": "^8.0.3", "react-router-dom": "^6.26.2", "react-use-clipboard": "^1.0.8", + "react-vtree": "^2.0.4", + "react-window": "^1.8.10", "recharts": "^2.5.0", "shlex": "^2.1.0", "swr": "^2.2.5", diff --git a/torchci/pages/hud/[repoOwner]/[repoName]/[branch]/[[...page]].tsx b/torchci/pages/hud/[repoOwner]/[repoName]/[branch]/[[...page]].tsx index ff0c8f60c4..6e28e46d48 100644 --- a/torchci/pages/hud/[repoOwner]/[repoName]/[branch]/[[...page]].tsx +++ b/torchci/pages/hud/[repoOwner]/[repoName]/[branch]/[[...page]].tsx @@ -1,3 +1,4 @@ +import { Button } from "@mui/material"; import CheckBoxSelector from "components/common/CheckBoxSelector"; import CopyLink from "components/common/CopyLink"; import LoadingPage from "components/common/LoadingPage"; @@ -10,18 +11,15 @@ import { GroupHudTableHeader, passesGroupFilter, } from "components/hud/GroupHudTableHeaders"; +import { getGroupingData } from "components/HudGroupingSettings/hudGroupingSettings"; +import SettingsModal from "components/HudGroupingSettings/MainPageSettings"; import HudGroupedCell from "components/job/GroupJobConclusion"; import JobConclusion from "components/job/JobConclusion"; import JobFilterInput from "components/job/JobFilterInput"; import JobTooltip from "components/job/JobTooltip"; import SettingsPanel from "components/SettingsPanel"; import { fetcher } from "lib/GeneralUtils"; -import { - getGroupingData, - groups, - isUnstableGroup, - sortGroupNamesForHUD, -} from "lib/JobClassifierUtil"; +import { isUnstableGroup, sortGroupNamesForHUD } from "lib/JobClassifierUtil"; import { isFailedJob, isRerunDisabledTestsJob, @@ -195,9 +193,10 @@ function HudJobCells({ unstableIssues: IssueData[]; params: HudParams; }) { - let groupNames = groups.map((group) => group.name).concat("other"); const { expandedGroups, setExpandedGroups, groupNameMapping } = useContext(GroupingContext); + const groupNames = Array.from(groupNameMapping.keys()); + return ( <> {names.map((name) => { @@ -289,6 +288,7 @@ function FiltersAndSettings({}: {}) { const { jobFilter, handleSubmit } = useTableFilter(params); const [mergeEphemeralLF, setMergeEphemeralLF] = useContext(MergeLFContext); const [settingsPanelOpen, setSettingsPanelOpen] = useState(false); + const [groupingSettingsOpen, setGroupingSettingsOpen] = useState(false); const [hideUnstable, setHideUnstable] = usePreference("hideUnstable"); const [hideGreenColumns, setHideGreenColumns] = useHideGreenColumnsPreference(); @@ -344,6 +344,13 @@ function FiltersAndSettings({}: {}) { isOpen={settingsPanelOpen} onToggle={() => setSettingsPanelOpen(!settingsPanelOpen)} /> + + setGroupingSettingsOpen(false)} + /> ); } @@ -561,13 +568,20 @@ function GroupedHudTable({ params }: { params: HudParams }) { const [hideGreenColumns] = useHideGreenColumnsPreference(); const [useGrouping] = useGroupingPreference(params.nameFilter); - const { shaGrid, groupNameMapping, jobsWithFailures, groupsWithFailures } = - getGroupingData( - data ?? [], - jobNames, - (!useGrouping && hideUnstable) || (useGrouping && !hideUnstable), - unstableIssuesData ?? [] - ); + const { + shaGrid, + groupNameMapping, + jobsWithFailures, + groupsWithFailures, + groupSettings, + } = getGroupingData( + `${params.repoOwner}/${params.repoName}`, + params.branch, + data ?? [], + jobNames, + (!useGrouping && hideUnstable) || (useGrouping && !hideUnstable), + unstableIssuesData ?? [] + ); const [expandedGroups, setExpandedGroups] = useState(new Set()); @@ -580,7 +594,7 @@ function GroupedHudTable({ params }: { params: HudParams }) { }, [router, useGrouping]); const groupNames = Array.from(groupNameMapping.keys()); - let names = sortGroupNamesForHUD(groupNames); + let names = sortGroupNamesForHUD(groupNames, groupSettings); if (useGrouping) { expandedGroups.forEach((group) => { @@ -598,7 +612,7 @@ function GroupedHudTable({ params }: { params: HudParams }) { } } else { names = [...jobNames]; - groups.forEach((group) => { + groupSettings.forEach((group) => { if ( groupNames.includes(group.name) && (group.persistent || diff --git a/torchci/yarn.lock b/torchci/yarn.lock index 8356314535..375c28933e 100644 --- a/torchci/yarn.lock +++ b/torchci/yarn.lock @@ -1321,7 +1321,7 @@ core-js-pure "^3.20.2" regenerator-runtime "^0.13.4" -"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.16.3", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.13", "@babel/runtime@^7.24.6", "@babel/runtime@^7.27.6", "@babel/runtime@^7.28.2", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.7": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.0", "@babel/runtime@^7.12.5", "@babel/runtime@^7.16.3", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.13", "@babel/runtime@^7.24.6", "@babel/runtime@^7.27.6", "@babel/runtime@^7.28.2", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": version "7.26.10" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.10.tgz#a07b4d8fa27af131a633d7b3524db803eb4764c2" integrity sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw== @@ -1470,6 +1470,37 @@ style-mod "^4.0.0" w3c-keyname "^2.2.4" +"@dnd-kit/accessibility@^3.1.1": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz#3b4202bd6bb370a0730f6734867785919beac6af" + integrity sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw== + dependencies: + tslib "^2.0.0" + +"@dnd-kit/core@^6.3.1": + version "6.3.1" + resolved "https://registry.yarnpkg.com/@dnd-kit/core/-/core-6.3.1.tgz#4c36406a62c7baac499726f899935f93f0e6d003" + integrity sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ== + dependencies: + "@dnd-kit/accessibility" "^3.1.1" + "@dnd-kit/utilities" "^3.2.2" + tslib "^2.0.0" + +"@dnd-kit/sortable@^10.0.0": + version "10.0.0" + resolved "https://registry.yarnpkg.com/@dnd-kit/sortable/-/sortable-10.0.0.tgz#1f9382b90d835cd5c65d92824fa9dafb78c4c3e8" + integrity sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg== + dependencies: + "@dnd-kit/utilities" "^3.2.2" + tslib "^2.0.0" + +"@dnd-kit/utilities@^3.2.2": + version "3.2.2" + resolved "https://registry.yarnpkg.com/@dnd-kit/utilities/-/utilities-3.2.2.tgz#5a32b6af356dc5f74d61b37d6f7129a4040ced7b" + integrity sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg== + dependencies: + tslib "^2.0.0" + "@emnapi/runtime@^1.4.4": version "1.4.5" resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.4.5.tgz#c67710d0661070f38418b6474584f159de38aba9" @@ -2793,6 +2824,21 @@ readable-stream "^3.6.0" split2 "^4.0.0" +"@react-dnd/asap@^5.0.1": + version "5.0.2" + resolved "https://registry.yarnpkg.com/@react-dnd/asap/-/asap-5.0.2.tgz#1f81f124c1cd6f39511c11a881cfb0f715343488" + integrity sha512-WLyfoHvxhs0V9U+GTsGilGgf2QsPl6ZZ44fnv0/b8T3nQyvzxidxsg/ZltbWssbsRDlYW8UKSQMTGotuTotZ6A== + +"@react-dnd/invariant@^4.0.1": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@react-dnd/invariant/-/invariant-4.0.2.tgz#b92edffca10a26466643349fac7cdfb8799769df" + integrity sha512-xKCTqAK/FFauOM9Ta2pswIyT3D8AQlfrYdOi/toTPEhqCuAs1v5tcJ3Y08Izh1cJ5Jchwy9SeAXmMg6zrKs2iw== + +"@react-dnd/shallowequal@^4.0.1": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@react-dnd/shallowequal/-/shallowequal-4.0.2.tgz#d1b4befa423f692fa4abf1c79209702e7d8ae4b4" + integrity sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA== + "@remix-run/router@1.19.2": version "1.19.2" resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.19.2.tgz#0c896535473291cb41f152c180bedd5680a3b273" @@ -4748,6 +4794,15 @@ dir-glob@^3.0.1: dependencies: path-type "^4.0.0" +dnd-core@^16.0.1: + version "16.0.1" + resolved "https://registry.yarnpkg.com/dnd-core/-/dnd-core-16.0.1.tgz#a1c213ed08961f6bd1959a28bb76f1a868360d19" + integrity sha512-HK294sl7tbw6F6IeuK16YSBUoorvHpY8RHO+9yFfaJyCDVb6n7PRcezrOEOa2SBCqiYpemh5Jx20ZcjKdFAVng== + dependencies: + "@react-dnd/asap" "^5.0.1" + "@react-dnd/invariant" "^4.0.1" + redux "^4.2.0" + doctrine@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz" @@ -5863,7 +5918,7 @@ hast-util-whitespace@^2.0.0: resolved "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-2.0.0.tgz" integrity sha512-Pkw+xBHuV6xFeJprJe2BBEoDV+AvQySaz3pPDRUs5PNZEMQjpXJJueqrpcHIXxnWTcAGi/UOCgVShlkY6kLoqg== -hoist-non-react-statics@^3.3.1: +hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2: version "3.3.2" resolved "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz" integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== @@ -6981,6 +7036,11 @@ media-typer@0.3.0: resolved "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz" integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== +"memoize-one@>=3.1.1 <6": + version "5.2.1" + resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e" + integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q== + merge-descriptors@1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz#d80319a65f3c7935351e5cfdac8f9318504dbed5" @@ -8032,6 +8092,17 @@ raw-body@2.5.2: iconv-lite "0.4.24" unpipe "1.0.0" +react-dnd@^16.0.1: + version "16.0.1" + resolved "https://registry.yarnpkg.com/react-dnd/-/react-dnd-16.0.1.tgz#2442a3ec67892c60d40a1559eef45498ba26fa37" + integrity sha512-QeoM/i73HHu2XF9aKksIUuamHPDvRglEwdHL4jsp784BgUuWcg6mzfxT0QDdQz8Wj0qyRKx2eMg8iZtWvU4E2Q== + dependencies: + "@react-dnd/invariant" "^4.0.1" + "@react-dnd/shallowequal" "^4.0.1" + dnd-core "^16.0.1" + fast-deep-equal "^3.1.3" + hoist-non-react-statics "^3.3.2" + react-dom@^18.3.1: version "18.3.1" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.3.1.tgz#c2265d79511b57d479b3dd3fdfa51536494c5cb4" @@ -8153,6 +8224,21 @@ react-use-clipboard@^1.0.8: dependencies: copy-to-clipboard "^3.3.1" +react-vtree@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/react-vtree/-/react-vtree-2.0.4.tgz#340e64255f5f4ec6f4c35dc44a7036f7fcd98bc5" + integrity sha512-UOld0VqyAZrryF06K753X4bcEVN6/wW831exvVlMZeZAVHk9KXnlHs4rpqDAeoiBgUwJqoW/rtn0hwsokRRxPA== + dependencies: + "@babel/runtime" "^7.11.0" + +react-window@^1.8.10: + version "1.8.10" + resolved "https://registry.yarnpkg.com/react-window/-/react-window-1.8.10.tgz#9e6b08548316814b443f7002b1cf8fd3a1bdde03" + integrity sha512-Y0Cx+dnU6NLa5/EvoHukUD0BklJ8qITCtVEPY1C/nL8wwoZ0b5aEw8Ff1dOVHw7fCzMt55XfJDd8S8W8LCaUCg== + dependencies: + "@babel/runtime" "^7.0.0" + memoize-one ">=3.1.1 <6" + react@^18.3.1: version "18.3.1" resolved "https://registry.yarnpkg.com/react/-/react-18.3.1.tgz#49ab892009c53933625bd16b2533fc754cab2891" @@ -8216,6 +8302,13 @@ reduce-css-calc@^2.1.8: css-unit-converter "^1.1.1" postcss-value-parser "^3.3.0" +redux@^4.2.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.1.tgz#c08f4306826c49b5e9dc901dee0452ea8fce6197" + integrity sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w== + dependencies: + "@babel/runtime" "^7.9.2" + regenerator-runtime@^0.13.4: version "0.13.11" resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz" @@ -8924,6 +9017,11 @@ tslib@^1.11.1, tslib@^1.8.1, tslib@^1.9.3: resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== +tslib@^2.0.0, tslib@^2.8.0: + version "2.8.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" + integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== + tslib@^2.3.1, tslib@^2.5.0: version "2.5.3" resolved "https://registry.npmjs.org/tslib/-/tslib-2.5.3.tgz" @@ -8934,11 +9032,6 @@ tslib@^2.4.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.0.tgz#d124c86c3c05a40a91e6fdea4021bd31d377971b" integrity sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA== -tslib@^2.8.0: - version "2.8.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" - integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== - tsutils@^3.21.0: version "3.21.0" resolved "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz"