Conversation
There was a problem hiding this comment.
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.
src/utils/calculateMigrationWaves.ts
Outdated
|
|
||
| // Process each organization | ||
| orgGroups.forEach((repos, orgName) => { | ||
| if (repos.length >= 50) { |
There was a problem hiding this comment.
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.
| min={min || 0} | ||
| style={{ | ||
| ...inputStyle, | ||
| borderColor: error ? '#f85149' : undefined, |
There was a problem hiding this comment.
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).
fix: tab navigation with keyboard support and accessibility improvements feat: remove constants and adds advanced settings and dynamic sizing
There was a problem hiding this comment.
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.
| <> | ||
| {/* 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> |
There was a problem hiding this comment.
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.
| <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> |
There was a problem hiding this comment.
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.
| 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; |
There was a problem hiding this comment.
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.
| const avgMetadata = totalMetadata / wave.repos.length; |
| // Only update if value is valid (positive number) | ||
| if (!isNaN(newValue) && newValue >= 0) { |
There was a problem hiding this comment.
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.
| // 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) { |
| borderColor: error ? theme.colors.error : undefined, | ||
| }} | ||
| aria-invalid={error} | ||
| aria-describedby={error ? `${label}-error` : undefined} |
There was a problem hiding this comment.
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".
| aria-describedby={error ? `${label}-error` : undefined} | |
| aria-describedby={error ? 'threshold-error' : undefined} |
| <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: <{thresholds.smallSizeMB}MB AND <{thresholds.smallMetadata.toLocaleString()} metadata | | ||
| Large: ≥{thresholds.largeSizeMB}MB OR ≥{thresholds.largeMetadata.toLocaleString()} metadata | | ||
| Medium: Everything else | ||
| </div> |
There was a problem hiding this comment.
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.
| <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 <{orgThreshold} repos are batched together efficiently</li> | ||
| </ul> |
There was a problem hiding this comment.
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.
| * Field mapping documentation for metadata record calculation. | ||
| * | ||
| * - issues | ||
| * - pullRequests | ||
| * - commitComments | ||
| * - milestones | ||
| * - releases | ||
| * - tags | ||
| * - discussions | ||
| * - issueEvents | ||
| * |
There was a problem hiding this comment.
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.
| * 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 |
| // 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]); | ||
|
|
There was a problem hiding this comment.
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.
| // 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]); |
|
|
||
| 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; |
There was a problem hiding this comment.
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.
| const avgSize = totalSize / wave.repos.length; |
This pull request introduces a tabbed navigation system to the
Dashboardcomponent, 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:
Dashboardcomponent, enabling users to switch between "Overview" and "Migration Wave Planner" views using new state and conditional rendering logic. [1] [2] [3] [4] [5]MigrationWaveAnalyzercomponent, which is shown in the "Migration Wave Planner" tab and receives repository data for analysis. [1] [2]Styling Improvements:
src/styles.tsfor tab navigation (tabContainerStyle,tabButtonStyle,activeTabStyle) and for input controls used in the migration planner (inputLabelStyle,inputStyle,inputGroupStyle,inputFieldStyle).Type Definitions for Migration Wave Analysis:
src/types.tsto support migration wave analysis, includingClassificationThresholds,SizeCategory,MigrationRepository,MigrationWave,WaveStats, andMigrationWaveResult, with detailed documentation for each.Screenshots