Files
prowler/ui/actions/integrations/github-dispatch.ts
Toni de la Fuente 55327704dd feat(ui): add GitHub integration UI components
Add complete frontend implementation for GitHub integration following
the same pattern as Jira integration.

Components:
- GitHubIntegrationForm: Form for creating/editing GitHub integrations
- GitHubIntegrationsManager: Manager component for listing and managing integrations
- GitHubIntegrationCard: Card component for main integrations page
- GitHub integration page at /integrations/github

Features:
- Personal Access Token input with validation
- Optional repository owner filter
- Connection testing and repository discovery
- Enable/disable integration toggle
- Edit credentials functionality
- Delete integration with confirmation
- Pagination support
- Integration status display with last checked timestamp

Server Actions:
- getGitHubIntegrations(): Fetch enabled GitHub integrations
- sendFindingToGitHub(): Send finding to GitHub as issue
- pollGitHubDispatchTask(): Poll async task completion

Types & Schemas:
- githubIntegrationFormSchema: Zod schema for creation
- editGitHubIntegrationFormSchema: Zod schema for editing
- GitHubCredentialsPayload: TypeScript interface for credentials
- GitHubDispatchRequest/Response: API types for dispatching

Integration Page:
- Created /integrations/github page with features list
- Added GitHub card to main integrations overview
- Exported GitHub components from integrations index

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-07 21:44:45 +01:00

166 lines
4.6 KiB
TypeScript

"use server";
import { pollTaskUntilSettled } from "@/actions/task/poll";
import { apiBaseUrl, getAuthHeaders } from "@/lib";
import { handleApiError } from "@/lib/server-actions-helper";
import type { IntegrationProps } from "@/types/integrations";
export interface GitHubDispatchRequest {
data: {
type: "integrations-github-dispatches";
attributes: {
repository: string;
labels?: string[];
};
};
}
export interface GitHubDispatchResponse {
data: {
id: string;
type: "tasks";
attributes: {
state: string;
result?: {
created_count?: number;
failed_count?: number;
error?: string;
};
};
};
}
export const getGitHubIntegrations = async (): Promise<
| { success: true; data: IntegrationProps[] }
| { success: false; error: string }
> => {
const headers = await getAuthHeaders({ contentType: false });
const url = new URL(`${apiBaseUrl}/integrations`);
// Filter for GitHub integrations only
url.searchParams.append("filter[integration_type]", "github");
try {
const response = await fetch(url.toString(), { method: "GET", headers });
if (response.ok) {
const data: { data: IntegrationProps[] } = await response.json();
// Filter for enabled integrations on the client side
const enabledIntegrations = (data.data || []).filter(
(integration: IntegrationProps) =>
integration.attributes.enabled === true,
);
return { success: true, data: enabledIntegrations };
}
const errorData: unknown = await response.json().catch(() => ({}));
const errorMessage =
(errorData as { errors?: { detail?: string }[] }).errors?.[0]?.detail ||
`Unable to fetch GitHub integrations: ${response.statusText}`;
return { success: false, error: errorMessage };
} catch (error) {
const errorResult = handleApiError(error);
return { success: false, error: errorResult.error || "An error occurred" };
}
};
export const sendFindingToGitHub = async (
integrationId: string,
findingId: string,
repository: string,
labels?: string[],
): Promise<
| { success: true; taskId: string; message: string }
| { success: false; error: string }
> => {
const headers = await getAuthHeaders({ contentType: true });
const url = new URL(
`${apiBaseUrl}/integrations/${integrationId}/github/dispatches`,
);
// Single finding: use direct filter without array notation
url.searchParams.append("filter[finding_id]", findingId);
const payload: GitHubDispatchRequest = {
data: {
type: "integrations-github-dispatches",
attributes: {
repository: repository,
labels: labels || [],
},
},
};
try {
const response = await fetch(url.toString(), {
method: "POST",
headers,
body: JSON.stringify(payload),
});
if (response.ok) {
const data: GitHubDispatchResponse = await response.json();
const taskId = data?.data?.id;
if (taskId) {
return {
success: true,
taskId,
message: "GitHub issue creation started. Processing...",
};
} else {
return {
success: false,
error: "Failed to start GitHub dispatch. No task ID received.",
};
}
}
const errorData: unknown = await response.json().catch(() => ({}));
const errorMessage =
(errorData as { errors?: { detail?: string }[] }).errors?.[0]?.detail ||
`Unable to send finding to GitHub: ${response.statusText}`;
return { success: false, error: errorMessage };
} catch (error) {
const errorResult = handleApiError(error);
return { success: false, error: errorResult.error || "An error occurred" };
}
};
export const pollGitHubDispatchTask = async (
taskId: string,
): Promise<
{ success: true; message: string } | { success: false; error: string }
> => {
const res = await pollTaskUntilSettled(taskId, {
maxAttempts: 10,
delayMs: 2000,
});
if (!res.ok) {
return { success: false, error: res.error };
}
const { state, result } = res;
type GitHubTaskResult =
GitHubDispatchResponse["data"]["attributes"]["result"];
const githubResult = result as GitHubTaskResult | undefined;
if (state === "completed") {
if (!githubResult?.error) {
return {
success: true,
message: "Finding successfully sent to GitHub!",
};
}
return {
success: false,
error: githubResult?.error || "Failed to create GitHub issue.",
};
}
if (state === "failed") {
return { success: false, error: githubResult?.error || "Task failed." };
}
return { success: false, error: `Unknown task state: ${state}` };
};