The implementation is complete. Here's a summary of the changes made: (#1871)
## Summary
I've implemented the GitHub PR status display on `WorkspaceSummary.tsx`. The changes include:
### Backend Changes
1. **`crates/db/src/models/merge.rs`** - Added a new method `get_latest_pr_status_for_workspaces()` that efficiently queries the latest PR status for all workspaces grouped by archived status.
2. **`crates/server/src/routes/task_attempts/workspace_summary.rs`** - Added `pr_status: Option<MergeStatus>` field to the `WorkspaceSummary` struct and included the PR status query in the summary endpoint.
### Frontend Changes
3. **`frontend/src/components/ui-new/hooks/useWorkspaces.ts`** - Added `prStatus` to the `SidebarWorkspace` interface and mapped it in `toSidebarWorkspace()`.
4. **`frontend/src/components/ui-new/primitives/WorkspaceSummary.tsx`** - Added:
- Import for `GitPullRequestIcon`
- `prStatus` prop to the component
- Display logic showing:
- **PR open**: `GitPullRequestIcon` with `text-brand` (orange) color
- **PR merged**: `GitPullRequestIcon` with `text-success` (green) color
- **No PR/closed/unknown**: No icon displayed
5. **`frontend/src/components/ui-new/views/WorkspacesSidebar.tsx`** - Passed the `prStatus` prop to both active and archived workspace summaries.
### Generated Types
6. **`shared/types.ts`** - Auto-generated to include the new `pr_status` field in `WorkspaceSummary`.
This commit is contained in:
committed by
GitHub
parent
b743f849f7
commit
af70dd9239
@@ -1,3 +1,5 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::{FromRow, SqlitePool, Type};
|
use sqlx::{FromRow, SqlitePool, Type};
|
||||||
@@ -288,6 +290,45 @@ impl Merge {
|
|||||||
|
|
||||||
Ok(rows.into_iter().map(Into::into).collect())
|
Ok(rows.into_iter().map(Into::into).collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the latest PR status for each workspace (for workspace summaries)
|
||||||
|
/// Returns a map of workspace_id -> MergeStatus for workspaces that have PRs
|
||||||
|
pub async fn get_latest_pr_status_for_workspaces(
|
||||||
|
pool: &SqlitePool,
|
||||||
|
archived: bool,
|
||||||
|
) -> Result<HashMap<Uuid, MergeStatus>, sqlx::Error> {
|
||||||
|
#[derive(FromRow)]
|
||||||
|
struct PrStatusRow {
|
||||||
|
workspace_id: Uuid,
|
||||||
|
pr_status: Option<MergeStatus>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the latest PR for each workspace by using a subquery to find the max created_at
|
||||||
|
// Only consider PR merges (not direct merges)
|
||||||
|
let rows = sqlx::query_as::<_, PrStatusRow>(
|
||||||
|
r#"SELECT
|
||||||
|
m.workspace_id,
|
||||||
|
m.pr_status
|
||||||
|
FROM merges m
|
||||||
|
INNER JOIN (
|
||||||
|
SELECT workspace_id, MAX(created_at) as max_created_at
|
||||||
|
FROM merges
|
||||||
|
WHERE merge_type = 'pr'
|
||||||
|
GROUP BY workspace_id
|
||||||
|
) latest ON m.workspace_id = latest.workspace_id
|
||||||
|
AND m.created_at = latest.max_created_at
|
||||||
|
INNER JOIN workspaces w ON m.workspace_id = w.id
|
||||||
|
WHERE m.merge_type = 'pr' AND w.archived = $1"#,
|
||||||
|
)
|
||||||
|
.bind(archived)
|
||||||
|
.fetch_all(pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(rows
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|row| row.pr_status.map(|status| (row.workspace_id, status)))
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Conversion implementations
|
// Conversion implementations
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ use axum::{Json, extract::State, response::Json as ResponseJson};
|
|||||||
use db::models::{
|
use db::models::{
|
||||||
coding_agent_turn::CodingAgentTurn,
|
coding_agent_turn::CodingAgentTurn,
|
||||||
execution_process::{ExecutionProcess, ExecutionProcessStatus},
|
execution_process::{ExecutionProcess, ExecutionProcessStatus},
|
||||||
|
merge::{Merge, MergeStatus},
|
||||||
workspace::Workspace,
|
workspace::Workspace,
|
||||||
workspace_repo::WorkspaceRepo,
|
workspace_repo::WorkspaceRepo,
|
||||||
};
|
};
|
||||||
@@ -45,6 +46,8 @@ pub struct WorkspaceSummary {
|
|||||||
pub has_running_dev_server: bool,
|
pub has_running_dev_server: bool,
|
||||||
/// Does this workspace have unseen coding agent turns?
|
/// Does this workspace have unseen coding agent turns?
|
||||||
pub has_unseen_turns: bool,
|
pub has_unseen_turns: bool,
|
||||||
|
/// PR status for this workspace (if any PR exists)
|
||||||
|
pub pr_status: Option<MergeStatus>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Response containing summaries for requested workspaces
|
/// Response containing summaries for requested workspaces
|
||||||
@@ -103,7 +106,10 @@ pub async fn get_workspace_summaries(
|
|||||||
// 5. Check which workspaces have unseen coding agent turns
|
// 5. Check which workspaces have unseen coding agent turns
|
||||||
let unseen_workspaces = CodingAgentTurn::find_workspaces_with_unseen(pool, archived).await?;
|
let unseen_workspaces = CodingAgentTurn::find_workspaces_with_unseen(pool, archived).await?;
|
||||||
|
|
||||||
// 6. Compute diff stats for each workspace (in parallel)
|
// 6. Get PR status for each workspace
|
||||||
|
let pr_statuses = Merge::get_latest_pr_status_for_workspaces(pool, archived).await?;
|
||||||
|
|
||||||
|
// 7. Compute diff stats for each workspace (in parallel)
|
||||||
let diff_futures: Vec<_> = workspaces
|
let diff_futures: Vec<_> = workspaces
|
||||||
.iter()
|
.iter()
|
||||||
.map(|ws| {
|
.map(|ws| {
|
||||||
@@ -126,7 +132,7 @@ pub async fn get_workspace_summaries(
|
|||||||
futures_util::future::join_all(diff_futures).await;
|
futures_util::future::join_all(diff_futures).await;
|
||||||
let diff_stats: HashMap<Uuid, DiffStats> = diff_results.into_iter().flatten().collect();
|
let diff_stats: HashMap<Uuid, DiffStats> = diff_results.into_iter().flatten().collect();
|
||||||
|
|
||||||
// 7. Assemble response
|
// 8. Assemble response
|
||||||
let summaries: Vec<WorkspaceSummary> = workspaces
|
let summaries: Vec<WorkspaceSummary> = workspaces
|
||||||
.iter()
|
.iter()
|
||||||
.map(|ws| {
|
.map(|ws| {
|
||||||
@@ -148,6 +154,7 @@ pub async fn get_workspace_summaries(
|
|||||||
latest_process_status: latest.map(|p| p.status.clone()),
|
latest_process_status: latest.map(|p| p.status.clone()),
|
||||||
has_running_dev_server: dev_server_workspaces.contains(&id),
|
has_running_dev_server: dev_server_workspaces.contains(&id),
|
||||||
has_unseen_turns: unseen_workspaces.contains(&id),
|
has_unseen_turns: unseen_workspaces.contains(&id),
|
||||||
|
pr_status: pr_statuses.get(&id).cloned(),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export interface SidebarWorkspace {
|
|||||||
hasUnseenActivity?: boolean;
|
hasUnseenActivity?: boolean;
|
||||||
latestProcessCompletedAt?: string;
|
latestProcessCompletedAt?: string;
|
||||||
latestProcessStatus?: 'running' | 'completed' | 'failed' | 'killed';
|
latestProcessStatus?: 'running' | 'completed' | 'failed' | 'killed';
|
||||||
|
prStatus?: 'open' | 'merged' | 'closed' | 'unknown';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keep the old export name for backwards compatibility
|
// Keep the old export name for backwards compatibility
|
||||||
@@ -67,6 +68,7 @@ function toSidebarWorkspace(
|
|||||||
hasUnseenActivity: summary?.has_unseen_turns,
|
hasUnseenActivity: summary?.has_unseen_turns,
|
||||||
latestProcessCompletedAt: summary?.latest_process_completed_at ?? undefined,
|
latestProcessCompletedAt: summary?.latest_process_completed_at ?? undefined,
|
||||||
latestProcessStatus: summary?.latest_process_status ?? undefined,
|
latestProcessStatus: summary?.latest_process_status ?? undefined,
|
||||||
|
prStatus: summary?.pr_status ?? undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
PlayIcon,
|
PlayIcon,
|
||||||
FileIcon,
|
FileIcon,
|
||||||
CircleIcon,
|
CircleIcon,
|
||||||
|
GitPullRequestIcon,
|
||||||
} from '@phosphor-icons/react';
|
} from '@phosphor-icons/react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
@@ -27,6 +28,7 @@ interface WorkspaceSummaryProps {
|
|||||||
hasUnseenActivity?: boolean;
|
hasUnseenActivity?: boolean;
|
||||||
latestProcessCompletedAt?: string;
|
latestProcessCompletedAt?: string;
|
||||||
latestProcessStatus?: 'running' | 'completed' | 'failed' | 'killed';
|
latestProcessStatus?: 'running' | 'completed' | 'failed' | 'killed';
|
||||||
|
prStatus?: 'open' | 'merged' | 'closed' | 'unknown';
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
summary?: boolean;
|
summary?: boolean;
|
||||||
@@ -48,6 +50,7 @@ export function WorkspaceSummary({
|
|||||||
hasUnseenActivity = false,
|
hasUnseenActivity = false,
|
||||||
latestProcessCompletedAt,
|
latestProcessCompletedAt,
|
||||||
latestProcessStatus,
|
latestProcessStatus,
|
||||||
|
prStatus,
|
||||||
onClick,
|
onClick,
|
||||||
className,
|
className,
|
||||||
summary = false,
|
summary = false,
|
||||||
@@ -120,6 +123,20 @@ export function WorkspaceSummary({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* PR status icon */}
|
||||||
|
{prStatus === 'open' && (
|
||||||
|
<GitPullRequestIcon
|
||||||
|
className="size-icon-xs text-brand shrink-0"
|
||||||
|
weight="fill"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{prStatus === 'merged' && (
|
||||||
|
<GitPullRequestIcon
|
||||||
|
className="size-icon-xs text-success shrink-0"
|
||||||
|
weight="fill"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Pin icon */}
|
{/* Pin icon */}
|
||||||
{isPinned && (
|
{isPinned && (
|
||||||
<PushPinIcon
|
<PushPinIcon
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ export function WorkspacesSidebar({
|
|||||||
hasUnseenActivity={workspace.hasUnseenActivity}
|
hasUnseenActivity={workspace.hasUnseenActivity}
|
||||||
latestProcessCompletedAt={workspace.latestProcessCompletedAt}
|
latestProcessCompletedAt={workspace.latestProcessCompletedAt}
|
||||||
latestProcessStatus={workspace.latestProcessStatus}
|
latestProcessStatus={workspace.latestProcessStatus}
|
||||||
|
prStatus={workspace.prStatus}
|
||||||
onClick={() => onSelectWorkspace(workspace.id)}
|
onClick={() => onSelectWorkspace(workspace.id)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@@ -124,6 +125,7 @@ export function WorkspacesSidebar({
|
|||||||
hasUnseenActivity={workspace.hasUnseenActivity}
|
hasUnseenActivity={workspace.hasUnseenActivity}
|
||||||
latestProcessCompletedAt={workspace.latestProcessCompletedAt}
|
latestProcessCompletedAt={workspace.latestProcessCompletedAt}
|
||||||
latestProcessStatus={workspace.latestProcessStatus}
|
latestProcessStatus={workspace.latestProcessStatus}
|
||||||
|
prStatus={workspace.prStatus}
|
||||||
onClick={() => onSelectWorkspace(workspace.id)}
|
onClick={() => onSelectWorkspace(workspace.id)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -352,7 +352,11 @@ has_running_dev_server: boolean,
|
|||||||
/**
|
/**
|
||||||
* Does this workspace have unseen coding agent turns?
|
* Does this workspace have unseen coding agent turns?
|
||||||
*/
|
*/
|
||||||
has_unseen_turns: boolean, };
|
has_unseen_turns: boolean,
|
||||||
|
/**
|
||||||
|
* PR status for this workspace (if any PR exists)
|
||||||
|
*/
|
||||||
|
pr_status: MergeStatus | null, };
|
||||||
|
|
||||||
export type WorkspaceSummaryResponse = { summaries: Array<WorkspaceSummary>, };
|
export type WorkspaceSummaryResponse = { summaries: Array<WorkspaceSummary>, };
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user