Skip to content

feat: Add Migration Wave Planner Tab#4

Draft
amenocal wants to merge 2 commits intomainfrom
amenocal/wave-planner
Draft

feat: Add Migration Wave Planner Tab#4
amenocal wants to merge 2 commits intomainfrom
amenocal/wave-planner

Conversation

@amenocal
Copy link

@amenocal amenocal commented Jan 2, 2026

This pull request introduces a tabbed navigation system to the Dashboard component, allowing users to switch between the existing overview and a newly added Migration Wave Planner. It also adds the necessary styles and type definitions to support the new planner feature. The main changes are grouped below:

Dashboard Feature Enhancements:

  • Added tab navigation to the Dashboard component, enabling users to switch between "Overview" and "Migration Wave Planner" views using new state and conditional rendering logic. [1] [2] [3] [4] [5]
  • Integrated the new MigrationWaveAnalyzer component, which is shown in the "Migration Wave Planner" tab and receives repository data for analysis. [1] [2]

Styling Improvements:

  • Added new styles in src/styles.ts for tab navigation (tabContainerStyle, tabButtonStyle, activeTabStyle) and for input controls used in the migration planner (inputLabelStyle, inputStyle, inputGroupStyle, inputFieldStyle).

Type Definitions for Migration Wave Analysis:

  • Defined new types in src/types.ts to support migration wave analysis, including ClassificationThresholds, SizeCategory, MigrationRepository, MigrationWave, WaveStats, and MigrationWaveResult, with detailed documentation for each.

Screenshots

image image

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a tabbed navigation system to the Dashboard component, introducing a new "Migration Wave Planner" feature alongside the existing repository overview. The planner helps organize repository migrations by calculating optimal waves based on repository size and metadata complexity.

Key Changes:

  • Added tab-based navigation to toggle between Overview and Migration Wave Planner views
  • Implemented migration wave calculation algorithm with configurable thresholds for repository classification
  • Created interactive UI for threshold configuration and wave visualization with expandable details

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
src/components/Dashboard.tsx Added tab navigation state and conditional rendering for Overview vs Migration Planner views
src/components/Dashboard/MigrationWaveAnalyzer.tsx New component providing threshold controls, wave statistics, and interactive wave breakdown by organization
src/utils/calculateMigrationWaves.ts New utility implementing wave generation algorithm with repository classification and organization grouping logic
src/types.ts Added type definitions for migration wave analysis (thresholds, categories, repositories, waves, and statistics)
src/styles.ts Added styling for tab navigation and form input controls

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.


// Process each organization
orgGroups.forEach((repos, orgName) => {
if (repos.length >= 50) {
Copy link

Copilot AI Jan 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hardcoded magic number 50 appears twice in the code (line 374 and line 597 in the UI description). This threshold for determining whether an organization gets dedicated waves should be extracted as a named constant at the module level for better maintainability. For example, define const ORG_DEDICATED_WAVES_THRESHOLD = 50; near the top of the file with DEFAULT_THRESHOLDS.

Copilot uses AI. Check for mistakes.
min={min || 0}
style={{
...inputStyle,
borderColor: error ? '#f85149' : undefined,
Copy link

Copilot AI Jan 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hardcoded hex color values for error states ('#f85149') and other UI elements are duplicated across multiple locations. These should reference theme colors for consistency with the rest of the application. Check if these colors already exist in the theme object, or add them if they represent standard semantic colors (e.g., error, warning).

Copilot uses AI. Check for mistakes.
fix: tab navigation with keyboard support and accessibility improvements

feat: remove constants and adds advanced settings and dynamic sizing
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 12 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +160 to +200
<>
{/* Organization Stats Section */}
{hasOrgData && (
<>
<div style={separatorStyle} />
<SummaryHeader
title="Organization Statistics"
description={
selectedOrg === "all"
? `Viewing ${stats.orgs!.length} organizations`
: undefined
}
/>
<OrgStatsSection
orgs={stats.orgs!}
selectedOrg={selectedOrg}
/>
</>
)}

{/* Repository Stats Section */}
<div style={separatorStyle} />
<SummaryHeader
title="Organization Statistics"
description={
selectedOrg === "all"
? `Viewing ${stats.orgs!.length} organizations`
: undefined
}
title="Repository Statistics"
description={`Analysis of ${filteredStats.basic.totalRepos.toLocaleString()} repositories${selectedOrg === "all" ? ` across ${filteredStats.orgData.length} organizations` : ""}`}
/>
<OrgStatsSection
orgs={stats.orgs!}
selectedOrg={selectedOrg}
/>
</>
<DashboardSection stats={filteredStats} />
</>
</div>
) : (
<div
id="migration-panel"
role="tabpanel"
aria-labelledby="migration-tab"
>
<>
{/* Migration Wave Planner Section */}
<div style={separatorStyle} />
<MigrationWaveAnalyzer repositories={filteredStats.repositories} />
</>
</div>
Copy link

Copilot AI Jan 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are unnecessary React fragments wrapping the tab panel contents. The outer div already serves as the container, so the inner fragments on lines 160 and 195 don't provide any value and should be removed for cleaner code.

Copilot uses AI. Check for mistakes.
Comment on lines +556 to +560
<div style={{ marginTop: '16px', fontSize: 13, color: '#8b949e' }}>
<strong>Wave Sizes:</strong> Number of repos per wave for each size category. Smaller waves = more careful processing.
<br />
<strong>Org Threshold:</strong> Organizations with ≥ this many repos get dedicated waves instead of being mixed.
</div>
Copy link

Copilot AI Jan 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The inline style on line 556 uses a hardcoded color '#8b949e' instead of using the theme constant theme.colors.subtle. For consistency with the rest of the codebase and easier maintainability, use the theme constant.

Copilot uses AI. Check for mistakes.
const totalSize = wave.repos.reduce((sum, repo) => sum + repo.sizeMB, 0);
const totalMetadata = wave.repos.reduce((sum, repo) => sum + repo.metadataRecords, 0);
const avgSize = totalSize / wave.repos.length;
const avgMetadata = totalMetadata / wave.repos.length;
Copy link

Copilot AI Jan 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variable 'avgMetadata' is calculated but never used in the component. It's computed on line 137 but doesn't appear anywhere in the rendered output. Consider removing this unused calculation or adding it to the display if it was intended to be shown.

Suggested change
const avgMetadata = totalMetadata / wave.repos.length;

Copilot uses AI. Check for mistakes.
Comment on lines +87 to +88
// Only update if value is valid (positive number)
if (!isNaN(newValue) && newValue >= 0) {
Copy link

Copilot AI Jan 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The onChange handler allows any non-negative number including 0, but the min parameter suggests 0 might not be valid. The handleBlur ensures the minimum is enforced, but during typing, users can enter 0 which may cause unexpected behavior in the migration wave calculations. Consider validating against the min value in handleChange as well, not just handleBlur.

Suggested change
// Only update if value is valid (positive number)
if (!isNaN(newValue) && newValue >= 0) {
// Only update if value is valid and respects the minimum
if (!isNaN(newValue) && newValue >= min) {

Copilot uses AI. Check for mistakes.
borderColor: error ? theme.colors.error : undefined,
}}
aria-invalid={error}
aria-describedby={error ? `${label}-error` : undefined}
Copy link

Copilot AI Jan 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The aria-describedby attribute references an id that doesn't exist. When there's an error, it points to ${label}-error but no element with this id is rendered. This breaks the accessibility contract and screen reader users won't receive proper error announcements. The error message should either have a matching id or the aria-describedby should reference the existing error element with id "threshold-error".

Suggested change
aria-describedby={error ? `${label}-error` : undefined}
aria-describedby={error ? 'threshold-error' : undefined}

Copilot uses AI. Check for mistakes.
Comment on lines +487 to +493
<div style={{ marginTop: '16px', fontSize: 13, color: '#8b949e' }}>
<strong>Wave Sizing:</strong> Small repos ({WAVE_SIZE_SMALL}/wave), Medium repos ({WAVE_SIZE_MEDIUM}/wave), Large repos ({WAVE_SIZE_LARGE}/wave)
<br />
<strong>Classification:</strong> Small: &lt;{thresholds.smallSizeMB}MB AND &lt;{thresholds.smallMetadata.toLocaleString()} metadata |
Large: ≥{thresholds.largeSizeMB}MB OR ≥{thresholds.largeMetadata.toLocaleString()} metadata |
Medium: Everything else
</div>
Copy link

Copilot AI Jan 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The inline style on line 487 uses a hardcoded color '#8b949e' instead of using the theme constant theme.colors.subtle which has the same value. For consistency with the rest of the codebase and easier maintainability, use the theme constant.

Copilot uses AI. Check for mistakes.
Comment on lines +733 to +739
<ul style={{ color: '#8b949e', fontSize: 14, lineHeight: 1.6, margin: 0, paddingLeft: '20px' }}>
<li><strong>Organization-first:</strong> Each organization with ≥{orgThreshold} repos gets dedicated waves</li>
<li><strong>Size-optimized waves:</strong> Wave size depends on repository complexity ({waveSizeSmall}/{waveSizeMedium}/{waveSizeLarge})</li>
<li><strong>Small repositories first:</strong> Process simple repos before complex ones</li>
<li><strong>Queue-aware sizing:</strong> Smaller waves for larger repositories to manage load</li>
<li><strong>Mixed small orgs:</strong> Organizations with &lt;{orgThreshold} repos are batched together efficiently</li>
</ul>
Copy link

Copilot AI Jan 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The inline style on line 733 uses a hardcoded color '#8b949e' instead of using the theme constant theme.colors.subtle. For consistency with the rest of the codebase and easier maintainability, use the theme constant.

Copilot uses AI. Check for mistakes.
Comment on lines +43 to +53
* Field mapping documentation for metadata record calculation.
*
* - issues
* - pullRequests
* - commitComments
* - milestones
* - releases
* - tags
* - discussions
* - issueEvents
*
Copy link

Copilot AI Jan 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The field mapping documentation comment describes a different set of fields than what's actually used in the calculateMetadataRecords function. The comment lists generic field names like "issues", "pullRequests", "commitComments" etc., but the actual implementation uses specific property names like "Issue_Count", "Pull_Request_Count", "Commit_Comment_Count". This documentation is misleading and should be updated to match the actual Repository type properties or removed if not providing value.

Suggested change
* Field mapping documentation for metadata record calculation.
*
* - issues
* - pullRequests
* - commitComments
* - milestones
* - releases
* - tags
* - discussions
* - issueEvents
*
* Fields included in the metadata record calculation.
*
* - PR_Review_Count
* - Issue_Count
* - Pull_Request_Count
* - PR_Review_Comment_Count
* - Commit_Comment_Count
* - Issue_Comment_Count
* - Issue_Event_Count
* - Release_Count
* - Milestone_Count
* - Tag_Count
* - Discussion_Count

Copilot uses AI. Check for mistakes.
Comment on lines +56 to 65
// Focus management: move focus to active tab after keyboard navigation
useEffect(() => {
// Only focus if the tab change wasn't triggered by a click (which already has focus)
// We use a small timeout to ensure the ref is updated after state change
const timeoutId = setTimeout(() => {
tabRefs.current[activeTab]?.focus();
}, 0);
return () => clearTimeout(timeoutId);
}, [activeTab]);

Copy link

Copilot AI Jan 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The focus management logic in the useEffect will trigger on every tab change, including clicks. This can cause unwanted focus shifts and may interrupt user interaction. The comment mentions checking if the change wasn't triggered by a click, but there's no actual implementation to distinguish between click-triggered and keyboard-triggered tab changes. Consider tracking the interaction type or removing this useEffect entirely since clicking a button already focuses it naturally.

Suggested change
// Focus management: move focus to active tab after keyboard navigation
useEffect(() => {
// Only focus if the tab change wasn't triggered by a click (which already has focus)
// We use a small timeout to ensure the ref is updated after state change
const timeoutId = setTimeout(() => {
tabRefs.current[activeTab]?.focus();
}, 0);
return () => clearTimeout(timeoutId);
}, [activeTab]);

Copilot uses AI. Check for mistakes.

const totalSize = wave.repos.reduce((sum, repo) => sum + repo.sizeMB, 0);
const totalMetadata = wave.repos.reduce((sum, repo) => sum + repo.metadataRecords, 0);
const avgSize = totalSize / wave.repos.length;
Copy link

Copilot AI Jan 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variable 'avgSize' is calculated but never used in the component. It's computed on line 136 but doesn't appear anywhere in the rendered output. Consider removing this unused calculation or adding it to the display if it was intended to be shown.

Suggested change
const avgSize = totalSize / wave.repos.length;

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants