mirror of
https://github.com/MODSetter/SurfSense.git
synced 2025-09-02 10:39:13 +00:00
reuse edit page for other connectors
This commit is contained in:
parent
5176569e30
commit
69eea7485b
1 changed files with 295 additions and 187 deletions
|
@ -37,7 +37,19 @@ import {
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
|
||||||
// Schema for PAT input when editing repos
|
// Helper function to get connector type display name (copied from manage page)
|
||||||
|
const getConnectorTypeDisplay = (type: string): string => {
|
||||||
|
const typeMap: Record<string, string> = {
|
||||||
|
"SERPER_API": "Serper API",
|
||||||
|
"TAVILY_API": "Tavily API",
|
||||||
|
"SLACK_CONNECTOR": "Slack",
|
||||||
|
"NOTION_CONNECTOR": "Notion",
|
||||||
|
"GITHUB_CONNECTOR": "GitHub",
|
||||||
|
};
|
||||||
|
return typeMap[type] || type;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Schema for PAT input when editing GitHub repos (remains separate)
|
||||||
const githubPatSchema = z.object({
|
const githubPatSchema = z.object({
|
||||||
github_pat: z.string()
|
github_pat: z.string()
|
||||||
.min(20, { message: "GitHub Personal Access Token seems too short." })
|
.min(20, { message: "GitHub Personal Access Token seems too short." })
|
||||||
|
@ -47,9 +59,15 @@ const githubPatSchema = z.object({
|
||||||
});
|
});
|
||||||
type GithubPatFormValues = z.infer<typeof githubPatSchema>;
|
type GithubPatFormValues = z.infer<typeof githubPatSchema>;
|
||||||
|
|
||||||
// Schema for main edit form (just the name for now)
|
// Updated schema for main edit form - includes optional fields for other connector configs
|
||||||
const editConnectorSchema = z.object({
|
const editConnectorSchema = z.object({
|
||||||
name: z.string().min(3, { message: "Connector name must be at least 3 characters." }),
|
name: z.string().min(3, { message: "Connector name must be at least 3 characters." }),
|
||||||
|
// Add optional fields for other connector types' configs
|
||||||
|
SLACK_BOT_TOKEN: z.string().optional(),
|
||||||
|
NOTION_INTEGRATION_TOKEN: z.string().optional(),
|
||||||
|
SERPER_API_KEY: z.string().optional(),
|
||||||
|
TAVILY_API_KEY: z.string().optional(),
|
||||||
|
// GITHUB_PAT is handled separately via patForm for repo editing flow
|
||||||
});
|
});
|
||||||
type EditConnectorFormValues = z.infer<typeof editConnectorSchema>;
|
type EditConnectorFormValues = z.infer<typeof editConnectorSchema>;
|
||||||
|
|
||||||
|
@ -63,9 +81,9 @@ interface GithubRepo {
|
||||||
last_updated: string | null;
|
last_updated: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
type EditMode = 'viewing' | 'editing_repos';
|
type EditMode = 'viewing' | 'editing_repos'; // Only relevant for GitHub
|
||||||
|
|
||||||
export default function EditGithubConnectorPage() {
|
export default function EditConnectorPage() { // Renamed for clarity
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const searchSpaceId = params.search_space_id as string;
|
const searchSpaceId = params.search_space_id as string;
|
||||||
|
@ -74,54 +92,74 @@ export default function EditGithubConnectorPage() {
|
||||||
const { connectors, updateConnector, isLoading: connectorsLoading } = useSearchSourceConnectors();
|
const { connectors, updateConnector, isLoading: connectorsLoading } = useSearchSourceConnectors();
|
||||||
|
|
||||||
const [connector, setConnector] = useState<SearchSourceConnector | null>(null);
|
const [connector, setConnector] = useState<SearchSourceConnector | null>(null);
|
||||||
|
const [originalConfig, setOriginalConfig] = useState<Record<string, any> | null>(null); // Store original config object
|
||||||
|
|
||||||
|
// GitHub specific state (only used if connector type is GitHub)
|
||||||
const [currentSelectedRepos, setCurrentSelectedRepos] = useState<string[]>([]);
|
const [currentSelectedRepos, setCurrentSelectedRepos] = useState<string[]>([]);
|
||||||
const [originalPat, setOriginalPat] = useState<string>(""); // State to hold the initial PAT
|
const [originalPat, setOriginalPat] = useState<string>("");
|
||||||
const [editMode, setEditMode] = useState<EditMode>('viewing');
|
const [editMode, setEditMode] = useState<EditMode>('viewing');
|
||||||
const [fetchedRepos, setFetchedRepos] = useState<GithubRepo[] | null>(null); // Null indicates not fetched yet for edit
|
const [fetchedRepos, setFetchedRepos] = useState<GithubRepo[] | null>(null);
|
||||||
const [newSelectedRepos, setNewSelectedRepos] = useState<string[]>([]); // Tracks selections *during* edit
|
const [newSelectedRepos, setNewSelectedRepos] = useState<string[]>([]);
|
||||||
const [isFetchingRepos, setIsFetchingRepos] = useState(false);
|
const [isFetchingRepos, setIsFetchingRepos] = useState(false);
|
||||||
|
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
|
||||||
// Form for just the PAT input
|
// Form for GitHub PAT input (only used for GitHub repo editing)
|
||||||
const patForm = useForm<GithubPatFormValues>({
|
const patForm = useForm<GithubPatFormValues>({
|
||||||
resolver: zodResolver(githubPatSchema),
|
resolver: zodResolver(githubPatSchema),
|
||||||
defaultValues: { github_pat: "" }, // Default empty, will be reset
|
defaultValues: { github_pat: "" },
|
||||||
});
|
});
|
||||||
|
|
||||||
// Form for the main connector details (e.g., name)
|
// Main form for connector details (name + simple config fields)
|
||||||
const editForm = useForm<EditConnectorFormValues>({
|
const editForm = useForm<EditConnectorFormValues>({
|
||||||
resolver: zodResolver(editConnectorSchema),
|
resolver: zodResolver(editConnectorSchema),
|
||||||
defaultValues: { name: "" },
|
defaultValues: {
|
||||||
|
name: "",
|
||||||
|
SLACK_BOT_TOKEN: "",
|
||||||
|
NOTION_INTEGRATION_TOKEN: "",
|
||||||
|
SERPER_API_KEY: "",
|
||||||
|
TAVILY_API_KEY: "",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Effect to find and set the current connector details on load
|
// Effect to load connector data
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!connectorsLoading && connectors.length > 0 && !connector) { // Added !connector check to prevent loop
|
if (!connectorsLoading && connectors.length > 0 && !connector) {
|
||||||
const currentConnector = connectors.find(c => c.id === connectorId);
|
const currentConnector = connectors.find(c => c.id === connectorId);
|
||||||
if (currentConnector && currentConnector.connector_type === 'GITHUB_CONNECTOR') {
|
if (currentConnector) {
|
||||||
setConnector(currentConnector);
|
setConnector(currentConnector);
|
||||||
const savedRepos = currentConnector.config?.repo_full_names || [];
|
setOriginalConfig(currentConnector.config || {}); // Store original config
|
||||||
const savedPat = currentConnector.config?.GITHUB_PAT || "";
|
|
||||||
setCurrentSelectedRepos(savedRepos);
|
// Reset main form with common and type-specific fields
|
||||||
setNewSelectedRepos(savedRepos);
|
editForm.reset({
|
||||||
setOriginalPat(savedPat); // Store the original PAT
|
name: currentConnector.name,
|
||||||
editForm.reset({ name: currentConnector.name });
|
SLACK_BOT_TOKEN: currentConnector.config?.SLACK_BOT_TOKEN || "",
|
||||||
patForm.reset({ github_pat: savedPat }); // Also reset PAT form initially
|
NOTION_INTEGRATION_TOKEN: currentConnector.config?.NOTION_INTEGRATION_TOKEN || "",
|
||||||
} else if (currentConnector) {
|
SERPER_API_KEY: currentConnector.config?.SERPER_API_KEY || "",
|
||||||
toast.error("This connector is not a GitHub connector.");
|
TAVILY_API_KEY: currentConnector.config?.TAVILY_API_KEY || "",
|
||||||
router.push(`/dashboard/${searchSpaceId}/connectors`);
|
});
|
||||||
|
|
||||||
|
// If GitHub, set up GitHub-specific state
|
||||||
|
if (currentConnector.connector_type === 'GITHUB_CONNECTOR') {
|
||||||
|
const savedRepos = currentConnector.config?.repo_full_names || [];
|
||||||
|
const savedPat = currentConnector.config?.GITHUB_PAT || "";
|
||||||
|
setCurrentSelectedRepos(savedRepos);
|
||||||
|
setNewSelectedRepos(savedRepos);
|
||||||
|
setOriginalPat(savedPat);
|
||||||
|
patForm.reset({ github_pat: savedPat });
|
||||||
|
setEditMode('viewing'); // Start in viewing mode for repos
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
toast.error("Connector not found.");
|
toast.error("Connector not found.");
|
||||||
router.push(`/dashboard/${searchSpaceId}/connectors`);
|
router.push(`/dashboard/${searchSpaceId}/connectors`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [connectorId, connectors, connectorsLoading, router, searchSpaceId, connector]); // Removed editForm, patForm from dependencies
|
}, [connectorId, connectors, connectorsLoading, router, searchSpaceId, connector, editForm, patForm]);
|
||||||
|
|
||||||
// Fetch repositories using the entered PAT
|
// Fetch repositories using the entered PAT
|
||||||
const handleFetchRepositories = async (values: GithubPatFormValues) => {
|
const handleFetchRepositories = async (values: GithubPatFormValues) => {
|
||||||
setIsFetchingRepos(true);
|
setIsFetchingRepos(true);
|
||||||
setFetchedRepos(null);
|
setFetchedRepos(null);
|
||||||
// No need for patInputValue state, values.github_pat has the submitted value
|
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('surfsense_bearer_token');
|
const token = localStorage.getItem('surfsense_bearer_token');
|
||||||
if (!token) throw new Error('No authentication token found');
|
if (!token) throw new Error('No authentication token found');
|
||||||
|
@ -137,21 +175,17 @@ export default function EditGithubConnectorPage() {
|
||||||
body: JSON.stringify({ github_pat: values.github_pat })
|
body: JSON.stringify({ github_pat: values.github_pat })
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorData = await response.json();
|
const errorData = await response.json();
|
||||||
throw new Error(errorData.detail || `Failed to fetch repositories: ${response.statusText}`);
|
throw new Error(errorData.detail || `Failed to fetch repositories: ${response.statusText}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const data: GithubRepo[] = await response.json();
|
const data: GithubRepo[] = await response.json();
|
||||||
setFetchedRepos(data);
|
setFetchedRepos(data);
|
||||||
// Reset selection based on currently SAVED repos when fetching
|
setNewSelectedRepos(currentSelectedRepos);
|
||||||
setNewSelectedRepos(currentSelectedRepos);
|
|
||||||
toast.success(`Found ${data.length} repositories. Select which ones to index.`);
|
toast.success(`Found ${data.length} repositories. Select which ones to index.`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching GitHub repositories:", error);
|
console.error("Error fetching GitHub repositories:", error);
|
||||||
toast.error(error instanceof Error ? error.message : "Failed to fetch repositories.");
|
toast.error(error instanceof Error ? error.message : "Failed to fetch repositories.");
|
||||||
// Don't clear PAT form on error, let user fix it
|
|
||||||
} finally {
|
} finally {
|
||||||
setIsFetchingRepos(false);
|
setIsFetchingRepos(false);
|
||||||
}
|
}
|
||||||
|
@ -166,62 +200,97 @@ export default function EditGithubConnectorPage() {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Save all changes (name and potentially repo selection + PAT)
|
// Save changes - updated to handle different connector types
|
||||||
const handleSaveChanges = async (formData: EditConnectorFormValues) => {
|
const handleSaveChanges = async (formData: EditConnectorFormValues) => {
|
||||||
if (!connector) return;
|
if (!connector || !originalConfig) return;
|
||||||
|
|
||||||
setIsSaving(true);
|
setIsSaving(true);
|
||||||
const updatePayload: Partial<SearchSourceConnector> = {};
|
const updatePayload: Partial<SearchSourceConnector> = {};
|
||||||
let configChanged = false;
|
let configChanged = false;
|
||||||
|
let newConfig: Record<string, any> | null = null;
|
||||||
|
|
||||||
// 1. Check if name changed
|
// 1. Check if name changed
|
||||||
if (formData.name !== connector.name) {
|
if (formData.name !== connector.name) {
|
||||||
updatePayload.name = formData.name;
|
updatePayload.name = formData.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Check PAT and Repo changes
|
// 2. Check for config changes based on connector type
|
||||||
const currentPatInForm = patForm.getValues('github_pat');
|
switch (connector.connector_type) {
|
||||||
let patChanged = false;
|
case 'GITHUB_CONNECTOR':
|
||||||
|
const currentPatInForm = patForm.getValues('github_pat');
|
||||||
|
const patChanged = currentPatInForm !== originalPat;
|
||||||
|
const initialRepoSet = new Set(currentSelectedRepos);
|
||||||
|
const newRepoSet = new Set(newSelectedRepos);
|
||||||
|
const reposChanged = initialRepoSet.size !== newRepoSet.size || ![...initialRepoSet].every(repo => newRepoSet.has(repo));
|
||||||
|
|
||||||
// Check if PAT input field was actually edited
|
if (patChanged || (editMode === 'editing_repos' && reposChanged && fetchedRepos !== null)) {
|
||||||
if (editMode === 'editing_repos' && currentPatInForm !== originalPat) {
|
if (!currentPatInForm || !(currentPatInForm.startsWith('ghp_') || currentPatInForm.startsWith('github_pat_'))) {
|
||||||
patChanged = true;
|
toast.error("Invalid GitHub PAT format. Cannot save.");
|
||||||
|
setIsSaving(false); return;
|
||||||
|
}
|
||||||
|
newConfig = {
|
||||||
|
GITHUB_PAT: currentPatInForm,
|
||||||
|
repo_full_names: newSelectedRepos,
|
||||||
|
};
|
||||||
|
if (reposChanged && newSelectedRepos.length === 0) {
|
||||||
|
toast.warning("Warning: No repositories selected.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'SLACK_CONNECTOR':
|
||||||
|
if (formData.SLACK_BOT_TOKEN !== originalConfig.SLACK_BOT_TOKEN) {
|
||||||
|
if (!formData.SLACK_BOT_TOKEN) {
|
||||||
|
toast.error("Slack Bot Token cannot be empty."); setIsSaving(false); return;
|
||||||
|
}
|
||||||
|
newConfig = { SLACK_BOT_TOKEN: formData.SLACK_BOT_TOKEN };
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'NOTION_CONNECTOR':
|
||||||
|
if (formData.NOTION_INTEGRATION_TOKEN !== originalConfig.NOTION_INTEGRATION_TOKEN) {
|
||||||
|
if (!formData.NOTION_INTEGRATION_TOKEN) {
|
||||||
|
toast.error("Notion Integration Token cannot be empty."); setIsSaving(false); return;
|
||||||
|
}
|
||||||
|
newConfig = { NOTION_INTEGRATION_TOKEN: formData.NOTION_INTEGRATION_TOKEN };
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'SERPER_API':
|
||||||
|
if (formData.SERPER_API_KEY !== originalConfig.SERPER_API_KEY) {
|
||||||
|
if (!formData.SERPER_API_KEY) {
|
||||||
|
toast.error("Serper API Key cannot be empty."); setIsSaving(false); return;
|
||||||
|
}
|
||||||
|
newConfig = { SERPER_API_KEY: formData.SERPER_API_KEY };
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'TAVILY_API':
|
||||||
|
if (formData.TAVILY_API_KEY !== originalConfig.TAVILY_API_KEY) {
|
||||||
|
if (!formData.TAVILY_API_KEY) {
|
||||||
|
toast.error("Tavily API Key cannot be empty."); setIsSaving(false); return;
|
||||||
|
}
|
||||||
|
newConfig = { TAVILY_API_KEY: formData.TAVILY_API_KEY };
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Add cases for other connector types if necessary
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if repo selection was modified
|
// If config was determined to have changed, add it to the payload
|
||||||
const initialRepoSet = new Set(currentSelectedRepos);
|
if (newConfig !== null) {
|
||||||
const newRepoSet = new Set(newSelectedRepos);
|
updatePayload.config = newConfig;
|
||||||
const reposChanged = initialRepoSet.size !== newRepoSet.size || ![...initialRepoSet].every(repo => newRepoSet.has(repo));
|
configChanged = true;
|
||||||
|
|
||||||
// If PAT was changed OR repos were changed (implying PAT was involved)
|
|
||||||
if (patChanged || (editMode === 'editing_repos' && reposChanged && fetchedRepos !== null)) {
|
|
||||||
// Validate the PAT from the form before including it
|
|
||||||
if (!currentPatInForm || !(currentPatInForm.startsWith('ghp_') || currentPatInForm.startsWith('github_pat_'))) {
|
|
||||||
toast.error("Invalid GitHub PAT format in the input field. Cannot save config changes.");
|
|
||||||
setIsSaving(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
updatePayload.config = {
|
|
||||||
// Use the PAT value currently in the form field
|
|
||||||
GITHUB_PAT: currentPatInForm,
|
|
||||||
// Use the latest repo selection state
|
|
||||||
repo_full_names: newSelectedRepos,
|
|
||||||
};
|
|
||||||
configChanged = true; // Mark config as changed
|
|
||||||
|
|
||||||
if (reposChanged && newSelectedRepos.length === 0) {
|
|
||||||
toast.warning("Warning: You haven't selected any repositories. The connector won't index anything.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Check if there are actual changes to save
|
// 3. Check if there are actual changes to save
|
||||||
if (Object.keys(updatePayload).length === 0) {
|
if (Object.keys(updatePayload).length === 0) {
|
||||||
toast.info("No changes detected.");
|
toast.info("No changes detected.");
|
||||||
setIsSaving(false);
|
setIsSaving(false);
|
||||||
setEditMode('viewing');
|
if (connector.connector_type === 'GITHUB_CONNECTOR') {
|
||||||
// Reset PAT form to original value if returning to view mode without saving PAT change
|
setEditMode('viewing');
|
||||||
patForm.reset({ github_pat: originalPat });
|
patForm.reset({ github_pat: originalPat });
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -230,29 +299,38 @@ export default function EditGithubConnectorPage() {
|
||||||
await updateConnector(connectorId, updatePayload);
|
await updateConnector(connectorId, updatePayload);
|
||||||
toast.success("Connector updated successfully!");
|
toast.success("Connector updated successfully!");
|
||||||
|
|
||||||
// Update local state based on what was *actually* saved
|
// Update local state after successful save
|
||||||
if (updatePayload.config) {
|
const newlySavedConfig = updatePayload.config || originalConfig;
|
||||||
setCurrentSelectedRepos(updatePayload.config.repo_full_names || []);
|
setOriginalConfig(newlySavedConfig);
|
||||||
setOriginalPat(updatePayload.config.GITHUB_PAT || "");
|
|
||||||
// Reset PAT form with the newly saved PAT
|
|
||||||
patForm.reset({ github_pat: updatePayload.config.GITHUB_PAT || "" });
|
|
||||||
} else {
|
|
||||||
// If config wasn't in payload, ensure PAT form is reset to original value
|
|
||||||
patForm.reset({ github_pat: originalPat });
|
|
||||||
}
|
|
||||||
// Update connector name state if it changed (or rely on hook refresh)
|
|
||||||
if (updatePayload.name) {
|
if (updatePayload.name) {
|
||||||
setConnector(prev => prev ? { ...prev, name: updatePayload.name! } : null);
|
setConnector(prev => prev ? { ...prev, name: updatePayload.name!, config: newlySavedConfig } : null);
|
||||||
|
editForm.setValue('name', updatePayload.name);
|
||||||
|
} else {
|
||||||
|
setConnector(prev => prev ? { ...prev, config: newlySavedConfig } : null);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset edit state
|
if (connector.connector_type === 'GITHUB_CONNECTOR' && configChanged) {
|
||||||
setEditMode('viewing');
|
const savedGitHubConfig = newlySavedConfig as { GITHUB_PAT?: string; repo_full_names?: string[] };
|
||||||
setFetchedRepos(null);
|
setCurrentSelectedRepos(savedGitHubConfig.repo_full_names || []);
|
||||||
// Reset working selection to match saved state (use the updated currentSelectedRepos)
|
setOriginalPat(savedGitHubConfig.GITHUB_PAT || "");
|
||||||
setNewSelectedRepos(updatePayload.config?.repo_full_names || currentSelectedRepos);
|
setNewSelectedRepos(savedGitHubConfig.repo_full_names || []);
|
||||||
|
patForm.reset({ github_pat: savedGitHubConfig.GITHUB_PAT || "" });
|
||||||
|
} else if (connector.connector_type === 'SLACK_CONNECTOR' && configChanged) {
|
||||||
|
editForm.setValue('SLACK_BOT_TOKEN', newlySavedConfig.SLACK_BOT_TOKEN || "");
|
||||||
|
} // Add similar blocks for Notion, Serper, Tavily
|
||||||
|
else if (connector.connector_type === 'NOTION_CONNECTOR' && configChanged) {
|
||||||
|
editForm.setValue('NOTION_INTEGRATION_TOKEN', newlySavedConfig.NOTION_INTEGRATION_TOKEN || "");
|
||||||
|
} else if (connector.connector_type === 'SERPER_API' && configChanged) {
|
||||||
|
editForm.setValue('SERPER_API_KEY', newlySavedConfig.SERPER_API_KEY || "");
|
||||||
|
} else if (connector.connector_type === 'TAVILY_API' && configChanged) {
|
||||||
|
editForm.setValue('TAVILY_API_KEY', newlySavedConfig.TAVILY_API_KEY || "");
|
||||||
|
}
|
||||||
|
|
||||||
// Optionally redirect or rely on hook refresh
|
// Reset GitHub specific edit state
|
||||||
// router.push(`/dashboard/${searchSpaceId}/connectors`);
|
if (connector.connector_type === 'GITHUB_CONNECTOR') {
|
||||||
|
setEditMode('viewing');
|
||||||
|
setFetchedRepos(null);
|
||||||
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error updating connector:", error);
|
console.error("Error updating connector:", error);
|
||||||
|
@ -298,26 +376,27 @@ export default function EditGithubConnectorPage() {
|
||||||
>
|
>
|
||||||
<Card className="border-2 border-border">
|
<Card className="border-2 border-border">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-2xl font-bold flex items-center gap-2"><Github className="h-6 w-6" /> Edit GitHub Connector</CardTitle>
|
{/* Title can be dynamic based on type */}
|
||||||
|
<CardTitle className="text-2xl font-bold flex items-center gap-2">
|
||||||
|
<Github className="h-6 w-6" /> {/* TODO: Make icon dynamic */}
|
||||||
|
Edit {getConnectorTypeDisplay(connector.connector_type)} Connector
|
||||||
|
</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Modify the connector name and repository selections. To change repository selections, you need to re-enter your PAT.
|
Modify the connector name and configuration.
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
{/* Use editForm for the main form structure */}
|
|
||||||
<Form {...editForm}>
|
<Form {...editForm}>
|
||||||
<form onSubmit={editForm.handleSubmit(handleSaveChanges)} className="space-y-6">
|
<form onSubmit={editForm.handleSubmit(handleSaveChanges)} className="space-y-6">
|
||||||
<CardContent className="space-y-6">
|
<CardContent className="space-y-6">
|
||||||
{/* Connector Name Field */}
|
{/* Name Field (Common) */}
|
||||||
<FormField
|
<FormField
|
||||||
control={editForm.control}
|
control={editForm.control}
|
||||||
name="name"
|
name="name"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Connector Name</FormLabel>
|
<FormLabel>Connector Name</FormLabel>
|
||||||
<FormControl>
|
<FormControl><Input {...field} /></FormControl>
|
||||||
<Input {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
|
@ -325,108 +404,137 @@ export default function EditGithubConnectorPage() {
|
||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
{/* Repository Selection Section */}
|
{/* --- Conditional Configuration Section --- */}
|
||||||
<div className="space-y-4">
|
<h3 className="text-lg font-semibold">Configuration</h3>
|
||||||
<h3 className="text-lg font-semibold flex items-center gap-2"><ListChecks className="h-5 w-5" /> Repository Selection</h3>
|
|
||||||
|
|
||||||
{editMode === 'viewing' && (
|
{/* == GitHub == */}
|
||||||
<div className="space-y-3 p-4 border rounded-md bg-muted/50">
|
{connector.connector_type === 'GITHUB_CONNECTOR' && (
|
||||||
<FormLabel>Currently Indexed Repositories:</FormLabel>
|
<div className="space-y-4">
|
||||||
{currentSelectedRepos.length > 0 ? (
|
<h4 className="font-medium text-muted-foreground">Repository Selection & Access</h4>
|
||||||
<ul className="list-disc pl-5 text-sm">
|
{editMode === 'viewing' && (
|
||||||
{currentSelectedRepos.map(repo => <li key={repo}>{repo}</li>)}
|
<div className="space-y-3 p-4 border rounded-md bg-muted/50">
|
||||||
</ul>
|
<FormLabel>Currently Indexed Repositories:</FormLabel>
|
||||||
) : (
|
{currentSelectedRepos.length > 0 ? (
|
||||||
<p className="text-sm text-muted-foreground">(No repositories currently selected for indexing)</p>
|
<ul className="list-disc pl-5 text-sm">
|
||||||
)}
|
{currentSelectedRepos.map(repo => <li key={repo}>{repo}</li>)}
|
||||||
<Button type="button" variant="outline" size="sm" onClick={() => setEditMode('editing_repos')}>
|
</ul>
|
||||||
<Edit className="mr-2 h-4 w-4" />
|
) : (
|
||||||
Change Selection
|
<p className="text-sm text-muted-foreground">(No repositories currently selected)</p>
|
||||||
</Button>
|
)}
|
||||||
<FormDescription>Click "Change Selection" to re-enter your PAT and update the list.</FormDescription>
|
<Button type="button" variant="outline" size="sm" onClick={() => setEditMode('editing_repos')}>
|
||||||
</div>
|
<Edit className="mr-2 h-4 w-4" /> Change Selection / Update PAT
|
||||||
)}
|
</Button>
|
||||||
|
<FormDescription>To change repo selections or update the PAT, click above.</FormDescription>
|
||||||
{editMode === 'editing_repos' && (
|
</div>
|
||||||
<div className="space-y-4 p-4 border rounded-md">
|
)}
|
||||||
{/* PAT Input Section (No nested Form provider) */}
|
{editMode === 'editing_repos' && (
|
||||||
{/* We still use patForm fields but trigger validation manually */}
|
<div className="space-y-4 p-4 border rounded-md">
|
||||||
<div className="flex items-end gap-4 p-4 border rounded-md bg-muted/90">
|
{/* PAT Input */}
|
||||||
<FormField
|
<div className="flex items-end gap-4 p-4 border rounded-md bg-muted/90">
|
||||||
// Associate with patForm instance for control/state
|
<FormField control={patForm.control} name="github_pat" render={({ field }) => (
|
||||||
control={patForm.control}
|
|
||||||
name="github_pat"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="flex-grow">
|
<FormItem className="flex-grow">
|
||||||
<FormLabel className="flex items-center gap-1"><KeyRound className="h-4 w-4" /> Re-enter PAT to Fetch Repos</FormLabel>
|
<FormLabel className="flex items-center gap-1"><KeyRound className="h-4 w-4" /> GitHub PAT</FormLabel>
|
||||||
<FormControl>
|
<FormControl><Input type="password" placeholder="ghp_... or github_pat_..." {...field} /></FormControl>
|
||||||
<Input type="password" placeholder="ghp_... or github_pat_..." {...field} />
|
<FormDescription>Enter PAT to fetch/update repos or if you need to update the stored token.</FormDescription>
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)} />
|
||||||
/>
|
<Button type="button" disabled={isFetchingRepos} size="sm" onClick={async () => { const isValid = await patForm.trigger('github_pat'); if (isValid) { handleFetchRepositories(patForm.getValues()); } }}>
|
||||||
<Button
|
{isFetchingRepos ? <Loader2 className="h-4 w-4 animate-spin" /> : "Fetch Repositories"}
|
||||||
type="button" // Changed from submit to button
|
</Button>
|
||||||
disabled={isFetchingRepos}
|
</div>
|
||||||
size="sm"
|
{/* Repo List */}
|
||||||
onClick={async () => { // Added async onClick handler
|
{isFetchingRepos && <Skeleton className="h-40 w-full" />}
|
||||||
const isValid = await patForm.trigger('github_pat'); // Trigger validation
|
{!isFetchingRepos && fetchedRepos !== null && (
|
||||||
if (isValid) {
|
fetchedRepos.length === 0 ? (
|
||||||
handleFetchRepositories(patForm.getValues()); // Call fetch if valid
|
<Alert variant="destructive"><CircleAlert className="h-4 w-4" /><AlertTitle>No Repositories Found</AlertTitle><AlertDescription>Check PAT & permissions.</AlertDescription></Alert>
|
||||||
}
|
) : (
|
||||||
}}
|
<div className="space-y-2">
|
||||||
>
|
<FormLabel>Select Repositories to Index ({newSelectedRepos.length} selected):</FormLabel>
|
||||||
{isFetchingRepos ? <Loader2 className="h-4 w-4 animate-spin" /> : "Fetch"}
|
<div className="h-64 w-full rounded-md border p-4 overflow-y-auto">
|
||||||
|
{fetchedRepos.map((repo) => (
|
||||||
|
<div key={repo.id} className="flex items-center space-x-2 mb-2 py-1">
|
||||||
|
<Checkbox id={`repo-${repo.id}`} checked={newSelectedRepos.includes(repo.full_name)} onCheckedChange={(checked) => handleRepoSelectionChange(repo.full_name, !!checked)} />
|
||||||
|
<label htmlFor={`repo-${repo.id}`} className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">{repo.full_name} {repo.private && "(Private)"}</label>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
<Button type="button" variant="ghost" size="sm" onClick={() => { setEditMode('viewing'); setFetchedRepos(null); setNewSelectedRepos(currentSelectedRepos); patForm.reset({ github_pat: originalPat }); }}>
|
||||||
|
Cancel Repo Change
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Fetched Repository List (shown after fetch) */}
|
{/* == Slack == */}
|
||||||
{isFetchingRepos && <Skeleton className="h-40 w-full" />}
|
{connector.connector_type === 'SLACK_CONNECTOR' && (
|
||||||
{!isFetchingRepos && fetchedRepos !== null && (
|
<FormField
|
||||||
fetchedRepos.length === 0 ? (
|
control={editForm.control}
|
||||||
<Alert variant="destructive">
|
name="SLACK_BOT_TOKEN"
|
||||||
<CircleAlert className="h-4 w-4" />
|
render={({ field }) => (
|
||||||
<AlertTitle>No Repositories Found</AlertTitle>
|
<FormItem>
|
||||||
<AlertDescription>Check the PAT and permissions.</AlertDescription>
|
<FormLabel className="flex items-center gap-1"><KeyRound className="h-4 w-4" /> Slack Bot Token</FormLabel>
|
||||||
</Alert>
|
<FormControl><Input type="password" placeholder="Begins with xoxb-..." {...field} /></FormControl>
|
||||||
) : (
|
<FormDescription>Update the Slack Bot Token if needed.</FormDescription>
|
||||||
<div className="space-y-2">
|
<FormMessage />
|
||||||
<FormLabel>Select Repositories to Index ({newSelectedRepos.length} selected):</FormLabel>
|
</FormItem>
|
||||||
<div className="h-64 w-full rounded-md border p-4 overflow-y-auto">
|
)}
|
||||||
{fetchedRepos.map((repo) => (
|
/>
|
||||||
<div key={repo.id} className="flex items-center space-x-2 mb-2 py-1">
|
)}
|
||||||
<Checkbox
|
|
||||||
id={`repo-${repo.id}`}
|
{/* == Notion == */}
|
||||||
checked={newSelectedRepos.includes(repo.full_name)}
|
{connector.connector_type === 'NOTION_CONNECTOR' && (
|
||||||
onCheckedChange={(checked) => handleRepoSelectionChange(repo.full_name, !!checked)}
|
<FormField
|
||||||
/>
|
control={editForm.control}
|
||||||
<label
|
name="NOTION_INTEGRATION_TOKEN"
|
||||||
htmlFor={`repo-${repo.id}`}
|
render={({ field }) => (
|
||||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
<FormItem>
|
||||||
>
|
<FormLabel className="flex items-center gap-1"><KeyRound className="h-4 w-4" /> Notion Integration Token</FormLabel>
|
||||||
{repo.full_name} {repo.private && "(Private)"}
|
<FormControl><Input type="password" placeholder="Begins with secret_..." {...field} /></FormControl>
|
||||||
</label>
|
<FormDescription>Update the Notion Integration Token if needed.</FormDescription>
|
||||||
</div>
|
<FormMessage />
|
||||||
))}
|
</FormItem>
|
||||||
</div>
|
)}
|
||||||
</div>
|
/>
|
||||||
)
|
)}
|
||||||
)}
|
|
||||||
<Button type="button" variant="ghost" size="sm" onClick={() => {
|
{/* == Serper API == */}
|
||||||
setEditMode('viewing');
|
{connector.connector_type === 'SERPER_API' && (
|
||||||
setFetchedRepos(null);
|
<FormField
|
||||||
setNewSelectedRepos(currentSelectedRepos);
|
control={editForm.control}
|
||||||
patForm.reset({ github_pat: originalPat }); // Reset PAT form on cancel
|
name="SERPER_API_KEY"
|
||||||
}}>
|
render={({ field }) => (
|
||||||
Cancel Repo Change
|
<FormItem>
|
||||||
</Button>
|
<FormLabel className="flex items-center gap-1"><KeyRound className="h-4 w-4" /> Serper API Key</FormLabel>
|
||||||
</div>
|
<FormControl><Input type="password" {...field} /></FormControl>
|
||||||
)}
|
<FormDescription>Update the Serper API Key if needed.</FormDescription>
|
||||||
</div>
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* == Tavily API == */}
|
||||||
|
{connector.connector_type === 'TAVILY_API' && (
|
||||||
|
<FormField
|
||||||
|
control={editForm.control}
|
||||||
|
name="TAVILY_API_KEY"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel className="flex items-center gap-1"><KeyRound className="h-4 w-4" /> Tavily API Key</FormLabel>
|
||||||
|
<FormControl><Input type="password" {...field} /></FormControl>
|
||||||
|
<FormDescription>Update the Tavily API Key if needed.</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
||||||
<CardFooter className="border-t pt-6">
|
<CardFooter className="border-t pt-6">
|
||||||
<Button type="submit" disabled={isSaving} className="w-full sm:w-auto">
|
<Button type="submit" disabled={isSaving} className="w-full sm:w-auto">
|
||||||
{isSaving ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <Check className="mr-2 h-4 w-4" />}
|
{isSaving ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <Check className="mr-2 h-4 w-4" />}
|
||||||
|
|
Loading…
Add table
Reference in a new issue