Skip to content

Commit 4d7eb4f

Browse files
sallyomclaude
andcommitted
feat(frontend): Match RFE workflow UI pattern in BugFix workspaces
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: sallyom <somalley@redhat.com>
1 parent c01c0a2 commit 4d7eb4f

File tree

7 files changed

+598
-349
lines changed

7 files changed

+598
-349
lines changed

components/backend/handlers/bugfix/jira_sync.go

Lines changed: 42 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -249,37 +249,23 @@ func SyncProjectBugFixWorkflowToJira(c *gin.Context) {
249249
}
250250
}
251251

252-
// Also post GitHub comment for updates (not just creates)
253-
if isUpdate {
254-
userID, _ := c.Get("userID")
255-
userIDStr, _ := userID.(string)
256-
githubToken, err := git.GetGitHubToken(c.Request.Context(), reqK8s, reqDyn, project, userIDStr)
257-
if err == nil && githubToken != "" {
258-
owner, repo, issueNumber, err := github.ParseGitHubIssueURL(workflow.GithubIssueURL)
259-
if err == nil {
260-
comment := formatGitHubJiraUpdateComment(jiraTaskKey, jiraTaskURL, workflow)
261-
ctx := context.Background()
262-
_, err = github.AddComment(ctx, owner, repo, issueNumber, githubToken, comment)
263-
if err != nil {
264-
// Non-fatal: Log but continue
265-
fmt.Printf("Warning: Failed to add Jira update to GitHub Issue: %v\n", err)
266-
} else {
267-
fmt.Printf("Posted Jira update comment to GitHub Issue #%d\n", issueNumber)
268-
}
269-
}
270-
}
271-
}
252+
// Note: Only post GitHub comment on initial creation, not on updates
253+
// This prevents spamming the GitHub Issue with repeated sync comments
272254

273255
// T055: Update BugFixWorkflow CR with jiraTaskKey, jiraTaskURL, and lastSyncedAt
256+
// Use backend service account client for CR write (following handlers/sessions.go:417 pattern)
274257
workflow.JiraTaskKey = &jiraTaskKey
275258
workflow.JiraTaskURL = &jiraTaskURL
276259
syncedAt := time.Now().UTC().Format(time.RFC3339)
277260
workflow.LastSyncedAt = &syncedAt
278261

279-
err = crd.UpsertProjectBugFixWorkflowCR(reqDyn, workflow)
262+
serviceAccountClient := GetServiceAccountDynamicClient()
263+
err = crd.UpsertProjectBugFixWorkflowCR(serviceAccountClient, workflow)
280264
if err != nil {
281-
// Try to continue even if CR update fails
265+
// Log error and continue - Jira sync itself succeeded
282266
fmt.Printf("Warning: Failed to update workflow CR with Jira info: %v\n", err)
267+
} else {
268+
fmt.Printf("Successfully updated workflow CR with Jira info: %s -> %s\n", workflowID, jiraTaskKey)
283269
}
284270

285271
// Broadcast success
@@ -467,6 +453,7 @@ func getSuccessMessage(created bool, jiraTaskKey string) string {
467453
}
468454

469455
// attachGistsToJira fetches Gist content and attaches it as markdown files to the Jira issue
456+
// Only attaches if the file doesn't already exist to prevent duplicates
470457
func attachGistsToJira(c *gin.Context, workflow *types.BugFixWorkflow, jiraBase, jiraTaskKey, authHeader string, reqK8s *kubernetes.Clientset, reqDyn dynamic.Interface, project string) {
471458
if workflow.Annotations == nil {
472459
return
@@ -483,34 +470,51 @@ func attachGistsToJira(c *gin.Context, workflow *types.BugFixWorkflow, jiraBase,
483470

484471
ctx := c.Request.Context()
485472

473+
// Get existing attachments to avoid duplicates
474+
existingAttachments, err := jira.GetJiraIssueAttachments(ctx, jiraBase, jiraTaskKey, authHeader)
475+
if err != nil {
476+
fmt.Printf("Warning: Failed to get existing Jira attachments: %v (will attempt upload anyway)\n", err)
477+
existingAttachments = make(map[string]bool) // Continue with empty map
478+
}
479+
486480
// Attach bug-review Gist if available
487481
if bugReviewGist := workflow.Annotations["bug-review-gist-url"]; bugReviewGist != "" {
488-
fmt.Printf("Fetching bug-review Gist from %s\n", bugReviewGist)
489-
gistContent, err := github.GetGist(ctx, bugReviewGist, githubToken)
490-
if err != nil {
491-
fmt.Printf("Warning: Failed to fetch bug-review Gist: %v\n", err)
482+
filename := fmt.Sprintf("bug-review-issue-%d.md", workflow.GithubIssueNumber)
483+
484+
if existingAttachments[filename] {
485+
fmt.Printf("Skipping %s - already attached to %s\n", filename, jiraTaskKey)
492486
} else {
493-
filename := fmt.Sprintf("bug-review-issue-%d.md", workflow.GithubIssueNumber)
494-
if attachErr := jira.AttachFileToJiraIssue(ctx, jiraBase, jiraTaskKey, authHeader, filename, []byte(gistContent)); attachErr != nil {
495-
fmt.Printf("Warning: Failed to attach bug-review.md to %s: %v\n", jiraTaskKey, attachErr)
487+
fmt.Printf("Fetching bug-review Gist from %s\n", bugReviewGist)
488+
gistContent, err := github.GetGist(ctx, bugReviewGist, githubToken)
489+
if err != nil {
490+
fmt.Printf("Warning: Failed to fetch bug-review Gist: %v\n", err)
496491
} else {
497-
fmt.Printf("Successfully attached %s to %s\n", filename, jiraTaskKey)
492+
if attachErr := jira.AttachFileToJiraIssue(ctx, jiraBase, jiraTaskKey, authHeader, filename, []byte(gistContent)); attachErr != nil {
493+
fmt.Printf("Warning: Failed to attach %s to %s: %v\n", filename, jiraTaskKey, attachErr)
494+
} else {
495+
fmt.Printf("Successfully attached %s to %s\n", filename, jiraTaskKey)
496+
}
498497
}
499498
}
500499
}
501500

502501
// Attach implementation Gist if available
503502
if implGist := workflow.Annotations["implementation-gist-url"]; implGist != "" {
504-
fmt.Printf("Fetching implementation Gist from %s\n", implGist)
505-
gistContent, err := github.GetGist(ctx, implGist, githubToken)
506-
if err != nil {
507-
fmt.Printf("Warning: Failed to fetch implementation Gist: %v\n", err)
503+
filename := fmt.Sprintf("implementation-issue-%d.md", workflow.GithubIssueNumber)
504+
505+
if existingAttachments[filename] {
506+
fmt.Printf("Skipping %s - already attached to %s\n", filename, jiraTaskKey)
508507
} else {
509-
filename := fmt.Sprintf("implementation-issue-%d.md", workflow.GithubIssueNumber)
510-
if attachErr := jira.AttachFileToJiraIssue(ctx, jiraBase, jiraTaskKey, authHeader, filename, []byte(gistContent)); attachErr != nil {
511-
fmt.Printf("Warning: Failed to attach implementation.md to %s: %v\n", jiraTaskKey, attachErr)
508+
fmt.Printf("Fetching implementation Gist from %s\n", implGist)
509+
gistContent, err := github.GetGist(ctx, implGist, githubToken)
510+
if err != nil {
511+
fmt.Printf("Warning: Failed to fetch implementation Gist: %v\n", err)
512512
} else {
513-
fmt.Printf("Successfully attached %s to %s\n", filename, jiraTaskKey)
513+
if attachErr := jira.AttachFileToJiraIssue(ctx, jiraBase, jiraTaskKey, authHeader, filename, []byte(gistContent)); attachErr != nil {
514+
fmt.Printf("Warning: Failed to attach %s to %s: %v\n", filename, jiraTaskKey, attachErr)
515+
} else {
516+
fmt.Printf("Successfully attached %s to %s\n", filename, jiraTaskKey)
517+
}
514518
}
515519
}
516520
}

components/backend/jira/integration.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,51 @@ func StripExecutionFlow(content string) string {
172172
return strings.Join(result, "\n")
173173
}
174174

175+
// GetJiraIssueAttachments returns a list of attachment filenames for a Jira issue
176+
func GetJiraIssueAttachments(ctx context.Context, jiraBase, issueKey, authHeader string) (map[string]bool, error) {
177+
endpoint := fmt.Sprintf("%s/rest/api/2/issue/%s", jiraBase, url.PathEscape(issueKey))
178+
179+
httpReq, err := http.NewRequestWithContext(ctx, "GET", endpoint, nil)
180+
if err != nil {
181+
return nil, fmt.Errorf("failed to create request: %w", err)
182+
}
183+
184+
httpReq.Header.Set("Authorization", authHeader)
185+
httpReq.Header.Set("Accept", "application/json")
186+
187+
httpClient := &http.Client{Timeout: 30 * time.Second}
188+
resp, err := httpClient.Do(httpReq)
189+
if err != nil {
190+
return nil, fmt.Errorf("request failed: %w", err)
191+
}
192+
defer resp.Body.Close()
193+
194+
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
195+
return nil, fmt.Errorf("jira API error: %s", resp.Status)
196+
}
197+
198+
var result map[string]interface{}
199+
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
200+
return nil, fmt.Errorf("failed to decode response: %w", err)
201+
}
202+
203+
// Extract attachment filenames
204+
attachments := make(map[string]bool)
205+
if fields, ok := result["fields"].(map[string]interface{}); ok {
206+
if attachmentList, ok := fields["attachment"].([]interface{}); ok {
207+
for _, att := range attachmentList {
208+
if attMap, ok := att.(map[string]interface{}); ok {
209+
if filename, ok := attMap["filename"].(string); ok {
210+
attachments[filename] = true
211+
}
212+
}
213+
}
214+
}
215+
}
216+
217+
return attachments, nil
218+
}
219+
175220
// AttachFileToJiraIssue attaches a file to a Jira issue
176221
func AttachFileToJiraIssue(ctx context.Context, jiraBase, issueKey, authHeader string, filename string, content []byte) error {
177222
endpoint := fmt.Sprintf("%s/rest/api/2/issue/%s/attachments", jiraBase, url.PathEscape(issueKey))
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
'use client';
2+
3+
import Link from 'next/link';
4+
import { Button } from '@/components/ui/button';
5+
import { Badge } from '@/components/ui/badge';
6+
import { ArrowLeft, Loader2, Trash2, Bug, ExternalLink, GitBranch, Clock } from 'lucide-react';
7+
import { formatDistanceToNow } from 'date-fns';
8+
9+
type BugFixWorkflow = {
10+
id: string;
11+
title: string;
12+
description?: string;
13+
githubIssueNumber: number;
14+
githubIssueURL: string;
15+
branchName: string;
16+
phase: string;
17+
createdAt?: string;
18+
};
19+
20+
type BugFixHeaderProps = {
21+
workflow: BugFixWorkflow;
22+
projectName: string;
23+
deleting: boolean;
24+
onDelete: () => Promise<void>;
25+
};
26+
27+
const getPhaseColor = (phase: string) => {
28+
switch (phase) {
29+
case 'Ready':
30+
case 'Completed':
31+
return 'bg-green-500/10 text-green-500 border-green-500/20';
32+
case 'Running':
33+
return 'bg-blue-500/10 text-blue-500 border-blue-500/20';
34+
case 'Initializing':
35+
case 'Pending':
36+
return 'bg-yellow-500/10 text-yellow-500 border-yellow-500/20';
37+
case 'Failed':
38+
return 'bg-red-500/10 text-red-500 border-red-500/20';
39+
default:
40+
return 'bg-gray-500/10 text-gray-500 border-gray-500/20';
41+
}
42+
};
43+
44+
export function BugFixHeader({ workflow, projectName, deleting, onDelete }: BugFixHeaderProps) {
45+
return (
46+
<div className="space-y-4">
47+
<div className="flex items-start justify-between">
48+
<div className="flex items-center gap-4">
49+
<Link href={`/projects/${encodeURIComponent(projectName)}/bugfix`}>
50+
<Button variant="ghost" size="sm">
51+
<ArrowLeft className="h-4 w-4 mr-2" />
52+
Back to BugFix Workspaces
53+
</Button>
54+
</Link>
55+
</div>
56+
<Button variant="destructive" size="sm" onClick={onDelete} disabled={deleting}>
57+
{deleting ? (
58+
<>
59+
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
60+
Deleting…
61+
</>
62+
) : (
63+
<>
64+
<Trash2 className="mr-2 h-4 w-4" />
65+
Delete Workspace
66+
</>
67+
)}
68+
</Button>
69+
</div>
70+
71+
<div className="flex items-start justify-between">
72+
<div className="flex-1">
73+
<div className="flex items-center gap-3">
74+
<Bug className="h-6 w-6" />
75+
<h1 className="text-3xl font-bold">{workflow.title}</h1>
76+
</div>
77+
<div className="flex items-center gap-4 mt-2 text-sm text-muted-foreground">
78+
<a
79+
href={workflow.githubIssueURL}
80+
target="_blank"
81+
rel="noopener noreferrer"
82+
className="flex items-center gap-1 hover:text-primary"
83+
>
84+
GitHub Issue #{workflow.githubIssueNumber}
85+
<ExternalLink className="h-3 w-3" />
86+
</a>
87+
<div className="flex items-center gap-1">
88+
<GitBranch className="h-3 w-3" />
89+
<span className="font-mono text-xs">{workflow.branchName}</span>
90+
</div>
91+
{workflow.createdAt && (
92+
<div className="flex items-center gap-1">
93+
<Clock className="h-3 w-3" />
94+
{formatDistanceToNow(new Date(workflow.createdAt), { addSuffix: true })}
95+
</div>
96+
)}
97+
</div>
98+
</div>
99+
<Badge variant="outline" className={getPhaseColor(workflow.phase)}>
100+
{workflow.phase}
101+
</Badge>
102+
</div>
103+
</div>
104+
);
105+
}

0 commit comments

Comments
 (0)