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:
Toni de la Fuente
2026-01-07 21:44:45 +01:00
parent a8c244849f
commit 55327704dd
8 changed files with 928 additions and 0 deletions
+165
View File
@@ -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}` };
};
@@ -0,0 +1,93 @@
import { getIntegrations } from "@/actions/integrations";
import { GitHubIntegrationsManager } from "@/components/integrations/github/github-integrations-manager";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/shadcn";
import { ContentLayout } from "@/components/ui";
interface GitHubIntegrationsProps {
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}
export default async function GitHubIntegrations({
searchParams,
}: GitHubIntegrationsProps) {
const resolvedSearchParams = await searchParams;
const page = parseInt(resolvedSearchParams.page?.toString() || "1", 10);
const pageSize = parseInt(
resolvedSearchParams.pageSize?.toString() || "10",
10,
);
const sort = resolvedSearchParams.sort?.toString();
// Extract all filter parameters
const filters = Object.fromEntries(
Object.entries(resolvedSearchParams).filter(([key]) =>
key.startsWith("filter["),
),
);
const urlSearchParams = new URLSearchParams();
urlSearchParams.set("filter[integration_type]", "github");
urlSearchParams.set("page[number]", page.toString());
urlSearchParams.set("page[size]", pageSize.toString());
if (sort) {
urlSearchParams.set("sort", sort);
}
// Add any additional filters
Object.entries(filters).forEach(([key, value]) => {
if (value !== undefined && key !== "filter[integration_type]") {
const stringValue = Array.isArray(value) ? value[0] : String(value);
urlSearchParams.set(key, stringValue);
}
});
const [integrations] = await Promise.all([getIntegrations(urlSearchParams)]);
const githubIntegrations = integrations?.data || [];
const metadata = integrations?.meta;
return (
<ContentLayout title="GitHub">
<div className="flex flex-col gap-6">
<div className="flex flex-col gap-4">
<p className="text-sm text-gray-600 dark:text-gray-300">
Configure GitHub integration to automatically create issues for
security findings in your GitHub repositories.
</p>
<Card variant="base" padding="lg">
<CardHeader className="mb-0 pb-3">
<CardTitle>Features</CardTitle>
</CardHeader>
<CardContent className="pt-0">
<ul className="grid grid-cols-1 gap-2 text-sm text-gray-600 md:grid-cols-2 dark:text-gray-300">
<li className="flex items-center gap-2">
<span className="bg-button-primary h-1.5 w-1.5 rounded-full" />
Automated issue creation
</li>
<li className="flex items-center gap-2">
<span className="bg-button-primary h-1.5 w-1.5 rounded-full" />
Multi-Cloud support
</li>
<li className="flex items-center gap-2">
<span className="bg-button-primary h-1.5 w-1.5 rounded-full" />
Repository-based tracking
</li>
<li className="flex items-center gap-2">
<span className="bg-button-primary h-1.5 w-1.5 rounded-full" />
Label customization
</li>
</ul>
</CardContent>
</Card>
</div>
<GitHubIntegrationsManager
integrations={githubIntegrations}
metadata={metadata}
/>
</div>
</ContentLayout>
);
}
+4
View File
@@ -1,5 +1,6 @@
import {
ApiKeyLinkCard,
GitHubIntegrationCard,
JiraIntegrationCard,
S3IntegrationCard,
SecurityHubIntegrationCard,
@@ -25,6 +26,9 @@ export default async function Integrations() {
{/* AWS Security Hub Integration */}
<SecurityHubIntegrationCard />
{/* GitHub Integration */}
<GitHubIntegrationCard />
{/* Jira Integration */}
<JiraIntegrationCard />
@@ -0,0 +1,54 @@
"use client";
import { GithubIcon, SettingsIcon } from "lucide-react";
import Link from "next/link";
import { Button } from "@/components/shadcn";
import { CustomLink } from "@/components/ui/custom/custom-link";
import { Card, CardContent, CardHeader } from "../../shadcn";
export const GitHubIntegrationCard = () => {
return (
<Card variant="base" padding="lg">
<CardHeader>
<div className="flex w-full flex-col items-start gap-2 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-3">
<GithubIcon size={40} className="text-gray-900 dark:text-gray-100" />
<div className="flex flex-col gap-1">
<h4 className="text-lg font-bold text-gray-900 dark:text-gray-100">
GitHub
</h4>
<div className="flex flex-col items-start gap-2 sm:flex-row sm:items-center">
<p className="text-xs text-nowrap text-gray-500 dark:text-gray-300">
Create security issues in GitHub repositories.
</p>
<CustomLink
href="https://docs.prowler.com"
aria-label="Learn more about GitHub integration"
size="xs"
>
Learn more
</CustomLink>
</div>
</div>
</div>
<div className="flex items-center gap-2 self-end sm:self-center">
<Button asChild size="sm">
<Link href="/integrations/github">
<SettingsIcon size={14} />
Manage
</Link>
</Button>
</div>
</div>
</CardHeader>
<CardContent>
<p className="text-sm text-gray-600 dark:text-gray-300">
Configure and manage your GitHub integrations to automatically create
issues for security findings in your GitHub repositories.
</p>
</CardContent>
</Card>
);
};
@@ -0,0 +1,210 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { createIntegration, updateIntegration } from "@/actions/integrations";
import { useToast } from "@/components/ui";
import { CustomInput } from "@/components/ui/custom";
import { CustomLink } from "@/components/ui/custom/custom-link";
import { Form } from "@/components/ui/form";
import { FormButtons } from "@/components/ui/form/form-buttons";
import {
editGitHubIntegrationFormSchema,
type GitHubCreateValues,
type GitHubCredentialsPayload,
type GitHubFormValues,
githubIntegrationFormSchema,
IntegrationProps,
} from "@/types/integrations";
interface GitHubIntegrationFormProps {
integration?: IntegrationProps | null;
onSuccess: (integrationId?: string, shouldTestConnection?: boolean) => void;
onCancel: () => void;
}
export const GitHubIntegrationForm = ({
integration,
onSuccess,
onCancel,
}: GitHubIntegrationFormProps) => {
const { toast } = useToast();
const isEditing = !!integration;
const isCreating = !isEditing;
const form = useForm<GitHubFormValues>({
resolver: zodResolver(
isCreating
? githubIntegrationFormSchema
: editGitHubIntegrationFormSchema,
),
defaultValues: {
integration_type: "github" as const,
owner: integration?.attributes.configuration.owner || "",
enabled: integration?.attributes.enabled ?? true,
token: "",
},
});
const isLoading = form.formState.isSubmitting;
const onSubmit = async (data: GitHubFormValues) => {
try {
const formData = new FormData();
// Add integration type
formData.append("integration_type", "github");
// Prepare credentials object
const credentials: GitHubCredentialsPayload = {};
// For editing, only add fields that have values
if (isEditing) {
if (data.token) credentials.token = data.token;
if (data.owner) credentials.owner = data.owner.trim();
} else {
// For creation, token is required
const createData = data as GitHubCreateValues;
credentials.token = createData.token;
if (createData.owner) credentials.owner = createData.owner.trim();
}
// Add credentials as JSON
if (Object.keys(credentials).length > 0) {
formData.append("credentials", JSON.stringify(credentials));
}
// For creation, we need to provide configuration and providers
if (isCreating) {
formData.append("configuration", JSON.stringify({}));
formData.append("providers", JSON.stringify([]));
// enabled exists only in create schema
formData.append(
"enabled",
JSON.stringify((data as GitHubCreateValues).enabled),
);
}
type IntegrationResult =
| { success: string; integrationId?: string }
| { error: string };
let result: IntegrationResult;
if (isEditing) {
result = await updateIntegration(integration.id, formData);
} else {
result = await createIntegration(formData);
}
if (result && "success" in result && result.success) {
toast({
title: "Success!",
description: `GitHub integration ${isEditing ? "updated" : "created"} successfully.`,
});
// Always test connection when creating or updating
const shouldTestConnection = true;
const integrationId =
"integrationId" in result ? result.integrationId : integration?.id;
onSuccess(integrationId, shouldTestConnection);
} else if (result && "error" in result) {
toast({
variant: "destructive",
title: "Operation Failed",
description: result.error,
});
}
} catch (error) {
toast({
variant: "destructive",
title: "Error",
description: `Failed to ${isEditing ? "update" : "create"} GitHub integration. Please try again.`,
});
}
};
const renderForm = () => {
return (
<>
<CustomInput
control={form.control}
name="token"
type="password"
label="Personal Access Token"
labelPlacement="inside"
placeholder="ghp_xxxxxxxxxxxx"
isRequired
isDisabled={isLoading}
/>
<CustomInput
control={form.control}
name="owner"
type="text"
label="Repository Owner (Optional)"
labelPlacement="inside"
placeholder="myorg or myusername"
isDisabled={isLoading}
/>
<div className="rounded-lg border border-blue-200 bg-blue-50 p-4 dark:border-blue-800 dark:bg-blue-900/20">
<p className="text-sm text-blue-800 dark:text-blue-200">
To generate a Personal Access Token with the <code>repo</code>{" "}
scope, visit your{" "}
<a
href="https://github.com/settings/tokens/new"
target="_blank"
rel="noopener noreferrer"
className="font-medium underline"
>
GitHub token settings
</a>
. The owner field is optional and filters repositories by user or
organization.
</p>
</div>
</>
);
};
const getButtonLabel = () => {
if (isEditing) {
return "Update Credentials";
}
return "Create Integration";
};
return (
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="flex flex-col gap-6"
>
<div className="flex flex-col gap-4">
<div className="flex flex-col items-start gap-2 sm:flex-row sm:items-center">
<p className="text-default-500 flex items-center gap-2 text-sm">
Need help configuring your GitHub integration?
</p>
<CustomLink
href="https://docs.prowler.com"
target="_blank"
size="sm"
>
Read the docs
</CustomLink>
</div>
{renderForm()}
</div>
<FormButtons
setIsOpen={() => {}}
onCancel={onCancel}
submitText={getButtonLabel()}
cancelText="Cancel"
loadingText="Processing..."
isDisabled={isLoading}
/>
</form>
</Form>
);
};
@@ -0,0 +1,376 @@
"use client";
import { format } from "date-fns";
import { GithubIcon, PlusIcon, Trash2Icon } from "lucide-react";
import { useState } from "react";
import {
deleteIntegration,
testIntegrationConnection,
updateIntegration,
} from "@/actions/integrations";
import {
IntegrationActionButtons,
IntegrationCardHeader,
IntegrationSkeleton,
} from "@/components/integrations/shared";
import { Button } from "@/components/shadcn";
import { useToast } from "@/components/ui";
import { CustomAlertModal } from "@/components/ui/custom";
import { DataTablePagination } from "@/components/ui/table/data-table-pagination";
import { triggerTestConnectionWithDelay } from "@/lib/integrations/test-connection-helper";
import { MetaDataProps } from "@/types";
import { IntegrationProps } from "@/types/integrations";
import { Card, CardContent, CardHeader } from "../../shadcn";
import { GitHubIntegrationForm } from "./github-integration-form";
interface GitHubIntegrationsManagerProps {
integrations: IntegrationProps[];
metadata?: MetaDataProps;
}
export const GitHubIntegrationsManager = ({
integrations,
metadata,
}: GitHubIntegrationsManagerProps) => {
const [isModalOpen, setIsModalOpen] = useState(false);
const [editingIntegration, setEditingIntegration] =
useState<IntegrationProps | null>(null);
const [isDeleting, setIsDeleting] = useState<string | null>(null);
const [isTesting, setIsTesting] = useState<string | null>(null);
const [isOperationLoading, setIsOperationLoading] = useState(false);
const [isDeleteOpen, setIsDeleteOpen] = useState(false);
const [integrationToDelete, setIntegrationToDelete] =
useState<IntegrationProps | null>(null);
const { toast } = useToast();
const handleAddIntegration = () => {
setEditingIntegration(null);
setIsModalOpen(true);
};
const handleEditCredentials = (integration: IntegrationProps) => {
setEditingIntegration(integration);
setIsModalOpen(true);
};
const handleOpenDeleteModal = (integration: IntegrationProps) => {
setIntegrationToDelete(integration);
setIsDeleteOpen(true);
};
const handleDeleteIntegration = async (id: string) => {
setIsDeleting(id);
try {
const result = await deleteIntegration(id, "github");
if (result.success) {
toast({
title: "Success!",
description: "GitHub integration deleted successfully.",
});
} else if (result.error) {
toast({
variant: "destructive",
title: "Delete Failed",
description: result.error,
});
}
} catch (error) {
toast({
variant: "destructive",
title: "Error",
description: "Failed to delete GitHub integration. Please try again.",
});
} finally {
setIsDeleting(null);
setIsDeleteOpen(false);
setIntegrationToDelete(null);
}
};
const handleTestConnection = async (id: string) => {
setIsTesting(id);
try {
const result = await testIntegrationConnection(id);
if (result.success) {
toast({
title: "Connection test successful!",
description:
result.message || "Connection test completed successfully.",
});
} else if (result.error) {
toast({
variant: "destructive",
title: "Connection test failed",
description: result.error,
});
}
} catch (error) {
toast({
variant: "destructive",
title: "Error",
description: "Failed to test connection. Please try again.",
});
} finally {
setIsTesting(null);
}
};
const handleToggleEnabled = async (integration: IntegrationProps) => {
try {
const newEnabledState = !integration.attributes.enabled;
const formData = new FormData();
formData.append(
"integration_type",
integration.attributes.integration_type,
);
formData.append("enabled", JSON.stringify(newEnabledState));
const result = await updateIntegration(integration.id, formData);
if (result && "success" in result) {
toast({
title: "Success!",
description: `Integration ${newEnabledState ? "enabled" : "disabled"} successfully.`,
});
// If enabling, trigger test connection automatically
if (newEnabledState) {
setIsTesting(integration.id);
triggerTestConnectionWithDelay(
integration.id,
true,
"github",
toast,
500,
() => {
setIsTesting(null);
},
);
}
} else if (result && "error" in result) {
toast({
variant: "destructive",
title: "Toggle Failed",
description: result.error,
});
}
} catch (error) {
toast({
variant: "destructive",
title: "Error",
description: "Failed to toggle integration. Please try again.",
});
}
};
const handleModalClose = () => {
setIsModalOpen(false);
setEditingIntegration(null);
};
const handleFormSuccess = async (
integrationId?: string,
shouldTestConnection?: boolean,
) => {
// Close the modal immediately
setIsModalOpen(false);
setEditingIntegration(null);
setIsOperationLoading(true);
// Set testing state for server-triggered test connections
if (integrationId && shouldTestConnection) {
setIsTesting(integrationId);
}
// Trigger test connection if needed
triggerTestConnectionWithDelay(
integrationId,
shouldTestConnection,
"github",
toast,
200,
() => {
// Clear testing state when server-triggered test completes
setIsTesting(null);
},
);
// Reset loading state after a short delay to show the skeleton briefly
setTimeout(() => {
setIsOperationLoading(false);
}, 1500);
};
return (
<>
<CustomAlertModal
isOpen={isDeleteOpen}
onOpenChange={setIsDeleteOpen}
title="Delete GitHub Integration"
description="This action cannot be undone. This will permanently delete your GitHub integration."
>
<div className="flex w-full justify-end gap-4">
<Button
type="button"
variant="ghost"
size="lg"
onClick={() => {
setIsDeleteOpen(false);
setIntegrationToDelete(null);
}}
disabled={isDeleting !== null}
>
Cancel
</Button>
<Button
type="button"
variant="destructive"
size="lg"
disabled={isDeleting !== null}
onClick={() =>
integrationToDelete &&
handleDeleteIntegration(integrationToDelete.id)
}
>
{!isDeleting && <Trash2Icon size={24} />}
{isDeleting ? "Deleting..." : "Delete"}
</Button>
</div>
</CustomAlertModal>
<CustomAlertModal
isOpen={isModalOpen}
onOpenChange={handleModalClose}
title={
editingIntegration ? "Edit GitHub Integration" : "Add GitHub Integration"
}
description={
editingIntegration
? "Update the credentials for your GitHub integration."
: "Configure your GitHub Personal Access Token to create issues from findings."
}
>
<GitHubIntegrationForm
integration={editingIntegration}
onSuccess={handleFormSuccess}
onCancel={handleModalClose}
/>
</CustomAlertModal>
<div className="space-y-6">
<div className="flex w-full flex-col items-start gap-4 sm:flex-row sm:items-center sm:justify-between">
<div>
<h2 className="text-2xl font-bold">GitHub Integrations</h2>
<p className="text-default-500 text-sm">
Manage your GitHub integrations to send findings as issues
</p>
</div>
<Button onClick={handleAddIntegration} size="sm">
<PlusIcon size={20} />
Add Integration
</Button>
</div>
<div className="flex flex-col gap-4">
{isOperationLoading && <IntegrationSkeleton />}
{!isOperationLoading && integrations.length === 0 && (
<Card variant="base" padding="lg">
<CardContent className="flex flex-col items-center justify-center py-12">
<GithubIcon
size={48}
className="mb-4 text-gray-400 dark:text-gray-600"
/>
<h3 className="mb-2 text-lg font-semibold text-gray-900 dark:text-gray-100">
No GitHub integrations configured
</h3>
<p className="mb-4 text-center text-sm text-gray-500 dark:text-gray-400">
Get started by adding your first GitHub integration
</p>
<Button onClick={handleAddIntegration} size="sm">
<PlusIcon size={16} />
Add Integration
</Button>
</CardContent>
</Card>
)}
{!isOperationLoading &&
integrations.map((integration) => (
<Card key={integration.id} variant="base" padding="lg">
<CardHeader>
<IntegrationCardHeader
icon={
<GithubIcon
size={32}
className="text-gray-900 dark:text-gray-100"
/>
}
title="GitHub Integration"
subtitle={
integration.attributes.configuration.owner
? `Owner: ${integration.attributes.configuration.owner}`
: "All accessible repositories"
}
integrationId={integration.id}
enabled={integration.attributes.enabled}
connected={integration.attributes.connected}
lastChecked={integration.attributes.connection_last_checked_at}
isTesting={isTesting === integration.id}
/>
</CardHeader>
<CardContent>
<div className="flex flex-col gap-4">
{integration.attributes.configuration.repositories &&
Object.keys(
integration.attributes.configuration.repositories,
).length > 0 && (
<div>
<p className="mb-2 text-sm font-medium text-gray-700 dark:text-gray-300">
Accessible Repositories:{" "}
{
Object.keys(
integration.attributes.configuration.repositories,
).length
}
</p>
<p className="text-xs text-gray-500 dark:text-gray-400">
Last synced:{" "}
{integration.attributes.connection_last_checked_at
? format(
new Date(
integration.attributes.connection_last_checked_at,
),
"PPpp",
)
: "Never"}
</p>
</div>
)}
<IntegrationActionButtons
integration={integration}
isTesting={isTesting === integration.id}
isDeleting={isDeleting === integration.id}
onTestConnection={handleTestConnection}
onToggleEnabled={handleToggleEnabled}
onEditCredentials={handleEditCredentials}
onDelete={handleOpenDeleteModal}
/>
</div>
</CardContent>
</Card>
))}
</div>
{metadata && integrations.length > 0 && !isOperationLoading && (
<DataTablePagination metadata={metadata} />
)}
</div>
</>
);
};
+3
View File
@@ -1,5 +1,8 @@
export * from "../providers/enhanced-provider-selector";
export * from "./api-key/api-key-link-card";
export * from "./github/github-integration-card";
export * from "./github/github-integration-form";
export * from "./github/github-integrations-manager";
export * from "./jira/jira-integration-card";
export * from "./jira/jira-integration-form";
export * from "./jira/jira-integrations-manager";
+23
View File
@@ -310,3 +310,26 @@ export interface JiraCredentialsPayload {
user_mail?: string;
api_token?: string;
}
// GitHub Integration Schemas
export const githubIntegrationFormSchema = z.object({
integration_type: z.literal("github"),
token: z.string().min(1, "GitHub token is required"),
owner: z.string().optional(),
enabled: z.boolean().default(true),
});
export const editGitHubIntegrationFormSchema = z.object({
integration_type: z.literal("github"),
token: z.string().min(1, "GitHub token is required").optional(),
owner: z.string().optional(),
});
export type GitHubCreateValues = z.infer<typeof githubIntegrationFormSchema>;
export type GitHubEditValues = z.infer<typeof editGitHubIntegrationFormSchema>;
export type GitHubFormValues = GitHubCreateValues | GitHubEditValues;
export interface GitHubCredentialsPayload {
token?: string;
owner?: string;
}