mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-24 21:11:53 +00:00
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>
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
"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}` };
|
||||
};
|
||||
Reference in New Issue
Block a user