From 4f5189dc91b0ddbd58ad59f2ae80e2551d745966 Mon Sep 17 00:00:00 2001 From: Anil Vishnoi Date: Wed, 11 Dec 2024 03:37:37 -0800 Subject: [PATCH] Add feature to delete, and publish the contribution Signed-off-by: Anil Vishnoi --- src/app/api/envConfig/route.ts | 3 +- src/app/api/native/clone-repo/route.ts | 8 +- src/app/api/native/git/branches/route.ts | 255 ++++++++++++++---- src/app/api/native/pr/knowledge/route.ts | 4 +- src/app/api/native/pr/skill/route.ts | 4 +- src/components/Dashboard/Native/dashboard.tsx | 222 ++++++++++++++- 6 files changed, 425 insertions(+), 71 deletions(-) diff --git a/src/app/api/envConfig/route.ts b/src/app/api/envConfig/route.ts index 5b30015e..330fb372 100644 --- a/src/app/api/envConfig/route.ts +++ b/src/app/api/envConfig/route.ts @@ -16,7 +16,8 @@ export async function GET() { UPSTREAM_REPO_NAME: process.env.NEXT_PUBLIC_TAXONOMY_REPO || '', DEPLOYMENT_TYPE: process.env.IL_UI_DEPLOYMENT || '', ENABLE_DEV_MODE: process.env.IL_ENABLE_DEV_MODE || '', - EXPERIMENTAL_FEATURES: process.env.NEXT_PUBLIC_EXPERIMENTAL_FEATURES || '' + EXPERIMENTAL_FEATURES: process.env.NEXT_PUBLIC_EXPERIMENTAL_FEATURES || '', + TAXONOMY_REPO_DIR: process.env.NEXT_PUBLIC_TAXONOMY_REPO_DIR || '' }; return NextResponse.json(envConfig); diff --git a/src/app/api/native/clone-repo/route.ts b/src/app/api/native/clone-repo/route.ts index 81630c1f..eb5fc362 100644 --- a/src/app/api/native/clone-repo/route.ts +++ b/src/app/api/native/clone-repo/route.ts @@ -6,11 +6,11 @@ import fs from 'fs'; import path from 'path'; // Retrieve the base directory from the environment variable -const TAXONOMY_ROOT_DIR = process.env.NEXT_PUBLIC_TAXONOMY_ROOT_DIR || './.instructlab-ui'; +const LOCAL_TAXONOMY_ROOT_DIR = process.env.NEXT_PUBLIC_LOCAL_TAXONOMY_ROOT_DIR || './.instructlab-ui'; const TAXONOMY_REPO_URL = process.env.NEXT_PUBLIC_TAXONOMY_REPO_URL || 'https://github.com/instructlab/taxonomy.git'; export async function POST() { - const taxonomyDirectoryPath = path.join(TAXONOMY_ROOT_DIR, '/taxonomy'); + const taxonomyDirectoryPath = path.join(LOCAL_TAXONOMY_ROOT_DIR, '/taxonomy'); if (fs.existsSync(taxonomyDirectoryPath)) { const files = fs.readdirSync(taxonomyDirectoryPath); @@ -32,8 +32,8 @@ export async function POST() { }); // Include the full path in the response for client display - console.log(`Repository cloned successfully to ${TAXONOMY_ROOT_DIR}.`); - return NextResponse.json({ message: `Repository cloned successfully to ${TAXONOMY_ROOT_DIR}.` }, { status: 200 }); + console.log(`Repository cloned successfully to ${LOCAL_TAXONOMY_ROOT_DIR}.`); + return NextResponse.json({ message: `Repository cloned successfully to ${LOCAL_TAXONOMY_ROOT_DIR}.` }, { status: 200 }); } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; console.error(`Failed to clone taxonomy repository: ${errorMessage}`); diff --git a/src/app/api/native/git/branches/route.ts b/src/app/api/native/git/branches/route.ts index 0fcd51d0..092c5865 100644 --- a/src/app/api/native/git/branches/route.ts +++ b/src/app/api/native/git/branches/route.ts @@ -5,10 +5,14 @@ import fs from 'fs'; import path from 'path'; // Get the repository path from the environment variable -const TAXONOMY_ROOT_DIR = process.env.NEXT_PUBLIC_TAXONOMY_ROOT_DIR || './.instructlab-ui'; +const LOCAL_TAXONOMY_ROOT_DIR = process.env.NEXT_PUBLIC_LOCAL_TAXONOMY_ROOT_DIR || './.instructlab-ui'; +interface Diffs { + file: string; + status: string; +} export async function GET() { - const REPO_DIR = path.join(TAXONOMY_ROOT_DIR, '/taxonomy'); + const REPO_DIR = path.join(LOCAL_TAXONOMY_ROOT_DIR, '/taxonomy'); try { // Ensure the repository path exists if (!fs.existsSync(REPO_DIR)) { @@ -41,91 +45,234 @@ export async function GET() { // Handle POST requests for merge or branch comparison export async function POST(req: NextRequest) { - const REPO_DIR = path.join(TAXONOMY_ROOT_DIR, '/taxonomy'); - const { branchName, action } = await req.json(); + const LOCAL_TAXONOMY_DIR = path.join(LOCAL_TAXONOMY_ROOT_DIR, '/taxonomy'); + const { branchName, action, remoteTaxonomyRepoDir } = await req.json(); + console.log('Received POST request:', { branchName, action, remoteTaxonomyRepoDir }); + + if (action === 'delete') { + return handleDelete(branchName, LOCAL_TAXONOMY_DIR); + } + + if (action === 'diff') { + return handleDiff(branchName, LOCAL_TAXONOMY_DIR); + } + if (action === 'publish') { + return handlePublish(branchName, LOCAL_TAXONOMY_DIR, remoteTaxonomyRepoDir); + } + return NextResponse.json({ error: 'Invalid action specified' }, { status: 400 }); +} + +async function handleDelete(branchName: string, localTaxonomyDir: string) { try { - if (action === 'merge') { - // Ensure valid branch name - if (!branchName || branchName === 'main') { - return NextResponse.json({ error: 'Invalid branch name for merge' }, { status: 400 }); - } + if (!branchName || branchName === 'main') { + return NextResponse.json({ error: 'Invalid branch name for deletion' }, { status: 400 }); + } - // Initialize the repository and checkout main branch - await git.init({ fs, dir: REPO_DIR }); - await git.checkout({ fs, dir: REPO_DIR, ref: 'main' }); + // Delete the target branch + await git.deleteBranch({ fs, dir: localTaxonomyDir, ref: branchName }); - // Perform the merge - await git.merge({ - fs, - dir: REPO_DIR, - ours: 'main', - theirs: branchName, - author: { - name: 'Instruct Lab Local', - email: 'local@instructlab.ai' - } - }); + return NextResponse.json({ message: `Successfully deleted branch ${branchName}.` }, { status: 200 }); + } catch (error) { + console.error(`Failed to delete contribution ${branchName}:`, error); + return NextResponse.json( + { + error: `Failed to delete contribution ${branchName}` + }, + { status: 500 } + ); + } finally { + // Ensure switching back to 'main' branch after any operation + try { + await git.checkout({ fs, dir: localTaxonomyDir, ref: 'main' }); + } catch (checkoutError) { + console.error('Failed to switch back to main branch:', checkoutError); + } + } +} + +async function handleDiff(branchName: string, localTaxonomyDir: string) { + try { + // Ensure valid branch name + if (!branchName || branchName === 'main') { + return NextResponse.json({ error: 'Invalid branch name for comparison' }, { status: 400 }); + } + + const changes = await findDiff(branchName, localTaxonomyDir); + return NextResponse.json({ changes }, { status: 200 }); + } catch (error) { + console.error(`Failed to show contribution changes ${branchName}:`, error); + return NextResponse.json( + { + error: `Failed to show contribution changes for ${branchName}` + }, + { status: 500 } + ); + } finally { + // Ensure switching back to 'main' branch after any operation + try { + await git.checkout({ fs, dir: localTaxonomyDir, ref: 'main' }); + } catch (checkoutError) { + console.error('Failed to switch back to main branch:', checkoutError); + } + } +} + +async function findDiff(branchName: string, localTaxonomyDir: string): Promise { + // Fetch the commit SHA for `main` and the target branch + const mainCommit = await git.resolveRef({ fs, dir: localTaxonomyDir, ref: 'main' }); + const branchCommit = await git.resolveRef({ fs, dir: localTaxonomyDir, ref: branchName }); - return NextResponse.json({ message: `Successfully merged ${branchName} into main.` }, { status: 200 }); - } else if (action === 'diff') { - // Ensure valid branch name - if (!branchName || branchName === 'main') { - return NextResponse.json({ error: 'Invalid branch name for comparison' }, { status: 400 }); + const mainFiles = await getFilesFromTree(mainCommit); + const branchFiles = await getFilesFromTree(branchCommit); + + // Create an array of Diffs to store changes + const changes: Diffs[] = []; + // Identify modified and deleted files + for (const file in mainFiles) { + if (branchFiles[file]) { + if (mainFiles[file] !== branchFiles[file]) { + changes.push({ file, status: 'modified' }); } + } else { + changes.push({ file, status: 'deleted' }); + } + } + + // Identify added files + for (const file in branchFiles) { + if (!mainFiles[file]) { + changes.push({ file, status: 'added' }); + } + } + return changes; +} - // Fetch the commit SHA for `main` and the target branch - const mainCommit = await git.resolveRef({ fs, dir: REPO_DIR, ref: 'main' }); - const branchCommit = await git.resolveRef({ fs, dir: REPO_DIR, ref: branchName }); +async function getTopCommitDetails(dir: string, ref: string = 'HEAD') { + try { + // Fetch the top commit (latest commit on the branch) + const [topCommit] = await git.log({ + fs, + dir, + ref, + depth: 1 // Only fetch the latest commit + }); - const mainFiles = await getFilesFromTree(mainCommit); - const branchFiles = await getFilesFromTree(branchCommit); + if (!topCommit) { + throw new Error('No commits found in the repository.'); + } - const changes = []; + // Extract commit message + const commitMessage = topCommit.commit.message; - // Identify modified and deleted files - for (const file in mainFiles) { - if (branchFiles[file]) { - if (mainFiles[file] !== branchFiles[file]) { - changes.push({ file, status: 'modified' }); - } - } else { - changes.push({ file, status: 'deleted' }); - } + // Check for Signed-off-by line + const signoffMatch = commitMessage.match(/^Signed-off-by: (.+)$/m); + const signoff = signoffMatch ? signoffMatch[1] : null; + + return { + message: commitMessage, + signoff + }; + } catch (error) { + console.error('Error reading top commit details:', error); + throw error; + } +} +async function handlePublish(branchName: string, localTaxonomyDir: string, remoteTaxonomyDir: string) { + try { + if (!branchName || branchName === 'main') { + return NextResponse.json({ error: 'Invalid branch name for publish' }, { status: 400 }); + } + + console.log(`Publishing contribution from ${branchName} to remote taxonomy repo at ${remoteTaxonomyDir}`); + const changes = await findDiff(branchName, localTaxonomyDir); + + // Check if there are any changes to publish, create a new branch at remoteTaxonomyDir and copy all the files listed in the changes array to the new branch and create a commit + if (changes.length > 0) { + const remoteBranchName = branchName; + await git.checkout({ fs, dir: localTaxonomyDir, ref: branchName }); + // Read the commit message of the top commit from the branch + const details = await getTopCommitDetails(localTaxonomyDir); + + // Check if the remote branch exists, if not create it + const remoteBranchExists = await git.listBranches({ fs, dir: remoteTaxonomyDir }); + if (remoteBranchExists.includes(remoteBranchName)) { + console.log(`Branch ${remoteBranchName} exist in remote taxonomy, deleting it.`); + // Delete the remote branch if it exists, we will recreate it + await git.deleteBranch({ fs, dir: remoteTaxonomyDir, ref: remoteBranchName }); + } else { + console.log(`Branch ${remoteBranchName} does not exist in remote taxonomy, creating a new branch.`); } - // Identify added files - for (const file in branchFiles) { - if (!mainFiles[file]) { - changes.push({ file, status: 'added' }); + await git.checkout({ fs, dir: remoteTaxonomyDir, ref: 'main' }); + await git.branch({ fs, dir: remoteTaxonomyDir, ref: remoteBranchName }); + await git.checkout({ fs, dir: remoteTaxonomyDir, ref: remoteBranchName }); + + // Copy the files listed in the changes array to the remote branch and if the directories do not exist, create them + for (const change of changes) { + console.log(`Copying ${change.file} to remote branch`); + const filePath = path.join(localTaxonomyDir, change.file); + const remoteFilePath = path.join(remoteTaxonomyDir, change.file); + const remoteFileDir = path.dirname(remoteFilePath); + if (!fs.existsSync(remoteFileDir)) { + fs.mkdirSync(remoteFileDir, { recursive: true }); } + fs.copyFileSync(filePath, remoteFilePath); } - return NextResponse.json({ changes }, { status: 200 }); + await git.add({ fs, dir: remoteTaxonomyDir, filepath: '.' }); + + const authorInfo = details.signoff!.match(/(.*?) <(.*?)>/); + let authorName = ''; + let authorEmail = ''; + if (authorInfo) { + console.log(`Author information found in signoff: ${authorInfo}`); + authorName = authorInfo[1]; + authorEmail = authorInfo[2]; + } else { + return NextResponse.json({ message: `Author information is not present in the contribution ${branchName}.` }, { status: 500 }); + } + // Create a commit with the same message and signoff as the top commit from the local branch + await git.commit({ + fs, + dir: remoteTaxonomyDir, + message: details.message, + author: { + name: authorName, + email: authorEmail + } + }); + console.log(`Successfully published contribution from ${branchName} to remote taxonomy repo at ${remoteTaxonomyDir}`); + return NextResponse.json({ message: `Successfully published contribution to ${remoteTaxonomyDir}.` }, { status: 200 }); } else { - return NextResponse.json({ error: 'Invalid action specified' }, { status: 400 }); + return NextResponse.json({ message: `No changes to publish from ${branchName}.` }, { status: 200 }); } } catch (error) { - console.error(`Failed to ${action === 'merge' ? 'merge branch' : 'compare branches'}:`, error); + console.error(`Failed to publish contribution from ${branchName}:`, error); return NextResponse.json( { - error: `Failed to ${action === 'merge' ? 'merge branch' : 'compare branches'}` + error: `Failed to publish contribution from ${branchName}` }, { status: 500 } ); } finally { // Ensure switching back to 'main' branch after any operation try { - await git.checkout({ fs, dir: REPO_DIR, ref: 'main' }); + await git.checkout({ fs, dir: localTaxonomyDir, ref: 'main' }); } catch (checkoutError) { - console.error('Failed to switch back to main branch:', checkoutError); + console.error('Failed to switch back to main branch in local taxonomy repo:', checkoutError); + } + try { + await git.checkout({ fs, dir: remoteTaxonomyDir, ref: 'main' }); + } catch (checkoutError) { + console.error('Failed to switch back to main branch in remote taxonomy repo:', checkoutError); } } } // Helper function to recursively gather file paths and their oids from a tree async function getFilesFromTree(commitOid: string) { - const REPO_DIR = path.join(TAXONOMY_ROOT_DIR, '/taxonomy'); + const REPO_DIR = path.join(LOCAL_TAXONOMY_ROOT_DIR, '/taxonomy'); const fileMap: Record = {}; async function walkTree(dir: string) { diff --git a/src/app/api/native/pr/knowledge/route.ts b/src/app/api/native/pr/knowledge/route.ts index 51549f81..a999d971 100644 --- a/src/app/api/native/pr/knowledge/route.ts +++ b/src/app/api/native/pr/knowledge/route.ts @@ -10,12 +10,12 @@ import { KnowledgeYamlData } from '@/types'; import yaml from 'js-yaml'; // Define paths and configuration -const TAXONOMY_ROOT_DIR = process.env.NEXT_PUBLIC_TAXONOMY_ROOT_DIR || './.instructlab-ui'; +const LOCAL_TAXONOMY_ROOT_DIR = process.env.NEXT_PUBLIC_LOCAL_TAXONOMY_ROOT_DIR || './.instructlab-ui'; const KNOWLEDGE_DIR = 'knowledge'; export async function POST(req: NextRequest) { - const REPO_DIR = path.join(TAXONOMY_ROOT_DIR, '/taxonomy'); + const REPO_DIR = path.join(LOCAL_TAXONOMY_ROOT_DIR, '/taxonomy'); try { // Extract the data from the request body const { content, attribution, name, email, submissionSummary, filePath } = await req.json(); diff --git a/src/app/api/native/pr/skill/route.ts b/src/app/api/native/pr/skill/route.ts index 72113bd3..c8d2864a 100644 --- a/src/app/api/native/pr/skill/route.ts +++ b/src/app/api/native/pr/skill/route.ts @@ -7,12 +7,12 @@ import path from 'path'; import yaml from 'js-yaml'; // Define paths and configuration -const TAXONOMY_ROOT_DIR = process.env.NEXT_PUBLIC_TAXONOMY_ROOT_DIR || './.instructlab-ui'; +const LOCAL_TAXONOMY_ROOT_DIR = process.env.NEXT_PUBLIC_LOCAL_TAXONOMY_ROOT_DIR || './.instructlab-ui'; const SKILLS_DIR = 'compositional_skills'; export async function POST(req: NextRequest) { - const REPO_DIR = path.join(TAXONOMY_ROOT_DIR, '/taxonomy'); + const REPO_DIR = path.join(LOCAL_TAXONOMY_ROOT_DIR, '/taxonomy'); try { // Extract the QnA data from the request body TODO: what is documentOutline? const { content, attribution, name, email, submissionSummary, documentOutline, filePath } = await req.json(); // eslint-disable-line @typescript-eslint/no-unused-vars diff --git a/src/components/Dashboard/Native/dashboard.tsx b/src/components/Dashboard/Native/dashboard.tsx index 3a66a965..f5ffdf07 100644 --- a/src/components/Dashboard/Native/dashboard.tsx +++ b/src/components/Dashboard/Native/dashboard.tsx @@ -14,19 +14,50 @@ import { EmptyState, EmptyStateBody, EmptyStateFooter, EmptyStateActions } from import GithubIcon from '@patternfly/react-icons/dist/esm/icons/github-icon'; import Image from 'next/image'; import { useRouter } from 'next/navigation'; +import { TrashIcon } from '@patternfly/react-icons/dist/esm/icons/trash-icon'; +import { Tooltip } from '@patternfly/react-core/dist/esm/components/Tooltip/Tooltip'; +import { CatalogIcon } from '@patternfly/react-icons/dist/esm/icons/catalog-icon'; +import { AlertGroup } from '@patternfly/react-core/dist/esm/components/Alert/AlertGroup'; +import { Alert, AlertProps, AlertVariant } from '@patternfly/react-core/dist/esm/components/Alert/Alert'; +import { AlertActionCloseButton } from '@patternfly/react-core/dist/esm/components/Alert/AlertActionCloseButton'; +import { PencilAltIcon } from '@patternfly/react-icons/dist/esm/icons/pencil-alt-icon'; +import { UploadIcon } from '@patternfly/react-icons/dist/esm/icons/upload-icon'; +import { ModalHeader } from '@patternfly/react-core/dist/esm/components/Modal/ModalHeader'; +import { ModalBody } from '@patternfly/react-core/dist/esm/components/Modal/ModalBody'; +import { FormGroup } from '@patternfly/react-core/dist/esm/components/Form/FormGroup'; +import { Form } from '@patternfly/react-core/dist/esm/components/Form/Form'; +import { TextInput } from '@patternfly/react-core/dist/esm/components/TextInput/TextInput'; +import { ModalFooter } from '@patternfly/react-core/dist/esm/components/Modal/ModalFooter'; const InstructLabLogo: React.FC = () => InstructLab Logo; const DashboardNative: React.FunctionComponent = () => { const [branches, setBranches] = React.useState<{ name: string; creationDate: number }[]>([]); + const [selectedTaxonomyRepoDir, setSelectedTaxonomyRepoDir] = React.useState(''); + const [defaultTaxonomyRepoDir, setDefaultTaxonomyRepoDir] = React.useState(''); const [isLoading, setIsLoading] = React.useState(true); const [mergeStatus] = React.useState<{ branch: string; message: string; success: boolean } | null>(null); const [diffData, setDiffData] = React.useState<{ branch: string; changes: { file: string; status: string }[] } | null>(null); const [isModalOpen, setIsModalOpen] = React.useState(false); + const [alerts, setAlerts] = React.useState[]>([]); + const [isDeleteModalOpen, setIsDeleteModalOpen] = React.useState(false); + const [isPublishModalOpen, setIsPublishModalOpen] = React.useState(false); + const [selectedBranch, setSelectedBranch] = React.useState(null); + + const getUniqueId = () => new Date().getTime(); + const router = useRouter(); // Fetch branches from the API route React.useEffect(() => { + const getEnvVariables = async () => { + const res = await fetch('/api/envConfig'); + const envConfig = await res.json(); + setDefaultTaxonomyRepoDir(envConfig.TAXONOMY_REPO_DIR); + setSelectedTaxonomyRepoDir(envConfig.TAXONOMY_REPO_DIR); + }; + getEnvVariables(); + cloneNativeTaxonomyRepo().then((success) => { if (success) { fetchBranches(); @@ -34,6 +65,22 @@ const DashboardNative: React.FunctionComponent = () => { }); }, []); + const addAlert = (title: string, variant: AlertProps['variant'], key: React.Key) => { + setAlerts((prevAlerts) => [...prevAlerts, { title, variant, key }]); + }; + + const removeAlert = (key: React.Key) => { + setAlerts((prevAlerts) => [...prevAlerts.filter((alert) => alert.key !== key)]); + }; + + const addSuccessAlert = (message: string) => { + addAlert(message, 'success', getUniqueId()); + }; + + const addDangerAlert = (message: string) => { + addAlert(message, 'danger', getUniqueId()); + }; + const fetchBranches = async () => { try { const response = await fetch('/api/native/git/branches'); @@ -121,6 +168,93 @@ const DashboardNative: React.FunctionComponent = () => { } }; + const handleDeleteContribution = async (branchName: string) => { + setSelectedBranch(branchName); + setIsDeleteModalOpen(true); + }; + + const handleDeleteContributionConfirm = async () => { + if (selectedBranch) { + await deleteContribution(selectedBranch); + setIsDeleteModalOpen(false); + } + }; + + const handleDeleteContributionCancel = () => { + setSelectedBranch(null); + setIsDeleteModalOpen(false); + }; + + const deleteContribution = async (branchName: string) => { + try { + const response = await fetch('/api/native/git/branches', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ branchName, action: 'delete' }) + }); + + const result = await response.json(); + if (response.ok) { + // Remove the branch from the list + setBranches((prevBranches) => prevBranches.filter((branch) => branch.name !== branchName)); + addSuccessAlert(result.message); + } else { + console.error(result.error); + addDangerAlert(result.error); + } + } catch (error) { + if (error instanceof Error) { + const errorMessage = 'Error deleting branch ' + branchName + ':' + error.message; + console.error(errorMessage); + addDangerAlert(errorMessage); + } else { + console.error('Unknown error deleting the contribution ${branchName}'); + addDangerAlert('Unknown error deleting the contribution ${branchName}'); + } + } + }; + const handleEditContribution = async (branchName: string) => { + setSelectedBranch(branchName); + setIsDeleteModalOpen(true); + }; + + const handlePublishContribution = async (branchName: string) => { + setSelectedBranch(branchName); + setIsPublishModalOpen(true); + }; + + const handlePublishContributionConfirm = async () => { + if (selectedBranch) { + try { + const response = await fetch('/api/native/git/branches', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ branchName: selectedBranch, action: 'publish', remoteTaxonomyRepoDir: selectedTaxonomyRepoDir }) + }); + + const result = await response.json(); + if (response.ok) { + addSuccessAlert(result.message); + setSelectedTaxonomyRepoDir(defaultTaxonomyRepoDir); + setSelectedBranch(null); + setIsPublishModalOpen(false); + } else { + console.error('Failed to publish the contribution:', result.error); + } + } catch (error) { + console.error('Error while publishing the contribution:', error); + } + } else { + addDangerAlert('No branch selected to publish'); + } + }; + + const handlePublishContributionCancel = () => { + setSelectedTaxonomyRepoDir(defaultTaxonomyRepoDir); + setSelectedBranch(null); + setIsPublishModalOpen(false); + }; + return (
@@ -135,6 +269,17 @@ const DashboardNative: React.FunctionComponent = () => { + + {alerts.map(({ key, variant, title }) => ( + removeAlert(key!)} />} + key={key} + /> + ))} + {isLoading ? ( ) : branches.length === 0 ? ( @@ -149,10 +294,10 @@ const DashboardNative: React.FunctionComponent = () => { - - */} - + Show Changes
}> + , + + ]} + > +

are you sure you want to delete this contribution?

+ + + setIsPublishModalOpen(false)} + aria-labelledby="form-modal-title" + aria-describedby="modal-box-description-form" + > + + + + + + + + + );