From 0b37b19f103d98878541f0d9e857b23ba0261880 Mon Sep 17 00:00:00 2001 From: Brent Salisbury Date: Sat, 22 Jun 2024 14:09:49 -0400 Subject: [PATCH] Feature: add the ability for a user to amend a commit Details in #15 Signed-off-by: Brent Salisbury --- src/app/api/pr/knowledge/route.ts | 4 +- src/app/api/pr/skill/route.ts | 2 +- src/app/dashboard/page.tsx | 13 +- .../edit-submission/knowledge/[id]/page.tsx | 603 ++++++++++++++++++ src/app/edit-submission/skill/[id]/page.tsx | 445 +++++++++++++ src/components/Dashboard/index.tsx | 97 ++- src/types/index.ts | 17 + src/utils/github.ts | 238 +++++++ 8 files changed, 1409 insertions(+), 10 deletions(-) create mode 100644 src/app/edit-submission/knowledge/[id]/page.tsx create mode 100644 src/app/edit-submission/skill/[id]/page.tsx create mode 100644 src/utils/github.ts diff --git a/src/app/api/pr/knowledge/route.ts b/src/app/api/pr/knowledge/route.ts index c860f7e0..2e703ee0 100644 --- a/src/app/api/pr/knowledge/route.ts +++ b/src/app/api/pr/knowledge/route.ts @@ -54,7 +54,7 @@ export async function POST(req: NextRequest) { const forkExists = await checkUserForkExists(headers, githubUsername); if (!forkExists) { await createFork(headers); - // Add a delay to ensure the fork operation completes to avoid a race condition when retrieving the bas SHA + // Add a delay to ensure the fork operation completes to avoid a race condition when retrieving the base SHA // This only occurs if this is the first time submitting and the fork isn't present. // TODO change to a retry console.log('Pause 5s for the forking operation to complete'); @@ -301,7 +301,7 @@ async function createPullRequest(headers: HeadersInit, username: string, branchN method: 'POST', headers, body: JSON.stringify({ - title: `Add knowledge: ${knowledgeName}`, + title: `Knowledge: ${knowledgeName}`, head: `${username}:${branchName}`, base: BASE_BRANCH }) diff --git a/src/app/api/pr/skill/route.ts b/src/app/api/pr/skill/route.ts index c379bf23..05f7f911 100644 --- a/src/app/api/pr/skill/route.ts +++ b/src/app/api/pr/skill/route.ts @@ -273,7 +273,7 @@ async function createPullRequest(headers: HeadersInit, username: string, branchN method: 'POST', headers, body: JSON.stringify({ - title: `Add skill: ${skillName}`, + title: `Skill: ${skillName}`, head: `${username}:${branchName}`, base: BASE_BRANCH }) diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index c3c76e9e..646f1dfd 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -1,9 +1,12 @@ -// src/app/jobs/dashboard/page.tsx +// src/app/page.tsx +'use client'; + import * as React from 'react'; -import { AppLayout } from '../../components/AppLayout'; -import { Index } from '../../components/Dashboard'; +import '@patternfly/react-core/dist/styles/base.css'; +import { AppLayout } from '@/components/AppLayout'; +import { Index } from '@/components/Dashboard'; -const DashboardPage: React.FC = () => { +const Home: React.FunctionComponent = () => { return ( @@ -11,4 +14,4 @@ const DashboardPage: React.FC = () => { ); }; -export default DashboardPage; +export default Home; diff --git a/src/app/edit-submission/knowledge/[id]/page.tsx b/src/app/edit-submission/knowledge/[id]/page.tsx new file mode 100644 index 00000000..2a325dc9 --- /dev/null +++ b/src/app/edit-submission/knowledge/[id]/page.tsx @@ -0,0 +1,603 @@ +// src/app/edit-submission/knowledge/[id]/page.tsx +'use client'; + +import * as React from 'react'; +import { useRouter } from 'next/navigation'; +import { useSession } from 'next-auth/react'; +import { Alert, AlertActionLink, AlertActionCloseButton } from '@patternfly/react-core/dist/dynamic/components/Alert'; +import { FormFieldGroupExpandable, FormFieldGroupHeader } from '@patternfly/react-core/dist/dynamic/components/Form'; +import { PlusIcon, MinusCircleIcon } from '@patternfly/react-icons/dist/dynamic/icons/'; +import { Title } from '@patternfly/react-core/dist/dynamic/components/Title'; +import { PageSection } from '@patternfly/react-core/dist/dynamic/components/Page'; +import { Form } from '@patternfly/react-core/dist/dynamic/components/Form'; +import { FormGroup } from '@patternfly/react-core/dist/dynamic/components/Form'; +import { TextInput } from '@patternfly/react-core/dist/dynamic/components/TextInput'; +import { TextArea } from '@patternfly/react-core/dist/dynamic/components/TextArea'; +import { ActionGroup } from '@patternfly/react-core/dist/dynamic/components/Form'; +import { Button } from '@patternfly/react-core/dist/dynamic/components/Button'; +import { Text } from '@patternfly/react-core/dist/dynamic/components/Text'; +import { AppLayout } from '../../../../components/AppLayout'; +import { UploadFile } from '../../../../components/Contribute/Knowledge/UploadFile'; +import { + fetchPullRequest, + fetchFileContent, + updatePullRequest, + fetchPullRequestFiles, + getGitHubUsername, + amendCommit +} from '../../../../utils/github'; +import yaml from 'js-yaml'; + +interface YamlData { + created_by: string; + domain: string; + task_description: string; + task_details: string; + document: { + repo: string; + commit: string; + patterns: string[]; + }; + seed_examples: Array<{ + question: string; + answer: string; + }>; +} + +interface AttributionData { + title_of_work: string; + link_to_work: string; + revision: string; + license_of_the_work: string; + creator_names: string; +} + +const EditPullRequestPage: React.FunctionComponent<{ params: { id: string } }> = ({ params }) => { + const { data: session } = useSession(); + const [title, setTitle] = React.useState(''); + const [body, setBody] = React.useState(''); + const [email, setEmail] = React.useState(''); + const [name, setName] = React.useState(''); + const [task_description, setTaskDescription] = React.useState(''); + const [task_details, setTaskDetails] = React.useState(''); + const [domain, setDomain] = React.useState(''); + const [repo, setRepo] = React.useState(''); + const [commit, setCommit] = React.useState(''); + const [patterns, setPatterns] = React.useState(''); + const [title_work, setTitleWork] = React.useState(''); + const [link_work, setLinkWork] = React.useState(''); + const [revision, setRevision] = React.useState(''); + const [license_work, setLicenseWork] = React.useState(''); + const [creators, setCreators] = React.useState(''); + const [questions, setQuestions] = React.useState([]); + const [answers, setAnswers] = React.useState([]); + const [error, setError] = React.useState(null); + const [yamlFile, setYamlFile] = React.useState<{ filename: string } | null>(null); + const [attributionFile, setAttributionFile] = React.useState<{ filename: string } | null>(null); + const [branchName, setBranchName] = React.useState(null); + const [useFileUpload, setUseFileUpload] = React.useState(false); + const [uploadedFiles, setUploadedFiles] = React.useState([]); + const router = useRouter(); + const { id: number } = params; + + // Alerts + const [isSuccessAlertVisible, setIsSuccessAlertVisible] = React.useState(false); + const [isFailureAlertVisible, setIsFailureAlertVisible] = React.useState(false); + const [failureAlertTitle, setFailureAlertTitle] = React.useState(''); + const [failureAlertMessage, setFailureAlertMessage] = React.useState(''); + const [successAlertTitle, setSuccessAlertTitle] = React.useState(''); + const [successAlertMessage, setSuccessAlertMessage] = React.useState(''); + const [successAlertLink, setSuccessAlertLink] = React.useState(''); + + React.useEffect(() => { + console.log('Params:', params); // Log the params to verify its content + const fetchPRData = async () => { + if (session?.accessToken) { + try { + console.log(`Fetching PR with number: ${number}`); + const prData = await fetchPullRequest(session.accessToken, number); + console.log(`Fetched PR data:`, prData); + setTitle(prData.title); + setBody(prData.body); + setBranchName(prData.head.ref); // Store the branch name from the pull request + + const prFiles = await fetchPullRequestFiles(session.accessToken, number); + console.log(`Fetched PR files:`, prFiles); + + const foundYamlFile = prFiles.find((file) => file.filename.endsWith('.yaml')); + if (!foundYamlFile) { + throw new Error('No YAML file found in the pull request.'); + } + setYamlFile(foundYamlFile); + console.log(`YAML file found:`, foundYamlFile); + + const yamlContent = await fetchFileContent(session.accessToken, foundYamlFile.filename, prData.head.sha); + console.log('Fetched YAML content:', yamlContent); + const yamlData: YamlData = yaml.load(yamlContent); + console.log('Parsed YAML data:', yamlData); + + // Populate the form fields with YAML data + setEmail(yamlData.created_by); + setName(yamlData.domain); + setTaskDescription(yamlData.task_description); + setTaskDetails(yamlData.task_details); + setDomain(yamlData.domain); + setRepo(yamlData.document.repo); + setCommit(yamlData.document.commit); + setPatterns(yamlData.document.patterns.join(', ')); + setQuestions(yamlData.seed_examples.map((example) => example.question)); + setAnswers(yamlData.seed_examples.map((example) => example.answer)); + + // Fetch and parse attribution file if it exists + const foundAttributionFile = prFiles.find((file) => file.filename.includes('attribution')); + if (foundAttributionFile) { + setAttributionFile(foundAttributionFile); + console.log(`Attribution file found:`, foundAttributionFile); + const attributionContent = await fetchFileContent(session.accessToken, foundAttributionFile.filename, prData.head.sha); + console.log('Fetched attribution content:', attributionContent); + const attributionData = parseAttributionContent(attributionContent); + console.log('Parsed attribution data:', attributionData); + + // Populate the form fields with attribution data + setTitleWork(attributionData.title_of_work); + setLinkWork(attributionData.link_to_work); + setRevision(attributionData.revision); + setLicenseWork(attributionData.license_of_the_work); + setCreators(attributionData.creator_names); + } + } catch (error) { + console.error('Error fetching pull request data:', error.response ? error.response.data : error.message); + setError(`Failed to fetch pull request data: ${error.message}`); + } + } + }; + fetchPRData(); + }, [session, number, params]); + + const handleSave = async () => { + if (session?.accessToken && yamlFile && attributionFile && branchName) { + try { + console.log(`Updating PR with number: ${number}`); + await updatePullRequest(session.accessToken, number, { title, body }); + + const githubUsername = await getGitHubUsername(session.accessToken); + console.log(`GitHub username: ${githubUsername}`); + + const updatedYamlData: YamlData = { + created_by: email, + domain, + task_description, + task_details, + document: { + repo, + commit, + patterns: patterns.split(',').map((pattern) => pattern.trim()) + }, + seed_examples: questions.map((question, index) => ({ + question, + answer: answers[index] + })) + }; + const updatedYamlContent = yaml.dump(updatedYamlData, { lineWidth: -1 }); + + console.log('Updated YAML content:', updatedYamlContent); + + const updatedAttributionContent = `Title of work: ${title_work} +Link to work: ${link_work} +Revision: ${revision} +License of the work: ${license_work} +Creator names: ${creators} +`; + + console.log('Updated Attribution content:', updatedAttributionContent); + + // Update the commit by amending it with the new content + console.log(`Amending commit with updated content`); + const amendedCommitResponse = await amendCommit( + session.accessToken, + githubUsername, + 'taxonomy-sub-testing', + { yaml: yamlFile.filename, attribution: attributionFile.filename }, + updatedYamlContent, + updatedAttributionContent, + branchName + ); + console.log('Amended commit response:', amendedCommitResponse); + + const prLink = `https://github.com/brents-pet-robot/taxonomy-sub-testing/pull/${number}`; + setSuccessAlertTitle('Pull request updated successfully!'); + setSuccessAlertMessage('Your pull request has been updated successfully.'); + setSuccessAlertLink(prLink); + setIsSuccessAlertVisible(true); + } catch (error) { + console.error('Error updating pull request:', error.response ? error.response.data : error.message); + setFailureAlertTitle('Failed to update pull request'); + setFailureAlertMessage(error.message); + setIsFailureAlertVisible(true); + } + } else { + setFailureAlertTitle('Error'); + setFailureAlertMessage('YAML file, Attribution file, or branch name is missing.'); + setIsFailureAlertVisible(true); + } + }; + + const handleFilesChange = (files: File[]) => { + setUploadedFiles(files); + setPatterns(files.map((file) => file.name).join(', ')); // Populate the patterns field + }; + + const handleDocumentUpload = async () => { + if (uploadedFiles.length > 0) { + const fileContents: { fileName: string; fileContent: string }[] = []; + + await Promise.all( + uploadedFiles.map( + (file) => + new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = (e) => { + const fileContent = e.target!.result as string; + fileContents.push({ fileName: file.name, fileContent }); + resolve(); + }; + reader.onerror = reject; + reader.readAsText(file); + }) + ) + ); + + if (fileContents.length === uploadedFiles.length) { + try { + const response = await fetch('/api/upload', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ files: fileContents }) + }); + + const result = await response.json(); + if (response.ok) { + setRepo(result.repoUrl); + setCommit(result.commitSha); + setPatterns(result.documentNames.join(', ')); // Populate the patterns field + console.log('Files uploaded:', result.documentNames); + setSuccessAlertTitle('Document uploaded successfully!'); + setSuccessAlertMessage('Documents have been uploaded to your repo to be referenced in the knowledge submission.'); + setSuccessAlertLink(result.prUrl); + setIsSuccessAlertVisible(true); + setUseFileUpload(false); // Switch back to manual mode to display the newly created values in the knowledge submission + } else { + throw new Error(result.error || 'Failed to upload document'); + } + } catch (error: unknown) { + if (error instanceof Error) { + setFailureAlertTitle('Failed to upload document'); + setFailureAlertMessage(error.message); + setIsFailureAlertVisible(true); + } + } + } + } + }; + + const handleInputChange = (index: number, type: string, value: string) => { + switch (type) { + case 'question': + setQuestions((prevQuestions) => { + const updatedQuestions = [...prevQuestions]; + updatedQuestions[index] = value; + return updatedQuestions; + }); + break; + case 'answer': + setAnswers((prevAnswers) => { + const updatedAnswers = [...prevAnswers]; + updatedAnswers[index] = value; + return updatedAnswers; + }); + break; + default: + break; + } + }; + + const addQuestionAnswerPair = () => { + setQuestions([...questions, '']); + setAnswers([...answers, '']); + }; + + const deleteQuestionAnswerPair = (index: number) => { + setQuestions(questions.filter((_, i) => i !== index)); + setAnswers(answers.filter((_, i) => i !== index)); + }; + + const parseAttributionContent = (content: string): AttributionData => { + const lines = content.split('\n'); + const attributionData: { [key: string]: string } = {}; + lines.forEach((line) => { + const [key, ...value] = line.split(':'); + if (key && value) { + // Remove spaces in the attribution field for parsing + const normalizedKey = key.trim().toLowerCase().replace(/\s+/g, '_'); + attributionData[normalizedKey] = value.join(':').trim(); + } + }); + return attributionData as AttributionData; + }; + + return ( + + + + Edit Pull Request + + {error && } +
+ + + + + } + > + + setEmail(value)} + /> + setName(value)} + /> + + + + + } + > + + setTaskDescription(value)} + /> + setDomain(value)} + /> +