diff --git a/.env.example b/.env.example index b52e8d0f..c26bfa9c 100644 --- a/.env.example +++ b/.env.example @@ -11,7 +11,9 @@ IL_GRANITE_API= IL_GRANITE_MODEL_NAME= IL_MERLINITE_API= IL_MERLINITE_MODEL_NAME= -IL_UI_DEPLOYMENT=dev ## Comment it out if it's not a dev deployment + +IL_UI_DEPLOYMENT=native # Two deployment modes are available: github and native +IL_ENABLE_DEV_MODE=true #Enable this option if you want to enable UI features that helps in development, such as form Auto-Fill feature. GITHUB_TOKEN= TAXONOMY_DOCUMENTS_REPO=github.com/instructlab-public/taxonomy-knowledge-docs @@ -19,7 +21,6 @@ NEXT_PUBLIC_AUTHENTICATION_ORG= NEXT_PUBLIC_TAXONOMY_REPO_OWNER= NEXT_PUBLIC_TAXONOMY_REPO= +NEXT_PUBLIC_TAXONOMY_REPO_DIR= + NEXT_PUBLIC_EXPERIMENTAL_FEATURES=false -# The following requires experimental and dev mode to be enabled -# NEXT_PUBLIC_BASE_CLONE_DIRECTORY=/base/path/ -# NEXT_PUBLIC_LOCAL_REPO_PATH=/base/path/cloned_dir_name diff --git a/.gitignore b/.gitignore index e00add2b..782698c0 100644 --- a/.gitignore +++ b/.gitignore @@ -27,7 +27,9 @@ pathservice.pid /blob-report/ /playwright/.cache/ playwright/.auth +.instructlab-ui # dont track secrets in git deploy/k8s/overlays/kind/umami/umami-secret.yaml deploy/k8s/overlays/openshift/umami/qa/umami-secret.yaml deploy/k8s/overlays/openshift/umami/prod/umami-secret.yaml + diff --git a/docs/development.md b/docs/development.md index b18c55ee..39a85af4 100644 --- a/docs/development.md +++ b/docs/development.md @@ -2,6 +2,45 @@ This is a [NextJS](https://nextjs.org) framework with [Patternfly](https://www.patternfly.org/get-started/develop/) UI library components. +## Deploying the UI stack on your local machine + +Podman is a requirement. Install and init instructions [here](https://podman.io/docs/installation). + +Set the .env in the ui directory and make sure you uncomment the `IL_ENABLE_DEV_MODE=true`. `IL_ENABLE_DEV_MODE` flag enables assistive features that help you automate the time consuming and repetitive tasks, such as filling skill and knowledge forms for testing. Once .env file is setup, run the following: + +```bash +make start-dev-local +``` + +This will start the UI and the dependent pathservice locally on the machine. + +> [!NOTE] +> It might ask for permission to allow to listen on port 4000. + +To stop the the local dev environment run the following: + +```bash +make stop-dev-local +``` + +## Deploying the UI stack in KIND cluster + +Set the .env in the ui directory and run the following: + +```bash +make start-dev-kind +``` + +This will start the Kind cluster and deploy the UI stack related manifest files in the cluster. + +To stop the Kind cluster and delete the UI stack related resources, run the following: + +```bash +make stop-dev-kind +``` + +Use `make help` to see all the available commands. + ## Manually Running the React UI Set the .env in the ui directory and run the following: @@ -15,7 +54,7 @@ npm run build npm run start ``` -## Other helpful NPM Commands +### Other helpful NPM Commands ```bash # Run a production build (outputs to ".next" dir) @@ -38,53 +77,20 @@ npm run type-check ``` -## Make scripts - -Podman is a requirement. Install and init instructions [here](https://podman.io/docs/installation). +UI stack supports two mode of deployments: -```bash -# Run markdown linter -make md-lint -``` +- github - This is the default mode and it allows users to push their knowledge and skill contribution to the github taxonomy repository. +- native - This mode allow users to keep the skill and knowledge contribution in their local machine. -## Deploying the UI stack on your local machine +## Running the UI in Native Deployment Mode -Set the .env in the ui directory and make sure you uncomment the `IL_UI_DEPLOYMENT=dev`. Once .env file is setup, run the following: +To enable the native mode, set the `IL_UI_DEPLOYMENT=native` in the .env file. Once the flag is set, the UI will not push the knowledge and skill contribution to the github repository. Instead, it will keep the contribution in the local machine. In the `native` mode, the UI login page will show username and password input box to authenticate the user. You can setup the username and password in the .env file through the `IL_UI_ADMIN_USERNAME` and `IL_UI_ADMIN_USERNAME` flags. -```bash -make start-dev-local -``` +## Running the UI in Github Deployment Mode -This will start the UI and the dependent pathservice locally on the machine. - -> [!NOTE] -> It might ask for permission to allow to listen on port 4000. - -To stop the the local dev environment run the following: - -```bash -make stop-dev-local -``` - -## Deploying the UI stack in KIND cluster - -Set the .env in the ui directory and run the following: - -```bash -make start-dev-kind -``` - -This will start the Kind cluster and deploy the UI stack related manifest files in the cluster. - -To stop the Kind cluster and delete the UI stack related resources, run the following: - -```bash -make stop-dev-kind -``` - -Use `make help` to see all the available commands. +To enable the github mode, set the `IL_UI_DEPLOYMENT=github` in the .env file. Once the flag is set, the UI will push the knowledge and skill contribution to the github taxonomy repository. In the `github` mode, the UI login page will show the github login button to authenticate the user. -## OAuth Configuration +### OAuth Configuration for Github Deployment Mode You can either set up the Oauth app in your [GitHub](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app) @@ -140,7 +146,7 @@ The chat interface should now use the server. ![enter image description here](../public/dev-local-chat-server/successful-chat.png) -### How to Cherry-Pick a Merged PR to `release-1.0` +## How to Cherry-Pick a Merged PR to `release-1.0` Until we finish automating releases, you may be asked to cherry-pick your PR after it is merged. Here are instructions for cherry-picking a merged Pull Request to the `release-1.0` branch. @@ -223,7 +229,7 @@ If there are multiple commits associated with the PR, repeat this command for ea - Navigate to your GitHub repository and create a new Pull Request from your cherry-pick branch (`cherry-pick-pr--release-1.0`) into the `release-1.0` branch. -### How to Run Playwright tests locally +## How to Run Playwright tests locally As a developer, you can add more integration (end to end tests) after you develop your feature. We use [playwright](https://playwright.dev/) as the automation test runner for executing integration tests on our app. To execute playwright tests locally run the following command: @@ -252,7 +258,7 @@ The configuration for playwright tests is defined in `playwright.config` file an If you'd like to run a specific single test, use the following command with the appropriate folder path to your test. Example: `npx playwright test tests/routing.spec.ts`. To get a detailed report of the completed tests, run `npx playwright show-report` and you'll get a detailed view. -### How to use the devcontainer +## How to use the devcontainer ** NOTE: requires the `devcontainer` binary @@ -285,6 +291,6 @@ This will generate the new encrypted sealed-secret manifest in the file you spec BE CERTAIN to delete the un-encrypted secret file, we do not want to leak these values in `git`. Finally you can move the `sealed-secret` to its correct location within this repo. -### Common issues +## Common issues - `error: cannot get sealed secret service: Unauthorized`: You must be signed in to the qa cluster to be able to communicate with the sealed secrets controller. diff --git a/src/app/api/envConfig/route.ts b/src/app/api/envConfig/route.ts index c58b2a65..39515ea6 100644 --- a/src/app/api/envConfig/route.ts +++ b/src/app/api/envConfig/route.ts @@ -15,7 +15,9 @@ export async function GET() { UPSTREAM_REPO_OWNER: process.env.NEXT_PUBLIC_TAXONOMY_REPO_OWNER || '', UPSTREAM_REPO_NAME: process.env.NEXT_PUBLIC_TAXONOMY_REPO || '', DEPLOYMENT_TYPE: process.env.IL_UI_DEPLOYMENT || '', - EXPERIMENTAL_FEATURES: process.env.NEXT_PUBLIC_EXPERIMENTAL_FEATURES || '' + ENABLE_DEV_MODE: process.env.IL_ENABLE_DEV_MODE || 'false', + 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/local/clone-repo/route.ts b/src/app/api/local/clone-repo/route.ts deleted file mode 100644 index 309582c5..00000000 --- a/src/app/api/local/clone-repo/route.ts +++ /dev/null @@ -1,45 +0,0 @@ -// src/pages/api/clone-repo.ts -import { NextRequest, NextResponse } from 'next/server'; -import * as git from 'isomorphic-git'; -import http from 'isomorphic-git/http/node'; -import fs from 'fs'; -import path from 'path'; - -// Retrieve the base directory from the environment variable -const BASE_DIRECTORY = process.env.NEXT_PUBLIC_BASE_CLONE_DIRECTORY; - -export async function POST(req: NextRequest) { - const { repoUrl, directory } = await req.json(); - - if (!repoUrl || !directory) { - return NextResponse.json({ message: 'Repository URL and directory are required' }, { status: 400 }); - } - - if (!BASE_DIRECTORY) { - return NextResponse.json({ message: 'Base directory is not configured on the server' }, { status: 500 }); - } - - try { - const clonePath = path.resolve(BASE_DIRECTORY, directory); - - // Ensure clonePath is within BASE_DIRECTORY - if (!clonePath.startsWith(BASE_DIRECTORY)) { - return NextResponse.json({ message: 'Invalid directory path' }, { status: 403 }); - } - - await git.clone({ - fs, - http, - dir: clonePath, - url: repoUrl, - singleBranch: true, - depth: 1 - }); - - // Include the full path in the response for client display - return NextResponse.json({ message: `Repository cloned successfully.`, fullPath: clonePath }, { status: 200 }); - } catch (error: unknown) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; - return NextResponse.json({ message: `Failed to clone repository: ${errorMessage}` }, { status: 500 }); - } -} diff --git a/src/app/api/local/git/branches/route.ts b/src/app/api/local/git/branches/route.ts deleted file mode 100644 index 2558f6f2..00000000 --- a/src/app/api/local/git/branches/route.ts +++ /dev/null @@ -1,142 +0,0 @@ -// src/app/api/local/git/branches/route.ts -import { NextRequest, NextResponse } from 'next/server'; -import * as git from 'isomorphic-git'; -import fs from 'fs'; -import path from 'path'; - -// Get the repository path from the environment variable -const REPO_DIR = process.env.NEXT_PUBLIC_LOCAL_REPO_PATH || '/path/to/local/repo'; - -export async function GET() { - try { - // Ensure the repository path exists - if (!fs.existsSync(REPO_DIR)) { - return NextResponse.json({ error: 'Repository path does not exist.' }, { status: 400 }); - } - - // List all branches in the repository - const branches = await git.listBranches({ fs, dir: REPO_DIR }); - const branchDetails = []; - - for (const branch of branches) { - const branchCommit = await git.resolveRef({ fs, dir: REPO_DIR, ref: branch }); - const commitDetails = await git.readCommit({ fs, dir: REPO_DIR, oid: branchCommit }); - - branchDetails.push({ - name: branch, - creationDate: commitDetails.commit.committer.timestamp * 1000 // Convert to milliseconds - }); - } - - branchDetails.sort((a, b) => b.creationDate - a.creationDate); // Sort by creation date, newest first - - return NextResponse.json({ branches: branchDetails }, { status: 200 }); - } catch (error) { - console.error('Failed to list branches:', error); - return NextResponse.json({ error: 'Failed to list branches' }, { status: 500 }); - } -} - -// Handle POST requests for merge or branch comparison -export async function POST(req: NextRequest) { - const { branchName, action } = await req.json(); - - try { - if (action === 'merge') { - // Ensure valid branch name - if (!branchName || branchName === 'main') { - return NextResponse.json({ error: 'Invalid branch name for merge' }, { 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' }); - - // 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 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 }); - } - - // 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 }); - - const mainFiles = await getFilesFromTree(mainCommit); - const branchFiles = await getFilesFromTree(branchCommit); - - const changes = []; - - // 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 NextResponse.json({ changes }, { status: 200 }); - } else { - return NextResponse.json({ error: 'Invalid action specified' }, { status: 400 }); - } - } catch (error) { - console.error(`Failed to ${action === 'merge' ? 'merge branch' : 'compare branches'}:`, error); - return NextResponse.json( - { - error: `Failed to ${action === 'merge' ? 'merge branch' : 'compare branches'}` - }, - { status: 500 } - ); - } finally { - // Ensure switching back to 'main' branch after any operation - try { - await git.checkout({ fs, dir: REPO_DIR, ref: 'main' }); - } catch (checkoutError) { - console.error('Failed to switch back to main branch:', checkoutError); - } - } -} - -// Helper function to recursively gather file paths and their oids from a tree -async function getFilesFromTree(commitOid: string) { - const fileMap: Record = {}; - - async function walkTree(dir: string) { - const tree = await git.readTree({ fs, dir: REPO_DIR, oid: commitOid, filepath: dir }); - - for (const entry of tree.tree) { - const fullPath = path.join(dir, entry.path); - if (entry.type === 'blob') { - fileMap[fullPath] = entry.oid; - } else if (entry.type === 'tree') { - await walkTree(fullPath); // Recursively walk subdirectories - } - } - } - - await walkTree(''); - return fileMap; -} diff --git a/src/app/api/native/clone-repo/route.ts b/src/app/api/native/clone-repo/route.ts new file mode 100644 index 00000000..cf6a3cd2 --- /dev/null +++ b/src/app/api/native/clone-repo/route.ts @@ -0,0 +1,42 @@ +// src/pages/api/clone-repo.ts +import { NextResponse } from 'next/server'; +import * as git from 'isomorphic-git'; +import http from 'isomorphic-git/http/node'; +import fs from 'fs'; +import path from 'path'; + +// Retrieve the base directory from the environment variable +const LOCAL_TAXONOMY_ROOT_DIR = process.env.NEXT_PUBLIC_LOCAL_TAXONOMY_ROOT_DIR || `${process.env.HOME}/.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(LOCAL_TAXONOMY_ROOT_DIR, '/taxonomy'); + + if (fs.existsSync(taxonomyDirectoryPath)) { + const files = fs.readdirSync(taxonomyDirectoryPath); + if (files.length > 0) { + console.log(`Using existing native Taxonomy repository at ${taxonomyDirectoryPath}.`); + return NextResponse.json({ message: `Using existing native Taxonomy repository at ${taxonomyDirectoryPath}.` }, { status: 200 }); + } + fs.rmdirSync(taxonomyDirectoryPath, { recursive: true }); + } + + try { + await git.clone({ + fs, + http, + dir: taxonomyDirectoryPath, + url: TAXONOMY_REPO_URL, + singleBranch: true, + depth: 1 + }); + + // Include the full path in the response for client display + 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}`); + return NextResponse.json({ message: `Failed to clone taxonomy repository: ${errorMessage}` }, { status: 500 }); + } +} diff --git a/src/app/api/native/git/branches/route.ts b/src/app/api/native/git/branches/route.ts new file mode 100644 index 00000000..d79fa18d --- /dev/null +++ b/src/app/api/native/git/branches/route.ts @@ -0,0 +1,294 @@ +// src/app/api/native/git/branches/route.ts +import { NextRequest, NextResponse } from 'next/server'; +import * as git from 'isomorphic-git'; +import fs from 'fs'; +import path from 'path'; + +// Get the repository path from the environment variable +const LOCAL_TAXONOMY_ROOT_DIR = process.env.NEXT_PUBLIC_LOCAL_TAXONOMY_ROOT_DIR || `${process.env.HOME}/.instructlab-ui`; + +interface Diffs { + file: string; + status: string; +} + +export async function GET() { + const REPO_DIR = path.join(LOCAL_TAXONOMY_ROOT_DIR, '/taxonomy'); + try { + // Ensure the repository path exists + if (!fs.existsSync(REPO_DIR)) { + return NextResponse.json({ error: 'Repository path does not exist.' }, { status: 400 }); + } + + // List all branches in the repository + const branches = await git.listBranches({ fs, dir: REPO_DIR }); + const branchDetails = []; + + for (const branch of branches) { + const branchCommit = await git.resolveRef({ fs, dir: REPO_DIR, ref: branch }); + const commitDetails = await git.readCommit({ fs, dir: REPO_DIR, oid: branchCommit }); + + branchDetails.push({ + name: branch, + creationDate: commitDetails.commit.committer.timestamp * 1000 // Convert to milliseconds + }); + } + + branchDetails.sort((a, b) => b.creationDate - a.creationDate); // Sort by creation date, newest first + + console.log('Branches present in native taxonomy:', branchDetails); + return NextResponse.json({ branches: branchDetails }, { status: 200 }); + } catch (error) { + console.error('Failed to list branches:', error); + return NextResponse.json({ error: 'Failed to list branches' }, { status: 500 }); + } +} + +// Handle POST requests for merge or branch comparison +export async function POST(req: NextRequest) { + 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 (!branchName || branchName === 'main') { + return NextResponse.json({ error: 'Invalid branch name for deletion' }, { status: 400 }); + } + + // Delete the target branch + await git.deleteBranch({ fs, dir: localTaxonomyDir, ref: branchName }); + + 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 }); + + 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; +} + +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 + }); + + if (!topCommit) { + throw new Error('No commits found in the repository.'); + } + + // Extract commit message + const commitMessage = topCommit.commit.message; + + // 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.`); + } + + 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); + } + + 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({ message: `No changes to publish from ${branchName}.` }, { status: 200 }); + } + } catch (error) { + console.error(`Failed to publish contribution from ${branchName}:`, error); + return NextResponse.json( + { + 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: localTaxonomyDir, ref: 'main' }); + } catch (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(LOCAL_TAXONOMY_ROOT_DIR, '/taxonomy'); + const fileMap: Record = {}; + + async function walkTree(dir: string) { + const tree = await git.readTree({ fs, dir: REPO_DIR, oid: commitOid, filepath: dir }); + + for (const entry of tree.tree) { + const fullPath = path.join(dir, entry.path); + if (entry.type === 'blob') { + fileMap[fullPath] = entry.oid; + } else if (entry.type === 'tree') { + await walkTree(fullPath); // Recursively walk subdirectories + } + } + } + + await walkTree(''); + return fileMap; +} diff --git a/src/app/api/local/pr/knowledge/route.ts b/src/app/api/native/pr/knowledge/route.ts similarity index 92% rename from src/app/api/local/pr/knowledge/route.ts rename to src/app/api/native/pr/knowledge/route.ts index 353a04c4..67cd6f8d 100644 --- a/src/app/api/local/pr/knowledge/route.ts +++ b/src/app/api/native/pr/knowledge/route.ts @@ -1,4 +1,4 @@ -// src/app/api/local/pr/knowledge/route.ts +// src/app/api/native/pr/knowledge/route.ts import { NextResponse } from 'next/server'; import { NextRequest } from 'next/server'; @@ -10,10 +10,12 @@ import { KnowledgeYamlData } from '@/types'; import yaml from 'js-yaml'; // Define paths and configuration -const REPO_DIR = process.env.NEXT_PUBLIC_LOCAL_REPO_PATH || '/path/to/local/repo'; // Update with actual local path +const LOCAL_TAXONOMY_ROOT_DIR = process.env.NEXT_PUBLIC_LOCAL_TAXONOMY_ROOT_DIR || `${process.env.HOME}/.instructlab-ui`; + const KNOWLEDGE_DIR = 'knowledge'; export async function POST(req: NextRequest) { + 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/local/pr/skill/route.ts b/src/app/api/native/pr/skill/route.ts similarity index 91% rename from src/app/api/local/pr/skill/route.ts rename to src/app/api/native/pr/skill/route.ts index 873da074..3d621899 100644 --- a/src/app/api/local/pr/skill/route.ts +++ b/src/app/api/native/pr/skill/route.ts @@ -1,4 +1,4 @@ -// src/app/api/local/pr/skill/route.ts +// src/app/api/native/pr/skill/route.ts import { NextResponse } from 'next/server'; import { NextRequest } from 'next/server'; import * as git from 'isomorphic-git'; @@ -7,10 +7,12 @@ import path from 'path'; import yaml from 'js-yaml'; // Define paths and configuration -const REPO_DIR = process.env.NEXT_PUBLIC_LOCAL_REPO_PATH || '/path/to/local/repo'; // Update with actual local path +const LOCAL_TAXONOMY_ROOT_DIR = process.env.NEXT_PUBLIC_LOCAL_TAXONOMY_ROOT_DIR || `${process.env.HOME}/.instructlab-ui`; + const SKILLS_DIR = 'compositional_skills'; export async function POST(req: NextRequest) { + 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/app/api/native/upload/route.ts b/src/app/api/native/upload/route.ts new file mode 100644 index 00000000..b67a452f --- /dev/null +++ b/src/app/api/native/upload/route.ts @@ -0,0 +1,98 @@ +// src/app/api/native/upload/route.ts +import { NextResponse } from 'next/server'; +import { NextRequest } from 'next/server'; +import * as git from 'isomorphic-git'; +import http from 'isomorphic-git/http/node'; +import path from 'path'; +import fs from 'fs'; + +const LOCAL_TAXONOMY_DOCS_ROOT_DIR = process.env.NEXT_PUBLIC_LOCAL_TAXONOMY_ROOT_DIR || `${process.env.HOME}/.instructlab-ui`; +const TAXONOMY_KNOWLEDGE_DOCS_REPO_URL = 'https://github.com/instructlab-public/taxonomy-knowledge-docs.git'; + +export async function POST(req: NextRequest) { + try { + const body = await req.json(); + const { files } = body; + const docsRepoUrl = await cloneTaxonomyDocsRepo(); + + // If the repository was not cloned, return an error + if (!docsRepoUrl) { + return NextResponse.json({ error: 'Failed to clone taxonomy knowledge docs repository' }, { status: 500 }); + } + + const timestamp = new Date().toISOString().replace(/[-:.]/g, '').replace('T', 'T').slice(0, -1); + const filesWithTimestamp = files.map((file: { fileName: string; fileContent: string }) => { + const [name, extension] = file.fileName.split(/\.(?=[^.]+$)/); + return { + fileName: `${name}-${timestamp}.${extension}`, + fileContent: file.fileContent + }; + }); + + // Write the files to the repository + for (const file of filesWithTimestamp) { + const filePath = path.join(docsRepoUrl, file.fileName); + fs.writeFileSync(filePath, file.fileContent); + } + + // Checkout the main branch + await git.checkout({ fs, dir: docsRepoUrl, ref: 'main' }); + + // Stage the files + await git.add({ fs, dir: docsRepoUrl, filepath: '.' }); + + // Commit the files + const commitSha = await git.commit({ + fs, + dir: docsRepoUrl, + author: { name: 'instructlab-ui', email: 'ui@instructlab.ai' }, + message: `Add files: ${files + .map((file: { fileName: string; fileContent: string }) => file.fileName) + .join(', ')}\n\nSigned-off-by: ui@instructlab.ai` + }); + + return NextResponse.json( + { + repoUrl: docsRepoUrl, + commitSha, + documentNames: filesWithTimestamp.map((file: { fileName: string }) => file.fileName), + prUrl: '' + }, + { status: 201 } + ); + } catch (error) { + console.error('Failed to upload documents:', error); + return NextResponse.json({ error: 'Failed to upload documents' }, { status: 500 }); + } +} + +async function cloneTaxonomyDocsRepo() { + const taxonomyDocsDirectoryPath = path.join(LOCAL_TAXONOMY_DOCS_ROOT_DIR, '/taxonomy-knowledge-docs'); + console.log(`Cloning taxonomy docs repository to ${taxonomyDocsDirectoryPath}...`); + + if (fs.existsSync(taxonomyDocsDirectoryPath)) { + console.log(`Using existing taxonomy knowledge docs repository at ${taxonomyDocsDirectoryPath}.`); + return taxonomyDocsDirectoryPath; + } else { + console.log(`Taxonomy knowledge docs repository not found at ${taxonomyDocsDirectoryPath}. Cloning...`); + } + + try { + await git.clone({ + fs, + http, + dir: taxonomyDocsDirectoryPath, + url: TAXONOMY_KNOWLEDGE_DOCS_REPO_URL, + singleBranch: true, + depth: 1 + }); + + // Include the full path in the response for client display + console.log(`Repository cloned successfully to ${taxonomyDocsDirectoryPath}.`); + return taxonomyDocsDirectoryPath; + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; + console.error(`Failed to clone taxonomy docs repository: ${errorMessage}`); + return null; + } +} diff --git a/src/app/contribute/knowledge/page.tsx b/src/app/contribute/knowledge/page.tsx index cab86364..19836b45 100644 --- a/src/app/contribute/knowledge/page.tsx +++ b/src/app/contribute/knowledge/page.tsx @@ -1,14 +1,11 @@ // src/app/contribute/knowledge/page.tsx +import KnowledgeFormNative from '@/components/Contribute/Knowledge/Native'; import * as React from 'react'; import { AppLayout } from '../../../components/AppLayout'; -import { KnowledgeForm } from '../../../components/Contribute/Knowledge'; +import { KnowledgeFormGithub } from '../../../components/Contribute/Knowledge/Github'; const KnowledgeFormPage: React.FC = () => { - return ( - - - - ); + return {process.env.IL_UI_DEPLOYMENT === 'native' ? : }; }; export default KnowledgeFormPage; diff --git a/src/app/contribute/skill/page.tsx b/src/app/contribute/skill/page.tsx index a2b5d3bc..01f1d1cb 100644 --- a/src/app/contribute/skill/page.tsx +++ b/src/app/contribute/skill/page.tsx @@ -1,14 +1,11 @@ // src/app/contribute/skill/page.tsx +import SkillFormNative from '@/components/Contribute/Skill/Native'; import * as React from 'react'; import { AppLayout } from '../../../components/AppLayout'; -import { SkillForm } from '../../../components/Contribute/Skill'; +import { SkillFormGithub } from '../../../components/Contribute/Skill/Github'; const SkillFormPage: React.FC = () => { - return ( - - - - ); + return {process.env.IL_UI_DEPLOYMENT === 'native' ? : }; }; export default SkillFormPage; diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index 646f1dfd..2d0fa212 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -4,14 +4,21 @@ import * as React from 'react'; import '@patternfly/react-core/dist/styles/base.css'; import { AppLayout } from '@/components/AppLayout'; -import { Index } from '@/components/Dashboard'; - +import { DashboardGithub } from '@/components/Dashboard/Github/dashboard'; +import { DashboardNative } from '@/components/Dashboard/Native/dashboard'; const Home: React.FunctionComponent = () => { - return ( - - - - ); + const [deploymentType, setDeploymentType] = React.useState(); + + React.useEffect(() => { + const getEnvVariables = async () => { + const res = await fetch('/api/envConfig'); + const envConfig = await res.json(); + setDeploymentType(envConfig.DEPLOYMENT_TYPE); + }; + getEnvVariables(); + }, []); + + return {deploymentType === 'native' ? : }; }; export default Home; diff --git a/src/app/experimental/contribute-local/configuration-local/page.tsx b/src/app/experimental/contribute-local/configuration-local/page.tsx deleted file mode 100644 index c7550df8..00000000 --- a/src/app/experimental/contribute-local/configuration-local/page.tsx +++ /dev/null @@ -1,14 +0,0 @@ -// src/app/experimental/contribute-local/clone-repo/page.tsx -import * as React from 'react'; -import { AppLayout } from '@/components/AppLayout'; -import CloneRepoLocal from '@/components/Experimental/CloneRepoLocal/CloneRepoLocal'; - -const CloneRepoPage: React.FC = () => { - return ( - - - - ); -}; - -export default CloneRepoPage; diff --git a/src/app/experimental/contribute-local/knowledge/page.tsx b/src/app/experimental/contribute-local/knowledge/page.tsx deleted file mode 100644 index dc1a4cf2..00000000 --- a/src/app/experimental/contribute-local/knowledge/page.tsx +++ /dev/null @@ -1,14 +0,0 @@ -// src/app/experimental/contribute-local/knowledge/page.tsx -import * as React from 'react'; -import { AppLayout } from '@/components/AppLayout'; -import { KnowledgeFormLocal } from '@/components/Experimental/ContributeLocal/Knowledge'; - -const KnowledgeFormLocalPage: React.FC = () => { - return ( - - - - ); -}; - -export default KnowledgeFormLocalPage; diff --git a/src/app/experimental/contribute-local/skill/page.tsx b/src/app/experimental/contribute-local/skill/page.tsx deleted file mode 100644 index 9bcdc347..00000000 --- a/src/app/experimental/contribute-local/skill/page.tsx +++ /dev/null @@ -1,14 +0,0 @@ -// src/app/experimental/contribute-local/skill/page.tsx -import * as React from 'react'; -import { AppLayout } from '@/components/AppLayout'; -import SkillFormLocal from '@/components/Experimental/ContributeLocal/Skill'; - -const SkillFormPageLocal: React.FC = () => { - return ( - - - - ); -}; - -export default SkillFormPageLocal; diff --git a/src/app/experimental/dashboard-local/page.tsx b/src/app/experimental/dashboard-local/page.tsx deleted file mode 100644 index e1da680f..00000000 --- a/src/app/experimental/dashboard-local/page.tsx +++ /dev/null @@ -1,17 +0,0 @@ -// src/app/experimental/dashboard-local/page.tsx -'use client'; - -import * as React from 'react'; -import '@patternfly/react-core/dist/styles/base.css'; -import { AppLayout } from '@/components/AppLayout'; -import { DashboardLocal } from '@/components/Experimental/DashboardLocal'; - -const Home: React.FunctionComponent = () => { - return ( - - - - ); -}; - -export default Home; diff --git a/src/app/login/locallogin.tsx b/src/app/login/devmodelogin.tsx similarity index 96% rename from src/app/login/locallogin.tsx rename to src/app/login/devmodelogin.tsx index f30e1136..21df3eb0 100644 --- a/src/app/login/locallogin.tsx +++ b/src/app/login/devmodelogin.tsx @@ -1,4 +1,4 @@ -// src/app/login/LocalLogin.tsx +// src/app/login/DevModeLogin.tsx import React, { useState } from 'react'; import { signIn } from 'next-auth/react'; import { Grid, GridItem } from '@patternfly/react-core/dist/dynamic/layouts/Grid'; @@ -12,7 +12,7 @@ import { HelperTextItem } from '@patternfly/react-core/dist/dynamic/components/H import GithubIcon from '@patternfly/react-icons/dist/dynamic/icons/github-icon'; import './githublogin.css'; -const LocalLogin: React.FunctionComponent = () => { +const DevModeLogin: React.FunctionComponent = () => { const [, setShowHelperText] = useState(false); const [username, setUsername] = useState(''); const [isValidUsername, setIsValidUsername] = useState(true); @@ -27,7 +27,7 @@ const LocalLogin: React.FunctionComponent = () => { setIsValidUsername(false); setIsValidPassword(false); } else { - window.location.href = '/'; + window.location.href = '/dashboard'; } }; @@ -40,7 +40,7 @@ const LocalLogin: React.FunctionComponent = () => { }; const handleGitHubLogin = () => { - signIn('github', { callbackUrl: '/' }); + signIn('github', { callbackUrl: '/dashboard' }); }; return ( @@ -158,4 +158,4 @@ const LocalLogin: React.FunctionComponent = () => { ); }; -export default LocalLogin; +export default DevModeLogin; diff --git a/src/app/login/githublogin.tsx b/src/app/login/githublogin.tsx index 4b20c9b8..8281b487 100644 --- a/src/app/login/githublogin.tsx +++ b/src/app/login/githublogin.tsx @@ -11,8 +11,8 @@ import { useRouter, useSearchParams } from 'next/navigation'; import { Modal, ModalVariant } from '@patternfly/react-core/dist/esm/deprecated/components/Modal'; const GithubLogin: React.FC = () => { - const searchParams = useSearchParams(); const router = useRouter(); + const searchParams = useSearchParams(); const [showError, setShowError] = useState(false); const [errorMsg, setErrorMsg] = useState('Something went wrong.'); const [githubUsername, setGithubUsername] = useState(null); diff --git a/src/app/login/nativelogin.tsx b/src/app/login/nativelogin.tsx new file mode 100644 index 00000000..5f0c76e7 --- /dev/null +++ b/src/app/login/nativelogin.tsx @@ -0,0 +1,146 @@ +// src/app/login/NativeLogin.tsx +import React, { useState } from 'react'; +import { signIn } from 'next-auth/react'; +import { Grid, GridItem } from '@patternfly/react-core/dist/dynamic/layouts/Grid'; +import { Content } from '@patternfly/react-core/dist/dynamic/components/Content'; +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 { Button } from '@patternfly/react-core/dist/dynamic/components/Button'; +import { HelperText } from '@patternfly/react-core/dist/dynamic/components/HelperText'; +import { HelperTextItem } from '@patternfly/react-core/dist/dynamic/components/HelperText'; +import './githublogin.css'; + +const NativeLogin: React.FunctionComponent = () => { + const [, setShowHelperText] = useState(false); + const [username, setUsername] = useState(''); + const [isValidUsername, setIsValidUsername] = useState(true); + const [password, setPassword] = useState(''); + const [isValidPassword, setIsValidPassword] = useState(true); + + const handleLogin = async (e: React.FormEvent) => { + e.preventDefault(); + const result = await signIn('credentials', { redirect: false, username, password }); + if (result?.error) { + setShowHelperText(true); + setIsValidUsername(false); + setIsValidPassword(false); + } else { + window.location.href = '/dashboard'; + } + }; + + const handleUsernameChange = (_event: React.FormEvent, value: string) => { + setUsername(value); + }; + + const handlePasswordChange = (_event: React.FormEvent, value: string) => { + setPassword(value); + }; + + return ( +
+ + + + + Login locally with a username and password or via GitHub OAuth + + + + + Join the novel, community-based movement to create truly open-source LLMs + + +
+
+ + + {!isValidUsername && ( + + Invalid Username + + )} + + + + {!isValidPassword && ( + + Invalid password + + )} + + +
+
+ + + + GitHub + {' '} + |{' '} + + Collaborate + {' '} + |{' '} + + Code Of Conduct + + + + + Terms of use + {' '} + |{' '} + + Privacy Policy + + + +
+
+
+ ); +}; + +export default NativeLogin; diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index cf5e59ce..85feca05 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -1,34 +1,43 @@ // src/app/login/page.tsx 'use client'; -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, Suspense } from 'react'; import './githublogin.css'; -import LocalLogin from '@/app/login/locallogin'; +import NativeLogin from '@/app/login/nativelogin'; import GithubLogin from '@/app/login/githublogin'; +import DevModeLogin from './devmodelogin'; const Login: React.FunctionComponent = () => { - const [isProd, setIsProd] = useState(null); + const [deploymentType, setDeploymentType] = useState(); + const [isDevModeEnabled, setIsDevModeEnabled] = useState(false); useEffect(() => { const chooseLoginPage = async () => { try { const res = await fetch('/api/envConfig'); const envConfig = await res.json(); - setIsProd(envConfig.DEPLOYMENT_TYPE !== 'dev'); + setDeploymentType(envConfig.DEPLOYMENT_TYPE); + setIsDevModeEnabled(envConfig.ENABLE_DEV_MODE === 'true'); } catch (error) { console.error('Error fetching environment config:', error); - setIsProd(true); + setDeploymentType('github'); } }; chooseLoginPage(); }, []); - if (isProd === null) { + if (isDevModeEnabled) { + return ; + } + if (deploymentType === 'native') { // Render a loading indicator or null while determining the environment - return null; + return ; } - - return isProd ? : ; + return ( + + + + ); }; export default Login; diff --git a/src/app/page.tsx b/src/app/page.tsx index 6853aa0b..75a82eda 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,11 +1,11 @@ // src/app/page.tsx 'use client'; +import { DashboardGithub } from '@/components/Dashboard/Github/dashboard'; import { GithubAccessPopup } from '@/components/GithubAccessPopup'; import * as React from 'react'; import { useState } from 'react'; import { AppLayout } from '../components/AppLayout'; -import { Index } from '../components/Dashboard'; const HomePage: React.FC = () => { const [isWarningConditionAccepted, setIsWarningConditionAccepted] = useState(false); @@ -19,7 +19,7 @@ const HomePage: React.FC = () => { return ( - {isWarningConditionAccepted && } + {isWarningConditionAccepted && } ); }; diff --git a/src/components/AppLayout.tsx b/src/components/AppLayout.tsx index 3023e6ce..f4a1f944 100644 --- a/src/components/AppLayout.tsx +++ b/src/components/AppLayout.tsx @@ -98,13 +98,7 @@ const AppLayout: React.FunctionComponent = ({ children }) => { }, isExperimentalEnabled && { path: '/experimental', - label: 'Experimental Features', - children: [ - { path: '/experimental/dashboard-local/', label: 'Local Dashboard' }, - { path: '/experimental/contribute-local/skill/', label: 'Local Skill' }, - { path: '/experimental/contribute-local/knowledge/', label: 'Local Knowledge' }, - { path: '/experimental/contribute-local/configuration-local/', label: 'Local Configuration' } - ] + label: 'Experimental Features' } ].filter(Boolean) as Route[]; diff --git a/src/components/Contribute/EditKnowledge/EditKnowledge.tsx b/src/components/Contribute/EditKnowledge/EditKnowledge.tsx index b37a35cf..3a8a2639 100644 --- a/src/components/Contribute/EditKnowledge/EditKnowledge.tsx +++ b/src/components/Contribute/EditKnowledge/EditKnowledge.tsx @@ -8,11 +8,12 @@ import { KnowledgeSchemaVersion } from '@/types/const'; import { fetchPullRequest, fetchFileContent, fetchPullRequestFiles } from '@/utils/github'; import yaml from 'js-yaml'; import axios from 'axios'; -import KnowledgeForm, { KnowledgeEditFormData, KnowledgeFormData, QuestionAndAnswerPair, SeedExample } from '@/components/Contribute/Knowledge'; +import { KnowledgeEditFormData, KnowledgeFormData, QuestionAndAnswerPair, KnowledgeSeedExample } from '@/types'; import { ValidatedOptions } from '@patternfly/react-core/dist/esm/helpers/constants'; import { useEffect, useState } from 'react'; import { Modal, ModalVariant } from '@patternfly/react-core/dist/esm/deprecated/components/Modal/Modal'; import { useRouter } from 'next/navigation'; +import KnowledgeFormGithub from '../Knowledge/Github'; interface EditKnowledgeClientComponentProps { prNumber: number; @@ -83,10 +84,10 @@ const EditKnowledge: React.FC = ({ prNumber } knowledgeExistingFormData.knowledgeDocumentCommit = yamlData.document.commit; knowledgeExistingFormData.documentName = yamlData.document.patterns.join(', '); - const seedExamples: SeedExample[] = []; + const seedExamples: KnowledgeSeedExample[] = []; yamlData.seed_examples.forEach((seed, index) => { // iterate through questions_and_answers and create a new object for each - const example: SeedExample = { + const example: KnowledgeSeedExample = { immutable: index < 5 ? true : false, isExpanded: true, context: seed.context, @@ -175,7 +176,7 @@ const EditKnowledge: React.FC = ({ prNumber } return ( // - + // ); }; diff --git a/src/components/Contribute/EditSkill/EditSkill.tsx b/src/components/Contribute/EditSkill/EditSkill.tsx index f98ae791..3212ae4f 100644 --- a/src/components/Contribute/EditSkill/EditSkill.tsx +++ b/src/components/Contribute/EditSkill/EditSkill.tsx @@ -7,11 +7,11 @@ import { useEffect, useState } from 'react'; import { ValidatedOptions } from '@patternfly/react-core/dist/esm/helpers/constants'; import { Modal, ModalVariant } from '@patternfly/react-core/dist/esm/deprecated/components/Modal/Modal'; import { useRouter } from 'next/navigation'; -import SkillForm, { SkillEditFormData, SkillFormData, SeedExample } from '@/components/Contribute/Skill'; +import SkillFormGithub, { SkillEditFormData } from '@/components/Contribute/Skill/Github'; import { fetchPullRequest, fetchFileContent, fetchPullRequestFiles } from '@/utils/github'; import yaml from 'js-yaml'; import axios from 'axios'; -import { SkillYamlData, AttributionData, PullRequestFile } from '@/types'; +import { SkillYamlData, AttributionData, PullRequestFile, SkillFormData, SkillSeedExample } from '@/types'; import { SkillSchemaVersion } from '@/types/const'; interface EditSkillClientComponentProps { @@ -72,9 +72,9 @@ const EditSkill: React.FC = ({ prNumber }) => { // Populate the form fields with YAML data skillExistingFormData.documentOutline = yamlData.task_description; - const seedExamples: SeedExample[] = []; + const seedExamples: SkillSeedExample[] = []; yamlData.seed_examples.forEach((seed, index) => { - const example: SeedExample = { + const example: SkillSeedExample = { immutable: index < 5 ? true : false, isExpanded: true, context: seed.context || '', @@ -147,7 +147,7 @@ const EditSkill: React.FC = ({ prNumber }) => { ); } - return ; + return ; }; export default EditSkill; diff --git a/src/components/Contribute/Knowledge/AttributionInformation/AttributionInformation.tsx b/src/components/Contribute/Knowledge/AttributionInformation/AttributionInformation.tsx index 87f97851..b606a3ce 100644 --- a/src/components/Contribute/Knowledge/AttributionInformation/AttributionInformation.tsx +++ b/src/components/Contribute/Knowledge/AttributionInformation/AttributionInformation.tsx @@ -5,8 +5,8 @@ import { HelperText } from '@patternfly/react-core/dist/dynamic/components/Helpe import { HelperTextItem } from '@patternfly/react-core/dist/dynamic/components/HelperText'; import ExclamationCircleIcon from '@patternfly/react-icons/dist/dynamic/icons/exclamation-circle-icon'; import { ValidatedOptions } from '@patternfly/react-core/dist/esm/helpers/constants'; -import { KnowledgeFormData } from '..'; import { checkKnowledgeFormCompletion } from '../validation'; +import { KnowledgeFormData } from '@/types'; interface Props { reset: boolean; diff --git a/src/components/Contribute/Knowledge/AutoFill.ts b/src/components/Contribute/Knowledge/AutoFill.ts index 568db39b..7e05911b 100644 --- a/src/components/Contribute/Knowledge/AutoFill.ts +++ b/src/components/Contribute/Knowledge/AutoFill.ts @@ -1,4 +1,4 @@ -import { KnowledgeFormData, QuestionAndAnswerPair, SeedExample } from '.'; +import { KnowledgeFormData, KnowledgeSeedExample, QuestionAndAnswerPair } from '@/types'; import { ValidatedOptions } from '@patternfly/react-core/dist/esm/helpers/constants'; const questionAndAnswerPairs1: QuestionAndAnswerPair[] = [ @@ -127,7 +127,7 @@ const questionAndAnswerPairs5: QuestionAndAnswerPair[] = [ } ]; -const seedExamples: SeedExample[] = [ +const seedExamples: KnowledgeSeedExample[] = [ { immutable: true, isExpanded: true, @@ -238,12 +238,12 @@ const seedExamples: SeedExample[] = [ { immutable: true, isExpanded: true, - context: `Phoenix is the radiant of two annual meteor showers. The Phoenicids, - also known as the December Phoenicids, were first observed on 3 December 1887. - The shower was particularly intense in December 1956, and is thought related - to the breakup of the short-period comet 289P/Blanpain. It peaks around 4–5 - December, though is not seen every year.[58] A very minor meteor shower peaks - around July 14 with around one meteor an hour, though meteors can be seen + context: `Phoenix is the radiant of two annual meteor showers. The Phoenicids, + also known as the December Phoenicids, were first observed on 3 December 1887. + The shower was particularly intense in December 1956, and is thought related + to the breakup of the short-period comet 289P/Blanpain. It peaks around 4–5 + December, though is not seen every year.[58] A very minor meteor shower peaks + around July 14 with around one meteor an hour, though meteors can be seen anytime from July 3 to 18; this shower is referred to as the July Phoenicids.[59]`, isContextValid: ValidatedOptions.success, questionAndAnswers: questionAndAnswerPairs5 diff --git a/src/components/Contribute/Knowledge/DownloadAttribution/DownloadAttribution.tsx b/src/components/Contribute/Knowledge/DownloadAttribution/DownloadAttribution.tsx index 3f0fe267..571682af 100644 --- a/src/components/Contribute/Knowledge/DownloadAttribution/DownloadAttribution.tsx +++ b/src/components/Contribute/Knowledge/DownloadAttribution/DownloadAttribution.tsx @@ -1,7 +1,7 @@ import React from 'react'; -import { KnowledgeFormData } from '..'; import { DropdownItem } from '@patternfly/react-core/dist/esm/components/Dropdown/DropdownItem'; import FileIcon from '@patternfly/react-icons/dist/esm/icons/file-icon'; +import { KnowledgeFormData } from '@/types'; interface Props { knowledgeFormData: KnowledgeFormData; diff --git a/src/components/Contribute/Knowledge/DownloadDropdown/DownloadDropdown.tsx b/src/components/Contribute/Knowledge/DownloadDropdown/DownloadDropdown.tsx index c7aa0dca..2e216ab1 100644 --- a/src/components/Contribute/Knowledge/DownloadDropdown/DownloadDropdown.tsx +++ b/src/components/Contribute/Knowledge/DownloadDropdown/DownloadDropdown.tsx @@ -4,8 +4,8 @@ import { DropdownList } from '@patternfly/react-core/dist/dynamic/components/Dro import { MenuToggle, MenuToggleElement } from '@patternfly/react-core/dist/dynamic/components/MenuToggle'; import DownloadYaml from '../DownloadYaml/DownloadYaml'; import DownloadAttribution from '../DownloadAttribution/DownloadAttribution'; -import { KnowledgeFormData } from '..'; import DownloadIcon from '@patternfly/react-icons/dist/esm/icons/download-icon'; +import { KnowledgeFormData } from '@/types'; interface Props { knowledgeFormData: KnowledgeFormData; diff --git a/src/components/Contribute/Knowledge/DownloadYaml/DownloadYaml.tsx b/src/components/Contribute/Knowledge/DownloadYaml/DownloadYaml.tsx index c5990b9e..78649408 100644 --- a/src/components/Contribute/Knowledge/DownloadYaml/DownloadYaml.tsx +++ b/src/components/Contribute/Knowledge/DownloadYaml/DownloadYaml.tsx @@ -1,6 +1,5 @@ import React from 'react'; -import { KnowledgeFormData } from '..'; -import { KnowledgeYamlData } from '@/types'; +import { KnowledgeFormData, KnowledgeYamlData } from '@/types'; import { KnowledgeSchemaVersion } from '@/types/const'; import { dumpYaml } from '@/utils/yamlConfig'; import { DropdownItem } from '@patternfly/react-core/dist/esm/components/Dropdown/DropdownItem'; diff --git a/src/components/Contribute/Knowledge/DocumentInformation/DocumentInformation.tsx b/src/components/Contribute/Knowledge/Github/DocumentInformation/DocumentInformation.tsx similarity index 98% rename from src/components/Contribute/Knowledge/DocumentInformation/DocumentInformation.tsx rename to src/components/Contribute/Knowledge/Github/DocumentInformation/DocumentInformation.tsx index 53f249d3..788063c4 100644 --- a/src/components/Contribute/Knowledge/DocumentInformation/DocumentInformation.tsx +++ b/src/components/Contribute/Knowledge/Github/DocumentInformation/DocumentInformation.tsx @@ -2,15 +2,15 @@ import React, { useEffect, useState } from 'react'; import { FormFieldGroupHeader, FormGroup, FormHelperText } from '@patternfly/react-core/dist/dynamic/components/Form'; import { Button } from '@patternfly/react-core/dist/dynamic/components/Button'; import { TextInput } from '@patternfly/react-core/dist/dynamic/components/TextInput'; -import { UploadFile } from './../UploadFile'; +import { UploadFile } from '../../UploadFile'; import { Alert, AlertActionLink, AlertActionCloseButton } from '@patternfly/react-core/dist/dynamic/components/Alert'; import { HelperText } from '@patternfly/react-core/dist/dynamic/components/HelperText'; import { HelperTextItem } from '@patternfly/react-core/dist/dynamic/components/HelperText'; import ExclamationCircleIcon from '@patternfly/react-icons/dist/dynamic/icons/exclamation-circle-icon'; import { ValidatedOptions } from '@patternfly/react-core/dist/esm/helpers/constants'; -import { KnowledgeFormData } from '..'; -import { checkKnowledgeFormCompletion } from '../validation'; +import { checkKnowledgeFormCompletion } from '../../validation'; import { Modal, ModalVariant } from '@patternfly/react-core/dist/esm/deprecated/components/Modal/Modal'; +import { KnowledgeFormData } from '@/types'; interface Props { reset: boolean; @@ -158,7 +158,9 @@ const DocumentInformation: React.FC = ({ 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); + if (result.prUrl !== '') { + setSuccessAlertLink(result.prUrl); + } } } }; diff --git a/src/components/Contribute/Knowledge/Submit/Submit.tsx b/src/components/Contribute/Knowledge/Github/Submit/Submit.tsx similarity index 95% rename from src/components/Contribute/Knowledge/Submit/Submit.tsx rename to src/components/Contribute/Knowledge/Github/Submit/Submit.tsx index d0c24bac..9d7416df 100644 --- a/src/components/Contribute/Knowledge/Submit/Submit.tsx +++ b/src/components/Contribute/Knowledge/Github/Submit/Submit.tsx @@ -1,10 +1,10 @@ import React from 'react'; import { Button } from '@patternfly/react-core/dist/dynamic/components/Button'; -import { ActionGroupAlertContent, KnowledgeFormData } from '..'; -import { AttributionData, KnowledgeYamlData } from '@/types'; +import { ActionGroupAlertContent } from '..'; +import { AttributionData, KnowledgeFormData, KnowledgeYamlData } from '@/types'; import { KnowledgeSchemaVersion } from '@/types/const'; import { dumpYaml } from '@/utils/yamlConfig'; -import { validateFields } from '../validation'; +import { validateFields } from '../../validation'; interface Props { disableAction: boolean; diff --git a/src/components/Contribute/Knowledge/Update/Update.tsx b/src/components/Contribute/Knowledge/Github/Update/Update.tsx similarity index 97% rename from src/components/Contribute/Knowledge/Update/Update.tsx rename to src/components/Contribute/Knowledge/Github/Update/Update.tsx index 4824c068..8961e2b7 100644 --- a/src/components/Contribute/Knowledge/Update/Update.tsx +++ b/src/components/Contribute/Knowledge/Github/Update/Update.tsx @@ -1,10 +1,10 @@ import React from 'react'; import { Button } from '@patternfly/react-core/dist/dynamic/components/Button'; -import { ActionGroupAlertContent, KnowledgeFormData } from '..'; -import { AttributionData, KnowledgeYamlData, PullRequestFile } from '@/types'; +import { ActionGroupAlertContent } from '..'; +import { AttributionData, KnowledgeFormData, KnowledgeYamlData, PullRequestFile } from '@/types'; import { KnowledgeSchemaVersion } from '@/types/const'; import { dumpYaml } from '@/utils/yamlConfig'; -import { validateFields } from '../validation'; +import { validateFields } from '../../validation'; import { amendCommit, getGitHubUsername, updatePullRequest } from '@/utils/github'; import { useSession } from 'next-auth/react'; import { useRouter } from 'next/navigation'; diff --git a/src/components/Experimental/ContributeLocal/Knowledge/index.tsx b/src/components/Contribute/Knowledge/Github/index.tsx similarity index 90% rename from src/components/Experimental/ContributeLocal/Knowledge/index.tsx rename to src/components/Contribute/Knowledge/Github/index.tsx index 7a7d03db..36cf3008 100644 --- a/src/components/Experimental/ContributeLocal/Knowledge/index.tsx +++ b/src/components/Contribute/Knowledge/Github/index.tsx @@ -1,7 +1,7 @@ // src/components/Experimental/ContributeLocal/Knowledge/index.tsx 'use client'; import React, { useEffect, useMemo, useState } from 'react'; -import './knowledge.css'; +import '../knowledge.css'; import { Alert, AlertActionCloseButton } from '@patternfly/react-core/dist/dynamic/components/Alert'; import { ActionGroup } from '@patternfly/react-core/dist/dynamic/components/Form'; import { getGitHubUsername } from '@/utils/github'; @@ -10,9 +10,9 @@ import AuthorInformation from '@/components/Contribute/AuthorInformation'; import { FormType } from '@/components/Contribute/AuthorInformation'; import KnowledgeInformation from '@/components/Contribute/Knowledge/KnowledgeInformation/KnowledgeInformation'; import FilePathInformation from '@/components/Contribute/Knowledge/FilePathInformation/FilePathInformation'; -import DocumentInformation from '@/components/Contribute/Knowledge/DocumentInformation/DocumentInformation'; +import DocumentInformation from '@/components/Contribute/Knowledge/Github/DocumentInformation/DocumentInformation'; import AttributionInformation from '@/components/Contribute/Knowledge/AttributionInformation/AttributionInformation'; -import Submit from './SubmitLocal/Submit'; +import Submit from './Submit/Submit'; import { Breadcrumb } from '@patternfly/react-core/dist/dynamic/components/Breadcrumb'; import { BreadcrumbItem } from '@patternfly/react-core/dist/dynamic/components/Breadcrumb'; import { PageBreadcrumb } from '@patternfly/react-core/dist/dynamic/components/Page'; @@ -25,62 +25,15 @@ import { checkKnowledgeFormCompletion } from '@/components/Contribute/Knowledge/ import { ValidatedOptions } from '@patternfly/react-core/dist/esm/helpers/constants'; import { DownloadDropdown } from '@/components/Contribute/Knowledge/DownloadDropdown/DownloadDropdown'; import { ViewDropdown } from '@/components/Contribute/Knowledge/ViewDropdown/ViewDropdown'; -import Update from '@/components/Contribute/Knowledge/Update/Update'; -import { PullRequestFile } from '@/types'; +import Update from '@/components/Contribute/Knowledge/Github/Update/Update'; +import { KnowledgeEditFormData, KnowledgeFormData, QuestionAndAnswerPair } from '@/types'; import { Button } from '@patternfly/react-core/dist/esm/components/Button/Button'; import { useRouter } from 'next/navigation'; import { autoFillKnowledgeFields } from '@/components/Contribute/Knowledge/AutoFill'; import { Spinner } from '@patternfly/react-core/dist/esm/components/Spinner'; import { Wizard, WizardStep } from '@patternfly/react-core/dist/esm/components/Wizard'; import { Content } from '@patternfly/react-core/dist/dynamic/components/Content'; -import ReviewSubmission from '@/components/Experimental/ReviewSubmission'; - -export interface QuestionAndAnswerPair { - immutable: boolean; - question: string; - isQuestionValid: ValidatedOptions; - questionValidationError?: string; - answer: string; - isAnswerValid: ValidatedOptions; - answerValidationError?: string; -} - -export interface SeedExample { - immutable: boolean; - isExpanded: boolean; - context: string; - isContextValid: ValidatedOptions; - validationError?: string; - questionAndAnswers: QuestionAndAnswerPair[]; -} - -export interface KnowledgeFormData { - email: string; - name: string; - submissionSummary: string; - domain: string; - documentOutline: string; - filePath: string; - seedExamples: SeedExample[]; - knowledgeDocumentRepositoryUrl: string; - knowledgeDocumentCommit: string; - documentName: string; - titleWork: string; - linkWork: string; - revision: string; - licenseWork: string; - creators: string; -} - -export interface KnowledgeEditFormData { - isEditForm: boolean; - knowledgeVersion: number; - pullRequestNumber: number; - branchName: string; - yamlFile: PullRequestFile; - attributionFile: PullRequestFile; - knowledgeFormData: KnowledgeFormData; -} +import ReviewSubmission from '@/components/Contribute/Knowledge/ReviewSubmission'; export interface ActionGroupAlertContent { title: string; @@ -95,8 +48,8 @@ export interface KnowledgeFormProps { knowledgeEditFormData?: KnowledgeEditFormData; } -export const KnowledgeFormLocal: React.FunctionComponent = ({ knowledgeEditFormData }) => { - const [deploymentType, setDeploymentType] = useState(); +export const KnowledgeFormGithub: React.FunctionComponent = ({ knowledgeEditFormData }) => { + const [devModeEnabled, setDevModeEnabled] = useState(); const { data: session } = useSession(); const [githubUsername, setGithubUsername] = useState(''); @@ -134,7 +87,7 @@ export const KnowledgeFormLocal: React.FunctionComponent = ( const [activeStepIndex] = useState(1); - const emptySeedExample: SeedExample = { + const emptySeedExample: KnowledgeSeedExample = { immutable: true, isExpanded: false, context: '', @@ -164,7 +117,7 @@ export const KnowledgeFormLocal: React.FunctionComponent = ( ] }; - const [seedExamples, setSeedExamples] = useState([ + const [seedExamples, setSeedExamples] = useState([ emptySeedExample, emptySeedExample, emptySeedExample, @@ -176,7 +129,7 @@ export const KnowledgeFormLocal: React.FunctionComponent = ( const getEnvVariables = async () => { const res = await fetch('/api/envConfig'); const envConfig = await res.json(); - setDeploymentType(envConfig.DEPLOYMENT_TYPE); + setDevModeEnabled(envConfig.ENABLE_DEV_MODE === 'true'); }; getEnvVariables(); }, []); @@ -280,7 +233,7 @@ export const KnowledgeFormLocal: React.FunctionComponent = ( const handleContextInputChange = (seedExampleIndex: number, contextValue: string): void => { setSeedExamples( - seedExamples.map((seedExample: SeedExample, index: number) => + seedExamples.map((seedExample: KnowledgeSeedExample, index: number) => index === seedExampleIndex ? { ...seedExample, @@ -292,7 +245,7 @@ export const KnowledgeFormLocal: React.FunctionComponent = ( }; const handleContextBlur = (seedExampleIndex: number): void => { - const updatedSeedExamples = seedExamples.map((seedExample: SeedExample, index: number): SeedExample => { + const updatedSeedExamples = seedExamples.map((seedExample: KnowledgeSeedExample, index: number): KnowledgeSeedExample => { if (index === seedExampleIndex) { const { msg, status } = validateContext(seedExample.context); return { @@ -308,7 +261,7 @@ export const KnowledgeFormLocal: React.FunctionComponent = ( const handleQuestionInputChange = (seedExampleIndex: number, questionAndAnswerIndex: number, questionValue: string): void => { setSeedExamples( - seedExamples.map((seedExample: SeedExample, index: number) => + seedExamples.map((seedExample: KnowledgeSeedExample, index: number) => index === seedExampleIndex ? { ...seedExample, @@ -328,7 +281,7 @@ export const KnowledgeFormLocal: React.FunctionComponent = ( const handleQuestionBlur = (seedExampleIndex: number, questionAndAnswerIndex: number): void => { setSeedExamples( - seedExamples.map((seedExample: SeedExample, index: number) => + seedExamples.map((seedExample: KnowledgeSeedExample, index: number) => index === seedExampleIndex ? { ...seedExample, @@ -351,7 +304,7 @@ export const KnowledgeFormLocal: React.FunctionComponent = ( const handleAnswerInputChange = (seedExampleIndex: number, questionAndAnswerIndex: number, answerValue: string): void => { setSeedExamples( - seedExamples.map((seedExample: SeedExample, index: number) => + seedExamples.map((seedExample: KnowledgeSeedExample, index: number) => index === seedExampleIndex ? { ...seedExample, @@ -371,7 +324,7 @@ export const KnowledgeFormLocal: React.FunctionComponent = ( const handleAnswerBlur = (seedExampleIndex: number, questionAndAnswerIndex: number): void => { setSeedExamples( - seedExamples.map((seedExample: SeedExample, index: number) => + seedExamples.map((seedExample: KnowledgeSeedExample, index: number) => index === seedExampleIndex ? { ...seedExample, @@ -597,7 +550,7 @@ export const KnowledgeFormLocal: React.FunctionComponent = ( - {deploymentType === 'dev' && ( + {devModeEnabled && ( @@ -652,7 +605,7 @@ export const KnowledgeFormLocal: React.FunctionComponent = ( disableAction={disableAction} knowledgeFormData={knowledgeFormData} setActionGroupAlertContent={setActionGroupAlertContent} - email={email} + githubUsername={githubUsername} resetForm={resetForm} /> )} @@ -667,4 +620,4 @@ export const KnowledgeFormLocal: React.FunctionComponent = ( ); }; -export default KnowledgeFormLocal; +export default KnowledgeFormGithub; diff --git a/src/components/Contribute/Knowledge/KnowledgeInformation/KnowledgeInformation.tsx b/src/components/Contribute/Knowledge/KnowledgeInformation/KnowledgeInformation.tsx index 677d72df..4dc0435c 100644 --- a/src/components/Contribute/Knowledge/KnowledgeInformation/KnowledgeInformation.tsx +++ b/src/components/Contribute/Knowledge/KnowledgeInformation/KnowledgeInformation.tsx @@ -6,8 +6,8 @@ import { HelperText } from '@patternfly/react-core/dist/dynamic/components/Helpe import { HelperTextItem } from '@patternfly/react-core/dist/dynamic/components/HelperText'; import ExclamationCircleIcon from '@patternfly/react-icons/dist/dynamic/icons/exclamation-circle-icon'; import { ValidatedOptions } from '@patternfly/react-core/dist/esm/helpers/constants'; -import { KnowledgeFormData } from '..'; import { checkKnowledgeFormCompletion } from '../validation'; +import { KnowledgeFormData } from '@/types'; interface Props { reset: boolean; diff --git a/src/components/Contribute/Knowledge/KnowledgeQuestionAnswerPairs/KnowledgeQuestionAnswerPairs.tsx b/src/components/Contribute/Knowledge/KnowledgeQuestionAnswerPairs/KnowledgeQuestionAnswerPairs.tsx index e57cc599..e464529d 100644 --- a/src/components/Contribute/Knowledge/KnowledgeQuestionAnswerPairs/KnowledgeQuestionAnswerPairs.tsx +++ b/src/components/Contribute/Knowledge/KnowledgeQuestionAnswerPairs/KnowledgeQuestionAnswerPairs.tsx @@ -2,13 +2,13 @@ import React from 'react'; import { FormFieldGroupHeader, FormGroup, FormHelperText } from '@patternfly/react-core/dist/dynamic/components/Form'; import { TextArea } from '@patternfly/react-core/dist/dynamic/components/TextArea'; import { ExclamationCircleIcon } from '@patternfly/react-icons/dist/dynamic/icons/'; -import { QuestionAndAnswerPair, SeedExample } from '..'; import { ValidatedOptions } from '@patternfly/react-core/dist/esm/helpers/constants'; import { HelperText } from '@patternfly/react-core/dist/dynamic/components/HelperText'; import { HelperTextItem } from '@patternfly/react-core/dist/dynamic/components/HelperText'; +import { KnowledgeSeedExample, QuestionAndAnswerPair } from '@/types'; interface Props { - seedExample: SeedExample; + seedExample: KnowledgeSeedExample; seedExampleIndex: number; handleContextInputChange: (seedExampleIndex: number, contextValue: string) => void; handleContextBlur: (seedExampleIndex: number) => void; diff --git a/src/components/Contribute/Knowledge/KnowledgeSeedExample/KnowledgeSeedExample.tsx b/src/components/Contribute/Knowledge/KnowledgeSeedExample/KnowledgeSeedExample.tsx index 9649763f..5e984b2b 100644 --- a/src/components/Contribute/Knowledge/KnowledgeSeedExample/KnowledgeSeedExample.tsx +++ b/src/components/Contribute/Knowledge/KnowledgeSeedExample/KnowledgeSeedExample.tsx @@ -3,11 +3,11 @@ import React from 'react'; import { Accordion, AccordionItem, AccordionContent, AccordionToggle } from '@patternfly/react-core/dist/dynamic/components/Accordion'; import { FormFieldGroupHeader } from '@patternfly/react-core/dist/dynamic/components/Form'; import KnowledgeQuestionAnswerPairs from '../KnowledgeQuestionAnswerPairs/KnowledgeQuestionAnswerPairs'; -import { SeedExample } from '..'; +import type { KnowledgeSeedExample } from '@/types'; import ExternalLinkAltIcon from '@patternfly/react-icons/dist/esm/icons/external-link-alt-icon'; interface Props { - seedExamples: SeedExample[]; + seedExamples: KnowledgeSeedExample[]; handleContextInputChange: (seedExampleIndex: number, contextValue: string) => void; handleContextBlur: (seedExampleIndex: number) => void; handleQuestionInputChange: (seedExampleIndex: number, questionAndAnswerIndex: number, questionValue: string) => void; @@ -51,7 +51,7 @@ const KnowledgeSeedExample: React.FC = ({ /> - {seedExamples.map((seedExample: SeedExample, seedExampleIndex: number) => ( + {seedExamples.map((seedExample: KnowledgeSeedExample, seedExampleIndex: number) => ( toggleSeedExampleExpansion(seedExampleIndex)} id={`seed-example-toggle-${seedExampleIndex}`}> Seed Example {seedExampleIndex + 1} {seedExample.immutable && *} diff --git a/src/components/Contribute/Knowledge/Native/DocumentInformation/DocumentInformation.tsx b/src/components/Contribute/Knowledge/Native/DocumentInformation/DocumentInformation.tsx new file mode 100644 index 00000000..63f043c6 --- /dev/null +++ b/src/components/Contribute/Knowledge/Native/DocumentInformation/DocumentInformation.tsx @@ -0,0 +1,370 @@ +import React, { useEffect, useState } from 'react'; +import { FormFieldGroupHeader, FormGroup, FormHelperText } from '@patternfly/react-core/dist/dynamic/components/Form'; +import { Button } from '@patternfly/react-core/dist/dynamic/components/Button'; +import { TextInput } from '@patternfly/react-core/dist/dynamic/components/TextInput'; +import { Alert, AlertActionLink, AlertActionCloseButton } from '@patternfly/react-core/dist/dynamic/components/Alert'; +import { HelperText } from '@patternfly/react-core/dist/dynamic/components/HelperText'; +import { HelperTextItem } from '@patternfly/react-core/dist/dynamic/components/HelperText'; +import ExclamationCircleIcon from '@patternfly/react-icons/dist/dynamic/icons/exclamation-circle-icon'; +import { ValidatedOptions } from '@patternfly/react-core/dist/esm/helpers/constants'; +import { Modal, ModalVariant } from '@patternfly/react-core/dist/esm/deprecated/components/Modal/Modal'; +import { UploadFile } from '@/components/Contribute/Knowledge/UploadFile'; +import { checkKnowledgeFormCompletion } from '@/components/Contribute/Knowledge/validation'; +import { KnowledgeFormData } from '@/types'; + +interface Props { + reset: boolean; + isEditForm?: boolean; + knowledgeFormData: KnowledgeFormData; + setDisableAction: React.Dispatch>; + knowledgeDocumentRepositoryUrl: string; + setKnowledgeDocumentRepositoryUrl: React.Dispatch>; + knowledgeDocumentCommit: string; + setKnowledgeDocumentCommit: React.Dispatch>; + documentName: string; + setDocumentName: React.Dispatch>; +} + +const DocumentInformation: React.FC = ({ + reset, + isEditForm, + knowledgeFormData, + setDisableAction, + knowledgeDocumentRepositoryUrl, + setKnowledgeDocumentRepositoryUrl, + knowledgeDocumentCommit, + setKnowledgeDocumentCommit, + documentName, + setDocumentName +}) => { + const [useFileUpload, setUseFileUpload] = useState(true); + const [uploadedFiles, setUploadedFiles] = useState([]); + const [isModalOpen, setIsModalOpen] = useState(false); + const [modalText, setModalText] = useState(); + + const [successAlertTitle, setSuccessAlertTitle] = useState(); + const [successAlertMessage, setSuccessAlertMessage] = useState(); + const [successAlertLink, setSuccessAlertLink] = useState(); + + const [failureAlertTitle, setFailureAlertTitle] = useState(); + const [failureAlertMessage, setFailureAlertMessage] = useState(); + + const [validRepo, setValidRepo] = useState(); + const [validCommit, setValidCommit] = useState(); + const [validDocumentName, setValidDocumentName] = useState(); + + useEffect(() => { + setValidRepo(ValidatedOptions.default); + setValidCommit(ValidatedOptions.default); + setValidDocumentName(ValidatedOptions.default); + }, [reset]); + + useEffect(() => { + if (isEditForm) { + setValidRepo(ValidatedOptions.success); + setValidCommit(ValidatedOptions.success); + setValidDocumentName(ValidatedOptions.success); + } + }, [isEditForm]); + + const validateRepo = (repoStr: string) => { + const repo = repoStr.trim(); + if (repo.length === 0) { + setDisableAction(true); + setValidRepo(ValidatedOptions.error); + return; + } + try { + new URL(repo); + setValidRepo(ValidatedOptions.success); + setDisableAction(!checkKnowledgeFormCompletion(knowledgeFormData)); + return; + } catch (e) { + setDisableAction(true); + setValidRepo(ValidatedOptions.warning); + return; + } + }; + + const validateCommit = (commitStr: string) => { + const commit = commitStr.trim(); + if (commit.length > 0) { + setValidCommit(ValidatedOptions.success); + setDisableAction(!checkKnowledgeFormCompletion(knowledgeFormData)); + return; + } + setDisableAction(true); + setValidCommit(ValidatedOptions.error); + return; + }; + + const validateDocumentName = (document: string) => { + const documentName = document.trim(); + if (documentName.length > 0) { + setValidDocumentName(ValidatedOptions.success); + setDisableAction(!checkKnowledgeFormCompletion(knowledgeFormData)); + return; + } + setDisableAction(true); + setValidDocumentName(ValidatedOptions.error); + return; + }; + + const handleFilesChange = (files: File[]) => { + setUploadedFiles(files); + }; + + 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) { + const response = await fetch('/api/native/upload', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ files: fileContents }) + }); + + if (!response.ok) { + setFailureAlertTitle('Failed to upload document'); + setFailureAlertMessage(`This upload failed. ${response.statusText}`); + new Error(response.statusText || 'Failed to upload document'); + return; + } + + const result = await response.json(); + + setKnowledgeDocumentRepositoryUrl(result.repoUrl); + setKnowledgeDocumentCommit(result.commitSha); + setDocumentName(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.'); + if (result.prUrl !== '') { + setSuccessAlertLink(result.prUrl); + } + } + } + }; + + const onCloseSuccessAlert = () => { + setSuccessAlertTitle(undefined); + setSuccessAlertMessage(undefined); + setSuccessAlertLink(undefined); + }; + + const onCloseFailureAlert = () => { + setFailureAlertTitle(undefined); + setFailureAlertMessage(undefined); + }; + + const handleAutomaticUpload = () => { + if (knowledgeDocumentRepositoryUrl.length > 0 || knowledgeDocumentCommit.length > 0 || documentName.length > 0) { + setModalText('Switching to automatic upload will clear the document information. Are you sure you want to continue?'); + setIsModalOpen(true); + } else { + setUseFileUpload(true); + } + }; + + const handleManualUpload = () => { + if (uploadedFiles.length > 0) { + setModalText('Switching to manual upload will clear the uploaded files. Are you sure you want to continue?'); + setIsModalOpen(true); + } else { + setUseFileUpload(false); + } + }; + + const handleModalContinue = () => { + if (useFileUpload) { + setUploadedFiles([]); + } else { + setKnowledgeDocumentRepositoryUrl(''); + setValidRepo(ValidatedOptions.default); + setKnowledgeDocumentCommit(''); + setValidCommit(ValidatedOptions.default); + setDocumentName(''); + setValidDocumentName(ValidatedOptions.default); + } + setUseFileUpload(!useFileUpload); + setIsModalOpen(false); + }; + + return ( +
+ + Document Information * +

+ ), + id: 'doc-info-id' + }} + titleDescription="Add the relevant document's information" + /> + +
+ + +
+
+ setIsModalOpen(false)} + actions={[ + , + + ]} + > +

{modalText}

+
+ {!useFileUpload ? ( + <> + + setKnowledgeDocumentRepositoryUrl(value)} + onBlur={() => validateRepo(knowledgeDocumentRepositoryUrl)} + /> + {validRepo === ValidatedOptions.error && ( + + + } variant={validRepo}> + Required field + + + + )} + {validRepo === ValidatedOptions.warning && ( + + + } variant="error"> + Please enter a valid URL. + + + + )} + + + setKnowledgeDocumentCommit(value)} + onBlur={() => validateCommit(knowledgeDocumentCommit)} + /> + {validCommit === ValidatedOptions.error && ( + + + } variant={validCommit}> + Valid commit SHA is required. + + + + )} + + + setDocumentName(value)} + onBlur={() => validateDocumentName(documentName)} + /> + {validDocumentName === ValidatedOptions.error && ( + + + } variant={validDocumentName}> + Required field + + + + )} + + + ) : ( + <> + + + + )} + + {successAlertTitle && successAlertMessage && successAlertLink && ( + } + actionLinks={ + <> + + View it here + + + } + > + {successAlertMessage} + + )} + + {failureAlertTitle && failureAlertMessage && ( + }> + {failureAlertMessage} + + )} +
+ ); +}; + +export default DocumentInformation; diff --git a/src/components/Experimental/ContributeLocal/Knowledge/SubmitLocal/Submit.tsx b/src/components/Contribute/Knowledge/Native/Submit/Submit.tsx similarity index 92% rename from src/components/Experimental/ContributeLocal/Knowledge/SubmitLocal/Submit.tsx rename to src/components/Contribute/Knowledge/Native/Submit/Submit.tsx index 442e9ad4..13255701 100644 --- a/src/components/Experimental/ContributeLocal/Knowledge/SubmitLocal/Submit.tsx +++ b/src/components/Contribute/Knowledge/Native/Submit/Submit.tsx @@ -1,8 +1,8 @@ -// src/components/Experimental/ContributeLocal/Knowledge/SubmitLocal/Submit.tsx +// src/components/contribute/native/Knowledge/Submit/Submit.tsx import React from 'react'; import { Button } from '@patternfly/react-core/dist/dynamic/components/Button'; -import { ActionGroupAlertContent, KnowledgeFormData } from '..'; -import { AttributionData, KnowledgeYamlData } from '@/types'; +import { ActionGroupAlertContent } from '..'; +import { AttributionData, KnowledgeFormData, KnowledgeYamlData } from '@/types'; import { KnowledgeSchemaVersion } from '@/types/const'; import { dumpYaml } from '@/utils/yamlConfig'; import { validateFields } from '@/components/Contribute/Knowledge/validation'; @@ -65,7 +65,7 @@ const Submit: React.FC = ({ disableAction, knowledgeFormData, setActionGr const name = knowledgeFormData.name; const submissionSummary = knowledgeFormData.submissionSummary; const documentOutline = knowledgeFormData.documentOutline; - const response = await fetch('/api/local/pr/knowledge', { + const response = await fetch('/api/native/pr/knowledge', { method: 'POST', headers: { 'Content-Type': 'application/json' @@ -95,7 +95,7 @@ const Submit: React.FC = ({ disableAction, knowledgeFormData, setActionGr const actionGroupAlertContent: ActionGroupAlertContent = { title: 'Knowledge contribution submitted successfully!', message: `Thank you for your contribution!`, - url: '/experimental/dashboard-local/', + url: '/dashboard/', success: true }; setActionGroupAlertContent(actionGroupAlertContent); diff --git a/src/components/Contribute/Knowledge/index.tsx b/src/components/Contribute/Knowledge/Native/index.tsx similarity index 57% rename from src/components/Contribute/Knowledge/index.tsx rename to src/components/Contribute/Knowledge/Native/index.tsx index fc472935..6e43fc5c 100644 --- a/src/components/Contribute/Knowledge/index.tsx +++ b/src/components/Contribute/Knowledge/Native/index.tsx @@ -1,87 +1,39 @@ -// src/components/Contribute/Knowledge/index.tsx +// src/components/Experimental/ContributeLocal/Knowledge/index.tsx 'use client'; -import React, { useEffect, useState } from 'react'; -import './knowledge.css'; +import React, { useEffect, useMemo, useState } from 'react'; +import '../knowledge.css'; import { Alert, AlertActionCloseButton } from '@patternfly/react-core/dist/dynamic/components/Alert'; import { ActionGroup } from '@patternfly/react-core/dist/dynamic/components/Form'; -import { Form } from '@patternfly/react-core/dist/dynamic/components/Form'; -import { getGitHubUsername } from '../../../utils/github'; +import { getGitHubUsername } from '@/utils/github'; import { useSession } from 'next-auth/react'; -import AuthorInformation from '../AuthorInformation'; -import { FormType } from '../AuthorInformation'; -import KnowledgeInformation from './KnowledgeInformation/KnowledgeInformation'; -import FilePathInformation from './FilePathInformation/FilePathInformation'; -import DocumentInformation from './DocumentInformation/DocumentInformation'; -import AttributionInformation from './AttributionInformation/AttributionInformation'; +import AuthorInformation from '@/components/Contribute/AuthorInformation'; +import { FormType } from '@/components/Contribute/AuthorInformation'; +import KnowledgeInformation from '@/components/Contribute/Knowledge/KnowledgeInformation/KnowledgeInformation'; +import FilePathInformation from '@/components/Contribute/Knowledge/FilePathInformation/FilePathInformation'; +import DocumentInformation from '@/components/Contribute/Knowledge/Native/DocumentInformation/DocumentInformation'; +import AttributionInformation from '@/components/Contribute/Knowledge/AttributionInformation/AttributionInformation'; import Submit from './Submit/Submit'; import { Breadcrumb } from '@patternfly/react-core/dist/dynamic/components/Breadcrumb'; import { BreadcrumbItem } from '@patternfly/react-core/dist/dynamic/components/Breadcrumb'; import { PageBreadcrumb } from '@patternfly/react-core/dist/dynamic/components/Page'; import { PageGroup } from '@patternfly/react-core/dist/dynamic/components/Page'; import { PageSection } from '@patternfly/react-core/dist/dynamic/components/Page'; -import { Content } from '@patternfly/react-core/dist/dynamic/components/Content'; import { Title } from '@patternfly/react-core/dist/dynamic/components/Title'; -import { Flex, FlexItem } from '@patternfly/react-core/dist/dynamic/layouts/Flex'; -import KnowledgeDescriptionContent from './KnowledgeDescription/KnowledgeDescriptionContent'; -import KnowledgeSeedExample from './KnowledgeSeedExample/KnowledgeSeedExample'; -import { checkKnowledgeFormCompletion } from './validation'; +import KnowledgeDescriptionContent from '@/components/Contribute/Knowledge/KnowledgeDescription/KnowledgeDescriptionContent'; +import KnowledgeSeedExample from '@/components/Contribute/Knowledge/KnowledgeSeedExample/KnowledgeSeedExample'; +import { checkKnowledgeFormCompletion } from '@/components/Contribute/Knowledge/validation'; import { ValidatedOptions } from '@patternfly/react-core/dist/esm/helpers/constants'; -import { DownloadDropdown } from './DownloadDropdown/DownloadDropdown'; -import { ViewDropdown } from './ViewDropdown/ViewDropdown'; -import Update from './Update/Update'; -import { KnowledgeYamlData, PullRequestFile } from '@/types'; +import { DownloadDropdown } from '@/components/Contribute/Knowledge/DownloadDropdown/DownloadDropdown'; +import { ViewDropdown } from '@/components/Contribute/Knowledge/ViewDropdown/ViewDropdown'; +import Update from '@/components/Contribute/Knowledge/Github/Update/Update'; +import { KnowledgeEditFormData, KnowledgeFormData, QuestionAndAnswerPair } from '@/types'; import { Button } from '@patternfly/react-core/dist/esm/components/Button/Button'; import { useRouter } from 'next/navigation'; -import { autoFillKnowledgeFields } from './AutoFill'; +import { autoFillKnowledgeFields } from '@/components/Contribute/Knowledge/AutoFill'; import { Spinner } from '@patternfly/react-core/dist/esm/components/Spinner'; -import { YamlFileUploadModal } from '../YamlFileUploadModal'; - -export interface QuestionAndAnswerPair { - immutable: boolean; - question: string; - isQuestionValid: ValidatedOptions; - questionValidationError?: string; - answer: string; - isAnswerValid: ValidatedOptions; - answerValidationError?: string; -} - -export interface SeedExample { - immutable: boolean; - isExpanded: boolean; - context: string; - isContextValid: ValidatedOptions; - validationError?: string; - questionAndAnswers: QuestionAndAnswerPair[]; -} - -export interface KnowledgeFormData { - email: string; - name: string; - submissionSummary: string; - domain: string; - documentOutline: string; - filePath: string; - seedExamples: SeedExample[]; - knowledgeDocumentRepositoryUrl: string; - knowledgeDocumentCommit: string; - documentName: string; - titleWork: string; - linkWork: string; - revision: string; - licenseWork: string; - creators: string; -} - -export interface KnowledgeEditFormData { - isEditForm: boolean; - knowledgeVersion: number; - pullRequestNumber: number; - branchName: string; - yamlFile: PullRequestFile; - attributionFile: PullRequestFile; - knowledgeFormData: KnowledgeFormData; -} +import { Wizard, WizardStep } from '@patternfly/react-core/dist/esm/components/Wizard'; +import { Content } from '@patternfly/react-core/dist/dynamic/components/Content'; +import ReviewSubmission from '../ReviewSubmission'; export interface ActionGroupAlertContent { title: string; @@ -96,8 +48,8 @@ export interface KnowledgeFormProps { knowledgeEditFormData?: KnowledgeEditFormData; } -export const KnowledgeForm: React.FunctionComponent = ({ knowledgeEditFormData }) => { - const [deploymentType, setDeploymentType] = useState(); +export const KnowledgeFormNative: React.FunctionComponent = ({ knowledgeEditFormData }) => { + const [devModeEnabled, setDevModeEnabled] = useState(); const { data: session } = useSession(); const [githubUsername, setGithubUsername] = useState(''); @@ -131,11 +83,11 @@ export const KnowledgeForm: React.FunctionComponent = ({ kno const [disableAction, setDisableAction] = useState(true); const [reset, setReset] = useState(false); - const [isModalOpen, setIsModalOpen] = React.useState(false); - const router = useRouter(); - const emptySeedExample: SeedExample = { + const [activeStepIndex] = useState(1); + + const emptySeedExample: KnowledgeSeedExample = { immutable: true, isExpanded: false, context: '', @@ -165,7 +117,7 @@ export const KnowledgeForm: React.FunctionComponent = ({ kno ] }; - const [seedExamples, setSeedExamples] = useState([ + const [seedExamples, setSeedExamples] = useState([ emptySeedExample, emptySeedExample, emptySeedExample, @@ -177,7 +129,7 @@ export const KnowledgeForm: React.FunctionComponent = ({ kno const getEnvVariables = async () => { const res = await fetch('/api/envConfig'); const envConfig = await res.json(); - setDeploymentType(envConfig.DEPLOYMENT_TYPE); + setDevModeEnabled(envConfig.ENABLE_DEV_MODE === 'true'); }; getEnvVariables(); }, []); @@ -189,7 +141,7 @@ export const KnowledgeForm: React.FunctionComponent = ({ kno } }, [session?.user]); - useEffect(() => { + useMemo(() => { const fetchUsername = async () => { if (session?.accessToken) { try { @@ -237,7 +189,7 @@ export const KnowledgeForm: React.FunctionComponent = ({ kno const contextStr = context.trim(); if (contextStr.length == 0) { setDisableAction(true); - return { msg: 'Required field', status: ValidatedOptions.error }; + return { msg: 'Context is required', status: ValidatedOptions.error }; } const tokens = contextStr.split(/\s+/); if (tokens.length > 0 && tokens.length <= 500) { @@ -253,7 +205,7 @@ export const KnowledgeForm: React.FunctionComponent = ({ kno const questionStr = question.trim(); if (questionStr.length == 0) { setDisableAction(true); - return { msg: 'Required field', status: ValidatedOptions.error }; + return { msg: 'Question is required', status: ValidatedOptions.error }; } const tokens = questionStr.split(/\s+/); if (tokens.length > 0 && tokens.length < 250) { @@ -268,7 +220,7 @@ export const KnowledgeForm: React.FunctionComponent = ({ kno const answerStr = answer.trim(); if (answerStr.length == 0) { setDisableAction(true); - return { msg: 'Required field', status: ValidatedOptions.error }; + return { msg: 'Answer is required', status: ValidatedOptions.error }; } const tokens = answerStr.split(/\s+/); if (tokens.length > 0 && tokens.length < 250) { @@ -281,7 +233,7 @@ export const KnowledgeForm: React.FunctionComponent = ({ kno const handleContextInputChange = (seedExampleIndex: number, contextValue: string): void => { setSeedExamples( - seedExamples.map((seedExample: SeedExample, index: number) => + seedExamples.map((seedExample: KnowledgeSeedExample, index: number) => index === seedExampleIndex ? { ...seedExample, @@ -293,7 +245,7 @@ export const KnowledgeForm: React.FunctionComponent = ({ kno }; const handleContextBlur = (seedExampleIndex: number): void => { - const updatedSeedExamples = seedExamples.map((seedExample: SeedExample, index: number): SeedExample => { + const updatedSeedExamples = seedExamples.map((seedExample: KnowledgeSeedExample, index: number): KnowledgeSeedExample => { if (index === seedExampleIndex) { const { msg, status } = validateContext(seedExample.context); return { @@ -309,7 +261,7 @@ export const KnowledgeForm: React.FunctionComponent = ({ kno const handleQuestionInputChange = (seedExampleIndex: number, questionAndAnswerIndex: number, questionValue: string): void => { setSeedExamples( - seedExamples.map((seedExample: SeedExample, index: number) => + seedExamples.map((seedExample: KnowledgeSeedExample, index: number) => index === seedExampleIndex ? { ...seedExample, @@ -329,7 +281,7 @@ export const KnowledgeForm: React.FunctionComponent = ({ kno const handleQuestionBlur = (seedExampleIndex: number, questionAndAnswerIndex: number): void => { setSeedExamples( - seedExamples.map((seedExample: SeedExample, index: number) => + seedExamples.map((seedExample: KnowledgeSeedExample, index: number) => index === seedExampleIndex ? { ...seedExample, @@ -352,7 +304,7 @@ export const KnowledgeForm: React.FunctionComponent = ({ kno const handleAnswerInputChange = (seedExampleIndex: number, questionAndAnswerIndex: number, answerValue: string): void => { setSeedExamples( - seedExamples.map((seedExample: SeedExample, index: number) => + seedExamples.map((seedExample: KnowledgeSeedExample, index: number) => index === seedExampleIndex ? { ...seedExample, @@ -372,7 +324,7 @@ export const KnowledgeForm: React.FunctionComponent = ({ kno const handleAnswerBlur = (seedExampleIndex: number, questionAndAnswerIndex: number): void => { setSeedExamples( - seedExamples.map((seedExample: SeedExample, index: number) => + seedExamples.map((seedExample: KnowledgeSeedExample, index: number) => index === seedExampleIndex ? { ...seedExample, @@ -393,6 +345,14 @@ export const KnowledgeForm: React.FunctionComponent = ({ kno ); }; + // const toggleSeedExampleExpansion = (index: number): void => { + // setSeedExamples(seedExamples.map((seedExample, idx) => (idx === index ? { ...seedExample, isExpanded: !seedExample.isExpanded } : seedExample))); + // }; + + const toggleSeedExampleExpansion = (index: number): void => { + setSeedExamples(seedExamples.map((seedExample, idx) => (idx === index ? { ...seedExample, isExpanded: !seedExample.isExpanded } : seedExample))); + }; + const onCloseActionGroupAlert = () => { setActionGroupAlertContent(undefined); }; @@ -436,32 +396,6 @@ export const KnowledgeForm: React.FunctionComponent = ({ kno setSeedExamples(autoFillKnowledgeFields.seedExamples); }; - const yamlSeedExampleToFormSeedExample = ( - yamlSeedExamples: { context: string; questions_and_answers: { question: string; answer: string }[] }[] - ) => { - return yamlSeedExamples.map((yamlSeedExample) => ({ - immutable: true, - isExpanded: false, - context: yamlSeedExample.context ?? '', - isContextValid: ValidatedOptions.default, - questionAndAnswers: yamlSeedExample.questions_and_answers.map((questionAndAnswer) => ({ - question: questionAndAnswer.question ?? '', - answer: questionAndAnswer.answer ?? '' - })) - })) as SeedExample[]; - }; - - const onYamlUploadKnowledgeFillForm = (data: KnowledgeYamlData): void => { - setName(data.created_by ?? ''); - setDocumentOutline(data.document_outline ?? ''); - setSubmissionSummary(data.document_outline ?? ''); - setDomain(data.domain ?? ''); - setKnowledgeDocumentRepositoryUrl(data.document.repo ?? ''); - setKnowledgeDocumentCommit(data.document.commit ?? ''); - setDocumentName(data.document.patterns.join(', ') ?? ''); - setSeedExamples(yamlSeedExampleToFormSeedExample(data.seed_examples)); - }; - const knowledgeFormData: KnowledgeFormData = { email: email, name: name, @@ -488,171 +422,202 @@ export const KnowledgeForm: React.FunctionComponent = ({ kno router.push('/dashboard'); }; + const steps = [ + { + id: 'author-info', + name: 'Author Information', + component: ( + + ) + }, + { + id: 'knowledge-info', + name: 'Knowledge Information', + component: ( + + ) + }, + { + id: 'file-path-info', + name: 'File Path Information', + component: ( + + ) + }, + { + id: 'seed-examples', + name: 'Seed Examples', + component: ( + + ) + }, + { + id: 'document-info', + name: 'Document Information', + component: ( + + ) + }, + { + id: 'attribution-info', + name: 'Attribution Information', + component: ( + + ) + }, + { + id: 'review-submission', + name: 'Review Submission', + component: , + footer: { + isNextDisabled: true + } + } + ]; + return ( - + Dashboard Knowledge Contribution - - - - - Knowledge Contribution - - - - - - - + + + Knowledge Contribution + - {deploymentType === 'dev' && ( + {devModeEnabled && ( )} - + + {steps.map((step) => ( + + {step.component} + + ))} + + + {actionGroupAlertContent && ( + } + > +

+ {actionGroupAlertContent.waitAlert && } + {actionGroupAlertContent.message} +
+ {!actionGroupAlertContent.waitAlert && + actionGroupAlertContent.success && + actionGroupAlertContent.url && + actionGroupAlertContent.url.trim().length > 0 && ( + + View your new branch + + )} +

+
+ )} -
- - - - - - - - - - - - - {actionGroupAlertContent && ( - } - > -

- {actionGroupAlertContent.waitAlert && } - {actionGroupAlertContent.message} -
- {!actionGroupAlertContent.waitAlert && - actionGroupAlertContent.success && - actionGroupAlertContent.url && - actionGroupAlertContent.url.trim().length > 0 && ( - - View your pull request - - )} -

-
+ + {knowledgeEditFormData?.isEditForm && ( + )} - - - {knowledgeEditFormData?.isEditForm && ( - - )} - {!knowledgeEditFormData?.isEditForm && ( - - )} - - - - - + {!knowledgeEditFormData?.isEditForm && ( + + )} + + + +
); }; -export default KnowledgeForm; +export default KnowledgeFormNative; diff --git a/src/components/Experimental/ReviewSubmission/index.tsx b/src/components/Contribute/Knowledge/ReviewSubmission/index.tsx similarity index 96% rename from src/components/Experimental/ReviewSubmission/index.tsx rename to src/components/Contribute/Knowledge/ReviewSubmission/index.tsx index 92ea05f5..3e39715c 100644 --- a/src/components/Experimental/ReviewSubmission/index.tsx +++ b/src/components/Contribute/Knowledge/ReviewSubmission/index.tsx @@ -1,6 +1,6 @@ // src/components/Contribute/Knowledge/ReviewSubmission/ReviewSubmission.tsx +import { KnowledgeFormData } from '@/types'; import React from 'react'; -import { KnowledgeFormData } from '@/components/Experimental/ContributeLocal/Knowledge'; interface ReviewSubmissionProps { knowledgeFormData: KnowledgeFormData; diff --git a/src/components/Contribute/Knowledge/ViewDropdown/ViewDropdown.tsx b/src/components/Contribute/Knowledge/ViewDropdown/ViewDropdown.tsx index bd1280bc..cd22f4a9 100644 --- a/src/components/Contribute/Knowledge/ViewDropdown/ViewDropdown.tsx +++ b/src/components/Contribute/Knowledge/ViewDropdown/ViewDropdown.tsx @@ -3,10 +3,9 @@ import { Dropdown } from '@patternfly/react-core/dist/dynamic/components/Dropdow import { DropdownItem } from '@patternfly/react-core/dist/dynamic/components/Dropdown'; import { DropdownList } from '@patternfly/react-core/dist/dynamic/components/Dropdown'; import { MenuToggle, MenuToggleElement } from '@patternfly/react-core/dist/dynamic/components/MenuToggle'; -import { KnowledgeFormData } from '..'; import YamlCodeModal from '@/components/YamlCodeModal'; import CodeIcon from '@patternfly/react-icons/dist/esm/icons/code-icon'; -import { AttributionData, KnowledgeYamlData } from '@/types'; +import { AttributionData, KnowledgeFormData, KnowledgeYamlData } from '@/types'; import { KnowledgeSchemaVersion } from '@/types/const'; import { dumpYaml } from '@/utils/yamlConfig'; import FileIcon from '@patternfly/react-icons/dist/dynamic/icons/file-icon'; diff --git a/src/components/Contribute/Knowledge/knowledge.css b/src/components/Contribute/Knowledge/knowledge.css index 9f65e32c..ab9c58c9 100644 --- a/src/components/Contribute/Knowledge/knowledge.css +++ b/src/components/Contribute/Knowledge/knowledge.css @@ -29,3 +29,11 @@ align-items: center; gap: 1rem; } + +.knowledge-form .pf-c-form__group { + margin-bottom: 30px; +} + +.knowledge-form { + line-height: 3; +} diff --git a/src/components/Contribute/Knowledge/validation.tsx b/src/components/Contribute/Knowledge/validation.tsx index e4bc819e..54d10d2c 100644 --- a/src/components/Contribute/Knowledge/validation.tsx +++ b/src/components/Contribute/Knowledge/validation.tsx @@ -1,12 +1,13 @@ +import { KnowledgeFormData, KnowledgeSeedExample } from '@/types'; import { ValidatedOptions } from '@patternfly/react-core/dist/esm/helpers/constants'; -import { ActionGroupAlertContent, KnowledgeFormData, SeedExample } from '.'; +import { ActionGroupAlertContent } from './Github'; const validateEmail = (email: string): boolean => { const emailRegex = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$/; return emailRegex.test(email); }; -const hasDuplicateSeedExamples = (seedExamples: SeedExample[]): { duplicate: boolean; index: number } => { +const hasDuplicateSeedExamples = (seedExamples: KnowledgeSeedExample[]): { duplicate: boolean; index: number } => { const contexts = new Set(); for (let index = 0; index < seedExamples.length; index++) { const seedExample = seedExamples[index]; @@ -20,7 +21,7 @@ const hasDuplicateSeedExamples = (seedExamples: SeedExample[]): { duplicate: boo }; // Check if the question in Q&A pairs in a each seed example are unique -const hasDuplicateQuestionAndAnswerPairs = (seedExample: SeedExample): { duplicate: boolean; index: number } => { +const hasDuplicateQuestionAndAnswerPairs = (seedExample: KnowledgeSeedExample): { duplicate: boolean; index: number } => { const questions = new Set(); for (let index = 0; index < seedExample.questionAndAnswers.length; index++) { const questionAndAnswerPair = seedExample.questionAndAnswers[index]; @@ -36,7 +37,7 @@ const hasDuplicateQuestionAndAnswerPairs = (seedExample: SeedExample): { duplica // Validate that the total length of all the question and answer pairs // and context in a seed example is not more than 750 characters. -const validateQuestionAndAnswerPairs = (seedExample: SeedExample): { success: boolean; currLength: number } => { +const validateQuestionAndAnswerPairs = (seedExample: KnowledgeSeedExample): { success: boolean; currLength: number } => { const totalQnAPairsTokenCount = seedExample.questionAndAnswers.reduce((acc, questionAndAnswerPair) => { const questionTokens = questionAndAnswerPair.question.trim().split(/\s+/); const answerTokens = questionAndAnswerPair.answer.trim().split(/\s+/); diff --git a/src/components/Contribute/Skill/AttributionInformation/AttributionInformation.tsx b/src/components/Contribute/Skill/AttributionInformation/AttributionInformation.tsx index bbedfc4a..024b9a53 100644 --- a/src/components/Contribute/Skill/AttributionInformation/AttributionInformation.tsx +++ b/src/components/Contribute/Skill/AttributionInformation/AttributionInformation.tsx @@ -5,8 +5,8 @@ import { HelperText } from '@patternfly/react-core/dist/dynamic/components/Helpe import { HelperTextItem } from '@patternfly/react-core/dist/dynamic/components/HelperText'; import ExclamationCircleIcon from '@patternfly/react-icons/dist/dynamic/icons/exclamation-circle-icon'; import { ValidatedOptions } from '@patternfly/react-core/dist/esm/helpers/constants'; -import { SkillFormData } from '..'; import { checkSkillFormCompletion } from '../validation'; +import { SkillFormData } from '@/types'; interface Props { reset: boolean; diff --git a/src/components/Contribute/Skill/AutoFill.ts b/src/components/Contribute/Skill/AutoFill.ts index be1cec2a..6470a338 100644 --- a/src/components/Contribute/Skill/AutoFill.ts +++ b/src/components/Contribute/Skill/AutoFill.ts @@ -1,7 +1,7 @@ -import { SeedExample, SkillFormData } from '.'; +import { SkillFormData, SkillSeedExample } from '@/types'; import { ValidatedOptions } from '@patternfly/react-core/dist/esm/helpers/constants'; -const seedExamples: SeedExample[] = [ +const seedExamples: SkillSeedExample[] = [ { immutable: false, isExpanded: false, diff --git a/src/components/Contribute/Skill/DownloadAttribution/DownloadAttribution.tsx b/src/components/Contribute/Skill/DownloadAttribution/DownloadAttribution.tsx index 1cfb3d67..330c274c 100644 --- a/src/components/Contribute/Skill/DownloadAttribution/DownloadAttribution.tsx +++ b/src/components/Contribute/Skill/DownloadAttribution/DownloadAttribution.tsx @@ -1,7 +1,7 @@ import React from 'react'; -import { SkillFormData } from '..'; import { DropdownItem } from '@patternfly/react-core/dist/esm/components/Dropdown/DropdownItem'; import FileIcon from '@patternfly/react-icons/dist/esm/icons/file-icon'; +import { SkillFormData } from '@/types'; interface Props { skillFormData: SkillFormData; diff --git a/src/components/Contribute/Skill/DownloadDropdown/DownloadDropdown.tsx b/src/components/Contribute/Skill/DownloadDropdown/DownloadDropdown.tsx index 41f39e55..6b5663a6 100644 --- a/src/components/Contribute/Skill/DownloadDropdown/DownloadDropdown.tsx +++ b/src/components/Contribute/Skill/DownloadDropdown/DownloadDropdown.tsx @@ -4,8 +4,8 @@ import { DropdownList } from '@patternfly/react-core/dist/dynamic/components/Dro import { MenuToggle, MenuToggleElement } from '@patternfly/react-core/dist/dynamic/components/MenuToggle'; import DownloadYaml from '../DownloadYaml/DownloadYaml'; import DownloadAttribution from '../DownloadAttribution/DownloadAttribution'; -import { SkillFormData } from '..'; import DownloadIcon from '@patternfly/react-icons/dist/esm/icons/download-icon'; +import { SkillFormData } from '@/types'; interface Props { skillFormData: SkillFormData; diff --git a/src/components/Contribute/Skill/DownloadYaml/DownloadYaml.tsx b/src/components/Contribute/Skill/DownloadYaml/DownloadYaml.tsx index 321fd207..798a45c8 100644 --- a/src/components/Contribute/Skill/DownloadYaml/DownloadYaml.tsx +++ b/src/components/Contribute/Skill/DownloadYaml/DownloadYaml.tsx @@ -1,6 +1,5 @@ import React from 'react'; -import { SkillFormData } from '..'; -import { SkillYamlData } from '@/types'; +import { SkillFormData, SkillYamlData } from '@/types'; import { dumpYaml } from '@/utils/yamlConfig'; import { DropdownItem } from '@patternfly/react-core/dist/esm/components/Dropdown/DropdownItem'; import CodeIcon from '@patternfly/react-icons/dist/esm/icons/code-icon'; diff --git a/src/components/Contribute/Skill/Submit/Submit.tsx b/src/components/Contribute/Skill/Github/Submit/Submit.tsx similarity index 95% rename from src/components/Contribute/Skill/Submit/Submit.tsx rename to src/components/Contribute/Skill/Github/Submit/Submit.tsx index 2c99c578..a623d9a5 100644 --- a/src/components/Contribute/Skill/Submit/Submit.tsx +++ b/src/components/Contribute/Skill/Github/Submit/Submit.tsx @@ -1,10 +1,10 @@ import React from 'react'; import { Button } from '@patternfly/react-core/dist/dynamic/components/Button'; -import { ActionGroupAlertContent, SkillFormData } from '..'; -import { AttributionData, SkillYamlData } from '@/types'; +import { ActionGroupAlertContent } from '..'; +import { AttributionData, SkillFormData, SkillYamlData } from '@/types'; import { SkillSchemaVersion } from '@/types/const'; import { dumpYaml } from '@/utils/yamlConfig'; -import { validateFields } from '../validation'; +import { validateFields } from '../../validation'; interface Props { disableAction: boolean; diff --git a/src/components/Contribute/Skill/Update/Update.tsx b/src/components/Contribute/Skill/Github/Update/Update.tsx similarity index 96% rename from src/components/Contribute/Skill/Update/Update.tsx rename to src/components/Contribute/Skill/Github/Update/Update.tsx index a63b95a7..7e1ecdcb 100644 --- a/src/components/Contribute/Skill/Update/Update.tsx +++ b/src/components/Contribute/Skill/Github/Update/Update.tsx @@ -1,10 +1,10 @@ import React from 'react'; import { Button } from '@patternfly/react-core/dist/dynamic/components/Button'; -import { ActionGroupAlertContent, SkillFormData } from '..'; -import { AttributionData, SkillYamlData, PullRequestFile } from '@/types'; +import { ActionGroupAlertContent } from '..'; +import { AttributionData, SkillYamlData, PullRequestFile, SkillFormData } from '@/types'; import { SkillSchemaVersion } from '@/types/const'; import { dumpYaml } from '@/utils/yamlConfig'; -import { validateFields } from '../validation'; +import { validateFields } from '../../validation'; import { amendCommit, getGitHubUsername, updatePullRequest } from '@/utils/github'; import { useSession } from 'next-auth/react'; import { useRouter } from 'next/navigation'; diff --git a/src/components/Contribute/Skill/index.tsx b/src/components/Contribute/Skill/Github/index.tsx similarity index 87% rename from src/components/Contribute/Skill/index.tsx rename to src/components/Contribute/Skill/Github/index.tsx index a11783d4..f4924a66 100644 --- a/src/components/Contribute/Skill/index.tsx +++ b/src/components/Contribute/Skill/Github/index.tsx @@ -5,12 +5,12 @@ import './skills.css'; import { Alert, AlertActionCloseButton } from '@patternfly/react-core/dist/dynamic/components/Alert'; import { ActionGroup } from '@patternfly/react-core/dist/dynamic/components/Form'; import { Form } from '@patternfly/react-core/dist/dynamic/components/Form'; -import { getGitHubUsername } from '../../../utils/github'; +import { getGitHubUsername } from '../../../../utils/github'; import { useSession } from 'next-auth/react'; -import AuthorInformation from '../AuthorInformation'; -import { FormType } from '../AuthorInformation'; -import FilePathInformation from './FilePathInformation/FilePathInformation'; -import AttributionInformation from './AttributionInformation/AttributionInformation'; +import AuthorInformation from '../../AuthorInformation'; +import { FormType } from '../../AuthorInformation'; +import FilePathInformation from '../FilePathInformation/FilePathInformation'; +import AttributionInformation from '../AttributionInformation/AttributionInformation'; import Submit from './Submit/Submit'; import { Breadcrumb } from '@patternfly/react-core/dist/dynamic/components/Breadcrumb'; import { BreadcrumbItem } from '@patternfly/react-core/dist/dynamic/components/Breadcrumb'; @@ -20,46 +20,20 @@ import { PageSection } from '@patternfly/react-core/dist/dynamic/components/Page import { Content } from '@patternfly/react-core/dist/dynamic/components/Content'; import { Title } from '@patternfly/react-core/dist/dynamic/components/Title'; import { Flex, FlexItem } from '@patternfly/react-core/dist/dynamic/layouts/Flex'; -import { checkSkillFormCompletion } from './validation'; +import { checkSkillFormCompletion } from '../validation'; import { ValidatedOptions } from '@patternfly/react-core/dist/esm/helpers/constants'; -import { DownloadDropdown } from './DownloadDropdown/DownloadDropdown'; -import { ViewDropdown } from './ViewDropdown/ViewDropdown'; +import { DownloadDropdown } from '../DownloadDropdown/DownloadDropdown'; +import { ViewDropdown } from '../ViewDropdown/ViewDropdown'; import Update from './Update/Update'; -import { SkillYamlData, PullRequestFile } from '@/types'; +import { SkillYamlData, PullRequestFile, SkillFormData, SkillSeedExample } from '@/types'; import { Button } from '@patternfly/react-core/dist/esm/components/Button/Button'; import { useRouter } from 'next/navigation'; -import SkillsSeedExample from './SkillsSeedExample/SkillsSeedExample'; -import SkillsInformation from './SkillsInformation/SkillsInformation'; -import SkillsDescriptionContent from './SkillsDescription/SkillsDescriptionContent'; -import { autoFillSkillsFields } from './AutoFill'; +import SkillsSeedExample from '../SkillsSeedExample/SkillsSeedExample'; +import SkillsInformation from '../SkillsInformation/SkillsInformation'; +import SkillsDescriptionContent from '../SkillsDescription/SkillsDescriptionContent'; +import { autoFillSkillsFields } from '../AutoFill'; import { Spinner } from '@patternfly/react-core/dist/dynamic/components/Spinner'; -import { YamlFileUploadModal } from '../YamlFileUploadModal'; - -export interface SeedExample { - immutable: boolean; - isExpanded: boolean; - context?: string; - isContextValid?: ValidatedOptions; - validationError?: string; - question: string; - isQuestionValid: ValidatedOptions; - questionValidationError?: string; - answer: string; - isAnswerValid: ValidatedOptions; - answerValidationError?: string; -} - -export interface SkillFormData { - email: string; - name: string; - submissionSummary: string; - documentOutline: string; - filePath: string; - seedExamples: SeedExample[]; - titleWork: string; - licenseWork: string; - creators: string; -} +import { YamlFileUploadModal } from '../../YamlFileUploadModal'; export interface SkillEditFormData { isEditForm: boolean; @@ -84,8 +58,8 @@ export interface SkillFormProps { skillEditFormData?: SkillEditFormData; } -export const SkillForm: React.FunctionComponent = ({ skillEditFormData }) => { - const [deploymentType, setDeploymentType] = useState(); +export const SkillFormGithub: React.FunctionComponent = ({ skillEditFormData }) => { + const [devModeEnabled, setDevModeEnabled] = useState(); const { data: session } = useSession(); const [githubUsername, setGithubUsername] = useState(''); @@ -115,7 +89,7 @@ export const SkillForm: React.FunctionComponent = ({ skillEditFo const router = useRouter(); - const emptySeedExample: SeedExample = { + const emptySeedExample: SkillSeedExample = { immutable: true, isExpanded: false, context: '', @@ -126,7 +100,7 @@ export const SkillForm: React.FunctionComponent = ({ skillEditFo isAnswerValid: ValidatedOptions.default }; - const [seedExamples, setSeedExamples] = useState([ + const [seedExamples, setSeedExamples] = useState([ emptySeedExample, emptySeedExample, emptySeedExample, @@ -138,7 +112,7 @@ export const SkillForm: React.FunctionComponent = ({ skillEditFo const getEnvVariables = async () => { const res = await fetch('/api/envConfig'); const envConfig = await res.json(); - setDeploymentType(envConfig.DEPLOYMENT_TYPE); + setDevModeEnabled(envConfig.ENABLE_DEV_MODE === 'true'); }; getEnvVariables(); }, []); @@ -213,7 +187,7 @@ export const SkillForm: React.FunctionComponent = ({ skillEditFo const handleContextInputChange = (seedExampleIndex: number, contextValue: string): void => { setSeedExamples( - seedExamples.map((seedExample: SeedExample, index: number) => + seedExamples.map((seedExample: SkillSeedExample, index: number) => index === seedExampleIndex ? { ...seedExample, @@ -226,7 +200,7 @@ export const SkillForm: React.FunctionComponent = ({ skillEditFo const handleContextBlur = (seedExampleIndex: number): void => { setSeedExamples( - seedExamples.map((seedExample: SeedExample, index: number) => + seedExamples.map((seedExample: SkillSeedExample, index: number) => index === seedExampleIndex ? { ...seedExample, @@ -239,7 +213,7 @@ export const SkillForm: React.FunctionComponent = ({ skillEditFo const handleAnswerInputChange = (seedExampleIndex: number, answerValue: string): void => { setSeedExamples( - seedExamples.map((seedExample: SeedExample, index: number) => + seedExamples.map((seedExample: SkillSeedExample, index: number) => index === seedExampleIndex ? { ...seedExample, @@ -252,7 +226,7 @@ export const SkillForm: React.FunctionComponent = ({ skillEditFo const handleAnswerBlur = (seedExampleIndex: number): void => { setSeedExamples( - seedExamples.map((seedExample: SeedExample, index: number) => + seedExamples.map((seedExample: SkillSeedExample, index: number) => index === seedExampleIndex ? { ...seedExample, @@ -265,7 +239,7 @@ export const SkillForm: React.FunctionComponent = ({ skillEditFo const handleQuestionInputChange = (seedExampleIndex: number, questionValue: string): void => { setSeedExamples( - seedExamples.map((seedExample: SeedExample, index: number) => + seedExamples.map((seedExample: SkillSeedExample, index: number) => index === seedExampleIndex ? { ...seedExample, @@ -278,7 +252,7 @@ export const SkillForm: React.FunctionComponent = ({ skillEditFo const handleQuestionBlur = (seedExampleIndex: number): void => { setSeedExamples( - seedExamples.map((seedExample: SeedExample, index: number) => + seedExamples.map((seedExample: SkillSeedExample, index: number) => index === seedExampleIndex ? { ...seedExample, @@ -342,7 +316,7 @@ export const SkillForm: React.FunctionComponent = ({ skillEditFo isContextValid: ValidatedOptions.default, question: yamlSeedExample.question, answer: yamlSeedExample.answer - })) as SeedExample[]; + })) as SkillSeedExample[]; }; const onYamlUploadSkillsFillForm = (data: SkillYamlData): void => { @@ -396,7 +370,7 @@ export const SkillForm: React.FunctionComponent = ({ skillEditFo - {deploymentType === 'dev' && ( + {devModeEnabled && ( @@ -520,4 +494,4 @@ export const SkillForm: React.FunctionComponent = ({ skillEditFo ); }; -export default SkillForm; +export default SkillFormGithub; diff --git a/src/components/Contribute/Skill/skills.css b/src/components/Contribute/Skill/Github/skills.css similarity index 100% rename from src/components/Contribute/Skill/skills.css rename to src/components/Contribute/Skill/Github/skills.css diff --git a/src/components/Experimental/ContributeLocal/Skill/SubmitLocal/SubmitLocal.tsx b/src/components/Contribute/Skill/Native/Submit/Submit.tsx similarity index 93% rename from src/components/Experimental/ContributeLocal/Skill/SubmitLocal/SubmitLocal.tsx rename to src/components/Contribute/Skill/Native/Submit/Submit.tsx index 7cf9967a..b05bbf85 100644 --- a/src/components/Experimental/ContributeLocal/Skill/SubmitLocal/SubmitLocal.tsx +++ b/src/components/Contribute/Skill/Native/Submit/Submit.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { Button } from '@patternfly/react-core/dist/dynamic/components/Button'; -import { ActionGroupAlertContent, SkillFormData } from '..'; -import { AttributionData, SkillYamlData } from '@/types'; +import { ActionGroupAlertContent } from '..'; +import { AttributionData, SkillFormData, SkillYamlData } from '@/types'; import { SkillSchemaVersion } from '@/types/const'; import { dumpYaml } from '@/utils/yamlConfig'; import { validateFields } from '@/components/Contribute/Skill/validation'; @@ -57,7 +57,7 @@ const Submit: React.FC = ({ disableAction, skillFormData, setActionGroupA const name = skillFormData.name; const submissionSummary = skillFormData.submissionSummary; const documentOutline = skillFormData.documentOutline; - const response = await fetch('/api/local/pr/skill/', { + const response = await fetch('/api/native/pr/skill/', { method: 'POST', headers: { 'Content-Type': 'application/json' @@ -87,7 +87,7 @@ const Submit: React.FC = ({ disableAction, skillFormData, setActionGroupA const actionGroupAlertContent: ActionGroupAlertContent = { title: 'Skill contribution submitted successfully!', message: `Thank you for your contribution!`, - url: '/experimental/dashboard-local/', + url: '/dashboard', success: true }; setActionGroupAlertContent(actionGroupAlertContent); diff --git a/src/components/Experimental/ContributeLocal/Skill/index.tsx b/src/components/Contribute/Skill/Native/index.tsx similarity index 89% rename from src/components/Experimental/ContributeLocal/Skill/index.tsx rename to src/components/Contribute/Skill/Native/index.tsx index 4ff6a35e..4a263e49 100644 --- a/src/components/Experimental/ContributeLocal/Skill/index.tsx +++ b/src/components/Contribute/Skill/Native/index.tsx @@ -1,4 +1,4 @@ -// src/components/Experimental/ContributeLocal/Skill/index.tsx +// src/components/contribute/Skill/index.tsx 'use client'; import React, { useEffect, useState } from 'react'; import './skills.css'; @@ -10,7 +10,7 @@ import AuthorInformation from '@/components/Contribute/AuthorInformation'; import { FormType } from '@/components/Contribute/AuthorInformation'; import FilePathInformation from '@/components/Contribute/Skill/FilePathInformation/FilePathInformation'; import AttributionInformation from '@/components/Contribute/Skill/AttributionInformation/AttributionInformation'; -import Submit from '@/components/Experimental/ContributeLocal/Skill/SubmitLocal/SubmitLocal'; +import Submit from '@/components/Contribute/Skill/Native/Submit/Submit'; import { Breadcrumb } from '@patternfly/react-core/dist/dynamic/components/Breadcrumb'; import { BreadcrumbItem } from '@patternfly/react-core/dist/dynamic/components/Breadcrumb'; import { PageBreadcrumb } from '@patternfly/react-core/dist/dynamic/components/Page'; @@ -22,8 +22,8 @@ import { checkSkillFormCompletion } from '@/components/Contribute/Skill/validati import { ValidatedOptions } from '@patternfly/react-core/dist/esm/helpers/constants'; import { DownloadDropdown } from '@/components/Contribute/Skill/DownloadDropdown/DownloadDropdown'; import { ViewDropdown } from '@/components/Contribute/Skill/ViewDropdown/ViewDropdown'; -import Update from '@/components/Contribute/Skill/Update/Update'; -import { PullRequestFile } from '@/types'; +import Update from '@/components/Contribute/Skill/Github/Update/Update'; +import { PullRequestFile, SkillSeedExample, SkillFormData } from '@/types'; import { Button } from '@patternfly/react-core/dist/esm/components/Button/Button'; import { useRouter } from 'next/navigation'; import SkillsSeedExample from '@/components/Contribute/Skill/SkillsSeedExample/SkillsSeedExample'; @@ -32,32 +32,6 @@ import SkillsDescriptionContent from '@/components/Contribute/Skill/SkillsDescri import { autoFillSkillsFields } from '@/components/Contribute/Skill/AutoFill'; import { Spinner } from '@patternfly/react-core/dist/dynamic/components/Spinner'; -export interface SeedExample { - immutable: boolean; - isExpanded: boolean; - context?: string; - isContextValid?: ValidatedOptions; - validationError?: string; - question: string; - isQuestionValid: ValidatedOptions; - questionValidationError?: string; - answer: string; - isAnswerValid: ValidatedOptions; - answerValidationError?: string; -} - -export interface SkillFormData { - email: string; - name: string; - submissionSummary: string; - documentOutline: string; - filePath: string; - seedExamples: SeedExample[]; - titleWork: string; - licenseWork: string; - creators: string; -} - export interface SkillEditFormData { isEditForm: boolean; skillVersion: number; @@ -81,8 +55,8 @@ export interface SkillFormProps { skillEditFormData?: SkillEditFormData; } -export const SkillFormLocal: React.FunctionComponent = ({ skillEditFormData }) => { - const [deploymentType, setDeploymentType] = useState(); +export const SkillFormNative: React.FunctionComponent = ({ skillEditFormData }) => { + const [devModeEnabled, setDevModeEnabled] = useState(); const { data: session } = useSession(); const [githubUsername] = useState(''); @@ -110,7 +84,7 @@ export const SkillFormLocal: React.FunctionComponent = ({ skillE const router = useRouter(); - const emptySeedExample: SeedExample = { + const emptySeedExample: SkillSeedExample = { immutable: true, isExpanded: false, context: '', @@ -121,7 +95,7 @@ export const SkillFormLocal: React.FunctionComponent = ({ skillE isAnswerValid: ValidatedOptions.default }; - const [seedExamples, setSeedExamples] = useState([ + const [seedExamples, setSeedExamples] = useState([ emptySeedExample, emptySeedExample, emptySeedExample, @@ -133,7 +107,7 @@ export const SkillFormLocal: React.FunctionComponent = ({ skillE const getEnvVariables = async () => { const res = await fetch('/api/envConfig'); const envConfig = await res.json(); - setDeploymentType(envConfig.DEPLOYMENT_TYPE); + setDevModeEnabled(envConfig.ENABLE_DEV_MODE === 'true'); }; getEnvVariables(); }, []); @@ -186,7 +160,7 @@ export const SkillFormLocal: React.FunctionComponent = ({ skillE const handleContextInputChange = (seedExampleIndex: number, contextValue: string): void => { setSeedExamples( - seedExamples.map((seedExample: SeedExample, index: number) => + seedExamples.map((seedExample: SkillSeedExample, index: number) => index === seedExampleIndex ? { ...seedExample, @@ -199,7 +173,7 @@ export const SkillFormLocal: React.FunctionComponent = ({ skillE const handleContextBlur = (seedExampleIndex: number): void => { setSeedExamples( - seedExamples.map((seedExample: SeedExample, index: number) => + seedExamples.map((seedExample: SkillSeedExample, index: number) => index === seedExampleIndex ? { ...seedExample, @@ -212,7 +186,7 @@ export const SkillFormLocal: React.FunctionComponent = ({ skillE const handleAnswerInputChange = (seedExampleIndex: number, answerValue: string): void => { setSeedExamples( - seedExamples.map((seedExample: SeedExample, index: number) => + seedExamples.map((seedExample: SkillSeedExample, index: number) => index === seedExampleIndex ? { ...seedExample, @@ -225,7 +199,7 @@ export const SkillFormLocal: React.FunctionComponent = ({ skillE const handleAnswerBlur = (seedExampleIndex: number): void => { setSeedExamples( - seedExamples.map((seedExample: SeedExample, index: number) => + seedExamples.map((seedExample: SkillSeedExample, index: number) => index === seedExampleIndex ? { ...seedExample, @@ -238,7 +212,7 @@ export const SkillFormLocal: React.FunctionComponent = ({ skillE const handleQuestionInputChange = (seedExampleIndex: number, questionValue: string): void => { setSeedExamples( - seedExamples.map((seedExample: SeedExample, index: number) => + seedExamples.map((seedExample: SkillSeedExample, index: number) => index === seedExampleIndex ? { ...seedExample, @@ -251,7 +225,7 @@ export const SkillFormLocal: React.FunctionComponent = ({ skillE const handleQuestionBlur = (seedExampleIndex: number): void => { setSeedExamples( - seedExamples.map((seedExample: SeedExample, index: number) => + seedExamples.map((seedExample: SkillSeedExample, index: number) => index === seedExampleIndex ? { ...seedExample, @@ -343,7 +317,7 @@ export const SkillFormLocal: React.FunctionComponent = ({ skillE - {deploymentType === 'dev' && ( + {devModeEnabled && ( @@ -459,4 +433,4 @@ export const SkillFormLocal: React.FunctionComponent = ({ skillE ); }; -export default SkillFormLocal; +export default SkillFormNative; diff --git a/src/components/Experimental/ContributeLocal/Skill/skills.css b/src/components/Contribute/Skill/Native/skills.css similarity index 100% rename from src/components/Experimental/ContributeLocal/Skill/skills.css rename to src/components/Contribute/Skill/Native/skills.css diff --git a/src/components/Contribute/Skill/SkillsInformation/SkillsInformation.tsx b/src/components/Contribute/Skill/SkillsInformation/SkillsInformation.tsx index 9d40692f..c2f5fe4e 100644 --- a/src/components/Contribute/Skill/SkillsInformation/SkillsInformation.tsx +++ b/src/components/Contribute/Skill/SkillsInformation/SkillsInformation.tsx @@ -6,8 +6,8 @@ import { HelperText } from '@patternfly/react-core/dist/dynamic/components/Helpe import { HelperTextItem } from '@patternfly/react-core/dist/dynamic/components/HelperText'; import ExclamationCircleIcon from '@patternfly/react-icons/dist/dynamic/icons/exclamation-circle-icon'; import { ValidatedOptions } from '@patternfly/react-core/dist/esm/helpers/constants'; -import { SkillFormData } from '..'; import { checkSkillFormCompletion } from '../validation'; +import { SkillFormData } from '@/types'; interface Props { reset: boolean; diff --git a/src/components/Contribute/Skill/SkillsSeedExample/SkillsSeedExample.tsx b/src/components/Contribute/Skill/SkillsSeedExample/SkillsSeedExample.tsx index ceef056a..59a95c25 100644 --- a/src/components/Contribute/Skill/SkillsSeedExample/SkillsSeedExample.tsx +++ b/src/components/Contribute/Skill/SkillsSeedExample/SkillsSeedExample.tsx @@ -2,13 +2,13 @@ import React from 'react'; import { FormFieldGroupExpandable, FormFieldGroupHeader, FormGroup, FormHelperText } from '@patternfly/react-core/dist/dynamic/components/Form'; import { Button } from '@patternfly/react-core/dist/dynamic/components/Button'; import { TrashIcon, PlusCircleIcon, ExclamationCircleIcon, ExternalLinkAltIcon } from '@patternfly/react-icons/dist/dynamic/icons/'; -import { SeedExample } from '..'; import { TextArea } from '@patternfly/react-core/dist/esm/components/TextArea'; import { ValidatedOptions } from '@patternfly/react-core/dist/esm/helpers/constants'; import { HelperText, HelperTextItem } from '@patternfly/react-core/dist/esm/components/HelperText'; +import { SkillSeedExample } from '@/types'; interface Props { - seedExamples: SeedExample[]; + seedExamples: SkillSeedExample[]; handleContextInputChange: (seedExampleIndex: number, contextValue: string) => void; handleContextBlur: (seedExampleIndex: number) => void; handleQuestionInputChange: (seedExampleIndex: number, questionValue: string) => void; @@ -58,7 +58,7 @@ const SkillsSeedExample: React.FC = ({ /> } > - {seedExamples.map((seedExample: SeedExample, seedExampleIndex: number) => ( + {seedExamples.map((seedExample: SkillSeedExample, seedExampleIndex: number) => ( { const emailRegex = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$/; return emailRegex.test(email); }; -const hasDuplicateSeedExamples = (seedExamples: SeedExample[]): { duplicate: boolean; index: number } => { +const hasDuplicateSeedExamples = (seedExamples: SkillSeedExample[]): { duplicate: boolean; index: number } => { const question = new Set(); for (let index = 0; index < seedExamples.length; index++) { const seedExample = seedExamples[index]; diff --git a/src/components/Dashboard/index.tsx b/src/components/Dashboard/Github/dashboard.tsx similarity index 97% rename from src/components/Dashboard/index.tsx rename to src/components/Dashboard/Github/dashboard.tsx index c31b8d73..03a83ac0 100644 --- a/src/components/Dashboard/index.tsx +++ b/src/components/Dashboard/Github/dashboard.tsx @@ -1,3 +1,4 @@ +// src/components/dashboard/github/dashboard.tsx import * as React from 'react'; import { useSession } from 'next-auth/react'; import { Label } from '@patternfly/react-core/dist/dynamic/components/Label'; @@ -19,8 +20,8 @@ import GithubIcon from '@patternfly/react-icons/dist/esm/icons/github-icon'; import Image from 'next/image'; import { Button } from '@patternfly/react-core/dist/dynamic/components/Button'; import { useRouter } from 'next/navigation'; -import { fetchPullRequests, getGitHubUsername } from '../../utils/github'; -import { PullRequest } from '../../types'; +import { fetchPullRequests, getGitHubUsername } from '../../../utils/github'; +import { PullRequest } from '../../../types'; import { Breadcrumb, BreadcrumbItem } from '@patternfly/react-core/dist/esm/components/Breadcrumb'; import { Content } from '@patternfly/react-core/dist/esm/components/Content'; import OutlinedQuestionCircleIcon from '@patternfly/react-icons/dist/esm/icons/outlined-question-circle-icon'; @@ -32,7 +33,7 @@ import { Spinner } from '@patternfly/react-core/dist/esm/components/Spinner'; const InstructLabLogo: React.FC = () => InstructLab Logo; -const Index: React.FunctionComponent = () => { +const DashboardGithub: React.FunctionComponent = () => { const { data: session } = useSession(); const [pullRequests, setPullRequests] = React.useState([]); const [isFirstPullDone, setIsFirstPullDone] = React.useState(false); @@ -210,4 +211,4 @@ const Index: React.FunctionComponent = () => { ); }; -export { Index }; +export { DashboardGithub }; diff --git a/src/components/Dashboard/Native/dashboard.tsx b/src/components/Dashboard/Native/dashboard.tsx new file mode 100644 index 00000000..f5ffdf07 --- /dev/null +++ b/src/components/Dashboard/Native/dashboard.tsx @@ -0,0 +1,440 @@ +// src/components/Dashboard/Native/dashboard.tsx +import * as React from 'react'; +import { Card, CardBody } from '@patternfly/react-core/dist/dynamic/components/Card'; +import { Stack, StackItem } from '@patternfly/react-core/dist/dynamic/layouts/Stack'; +import { PageBreadcrumb } from '@patternfly/react-core/dist/dynamic/components/Page'; +import { PageSection } from '@patternfly/react-core/dist/dynamic/components/Page'; +import { Title } from '@patternfly/react-core/dist/dynamic/components/Title'; +import { Breadcrumb, BreadcrumbItem } from '@patternfly/react-core/dist/esm/components/Breadcrumb'; +import { Spinner } from '@patternfly/react-core/dist/esm/components/Spinner'; +import { Button } from '@patternfly/react-core/dist/dynamic/components/Button'; +import { Flex, FlexItem } from '@patternfly/react-core/dist/dynamic/layouts/Flex'; +import { Modal, ModalVariant } from '@patternfly/react-core/dist/esm/deprecated/components/Modal'; +import { EmptyState, EmptyStateBody, EmptyStateFooter, EmptyStateActions } from '@patternfly/react-core/dist/dynamic/components/EmptyState'; +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(); + } + }); + }, []); + + 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'); + const result = await response.json(); + if (response.ok) { + // Filter out 'main' branch + const filteredBranches = result.branches.filter((branch: { name: string }) => branch.name !== 'main'); + setBranches(filteredBranches); + } else { + console.error('Failed to fetch branches:', result.error); + } + } catch (error) { + console.error('Error fetching branches:', error); + } finally { + setIsLoading(false); + } + }; + + async function cloneNativeTaxonomyRepo(): Promise { + try { + const response = await fetch('/api/native/clone-repo', { + method: 'POST', + headers: { 'Content-Type': 'application/json' } + }); + + const result = await response.json(); + if (response.ok) { + console.log(result.message); + return true; + } else { + console.error(result.message); + return false; + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + console.error('Error cloning repo:', errorMessage); + return false; + } + } + + const formatDateTime = (timestamp: number) => { + const date = new Date(timestamp); + return `${date.toLocaleDateString()} ${date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`; + }; + + // Disabling Merge for now, leaving the code for when we re-implement the feature. + // const handleMerge = async (branchName: string) => { + // setMergeStatus(null); // Clear previous status + // try { + // const response = await fetch('/api/native/git/branches', { + // method: 'POST', + // headers: { 'Content-Type': 'application/json' }, + // body: JSON.stringify({ branchName, action: 'merge' }) + // }); + // + // const result = await response.json(); + // if (response.ok) { + // setMergeStatus({ branch: branchName, message: result.message, success: true }); + // } else { + // setMergeStatus({ branch: branchName, message: result.error, success: false }); + // } + // } catch (error) { + // setMergeStatus({ branch: branchName, message: 'Merge failed due to an unexpected error.', success: false }); + // console.error('Error merging branch:', error); + // } + // }; + + const handleShowChanges = async (branchName: string) => { + try { + const response = await fetch('/api/native/git/branches', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ branchName, action: 'diff' }) + }); + + const result = await response.json(); + if (response.ok) { + setDiffData({ branch: branchName, changes: result.changes }); + setIsModalOpen(true); + } else { + console.error('Failed to get branch changes:', result.error); + } + } catch (error) { + console.error('Error fetching branch changes:', error); + } + }; + + 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 ( +
+ + + Dashboard + + + + + Local Git Repository Branches + + + + + + {alerts.map(({ key, variant, title }) => ( + removeAlert(key!)} />} + key={key} + /> + ))} + + {isLoading ? ( + + ) : branches.length === 0 ? ( + + +
+ InstructLab is a powerful and accessible tool for advancing generative AI through community collaboration and open-source principles. + By contributing your own data, you can help train and refine the language model.
+
+ To get started, contribute a skill or contribute knowledge. +
+
+ + + + + + + + + + +
+ ) : ( + + {branches.map((branch) => ( + + + + + + Branch Name: {branch.name} +
+ Created on: {formatDateTime(branch.creationDate)} +
+ + Show Changes
}> + , + + ]} + > +

are you sure you want to delete this contribution?

+ + + setIsPublishModalOpen(false)} + aria-labelledby="form-modal-title" + aria-describedby="modal-box-description-form" + > + + + + + + + + + + + + ); +}; + +export { DashboardNative }; diff --git a/src/components/Experimental/CloneRepoLocal/CloneRepoLocal.module.css b/src/components/Experimental/CloneRepoLocal/CloneRepoLocal.module.css deleted file mode 100644 index f975b732..00000000 --- a/src/components/Experimental/CloneRepoLocal/CloneRepoLocal.module.css +++ /dev/null @@ -1,9 +0,0 @@ -/* CloneRepoLocal.module.css */ - -.formContainer { - padding: 3rem; -} - -.formGroup { - max-width: 500px; -} diff --git a/src/components/Experimental/CloneRepoLocal/CloneRepoLocal.tsx b/src/components/Experimental/CloneRepoLocal/CloneRepoLocal.tsx deleted file mode 100644 index a7e2cd1e..00000000 --- a/src/components/Experimental/CloneRepoLocal/CloneRepoLocal.tsx +++ /dev/null @@ -1,115 +0,0 @@ -// src/components/Experimental/CloneRepoLocal/CloneRepoLocal.tsx -'use client'; - -import React, { useState } from 'react'; -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 { ActionGroup } from '@patternfly/react-core/dist/dynamic/components/Form'; -import { Button } from '@patternfly/react-core/dist/dynamic/components/Button'; -import { FormHelperText } from '@patternfly/react-core/dist/dynamic/components/Form'; -import { HelperText } from '@patternfly/react-core/dist/dynamic/components/HelperText'; -import { HelperTextItem } from '@patternfly/react-core/dist/dynamic/components/HelperText'; -import styles from './CloneRepoLocal.module.css'; - -// Retrieve the public base directory from environment variables -const BASE_DIRECTORY = process.env.NEXT_PUBLIC_BASE_CLONE_DIRECTORY; - -const CloneRepoLocal: React.FC = () => { - const [repoUrl, setRepoUrl] = useState(''); - const [directory, setDirectory] = useState(''); - const [message, setMessage] = useState(''); - const [fullPath, setFullPath] = useState(''); - - const handleRepoUrlChange = (_event: React.FormEvent, value: string) => { - setRepoUrl(value); - }; - - const handleDirectoryChange = (_event: React.FormEvent, value: string) => { - setDirectory(value); - }; - - const handleCloneRepo = async () => { - if (!repoUrl || !directory) { - setMessage('Please provide both repository URL and directory path.'); - return; - } - - try { - const response = await fetch('/api/local/clone-repo', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ repoUrl, directory }) - }); - - const result = await response.json(); - if (response.ok) { - setMessage(result.message); - setFullPath(result.fullPath); // Store the full path to display to the user - } else { - setMessage(`Error: ${result.message}`); - setFullPath(''); // Clear full path if there's an error - } - } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - setMessage(`An unexpected error occurred: ${errorMessage}`); - setFullPath(''); - } - }; - - return ( -
- - - - {`Base Directory: ${BASE_DIRECTORY}`} - - - - - - - - - Enter the repository URL. - URL should be a valid Git repository. - - - - - - - - - Enter the directory path. - The cloned directory will be appended to the base directory path. - - - - - - - - - {message && ( - - - {message} - - - )} - - {fullPath && ( - - - Cloned to: {fullPath} - - - )} -
- ); -}; - -export default CloneRepoLocal; diff --git a/src/components/Experimental/ContributeLocal/Knowledge/knowledge.css b/src/components/Experimental/ContributeLocal/Knowledge/knowledge.css deleted file mode 100644 index ab9c58c9..00000000 --- a/src/components/Experimental/ContributeLocal/Knowledge/knowledge.css +++ /dev/null @@ -1,39 +0,0 @@ -/* Knowledge CSS */ - -.form-k { - width: 80%; - margin-bottom: 50px; - background-color: white; -} - -.submit-k:hover, -.download-k-yaml:hover, -.download-k-attribution:hover, -.button-active, -.button-active:hover { - background-color: #45a049; -} - -.heading-k { - text-align: left; - font-size: medium; -} - -.button-secondary:hover { - border-color: #45a049; -} - -.spinner-container { - display: flex; - justify-content: center; - align-items: center; - gap: 1rem; -} - -.knowledge-form .pf-c-form__group { - margin-bottom: 30px; -} - -.knowledge-form { - line-height: 3; -} diff --git a/src/components/Experimental/DashboardLocal/index.tsx b/src/components/Experimental/DashboardLocal/index.tsx deleted file mode 100644 index 3db4208d..00000000 --- a/src/components/Experimental/DashboardLocal/index.tsx +++ /dev/null @@ -1,211 +0,0 @@ -// src/components/Experimental/DashboardLocal/index.tsx -import * as React from 'react'; -import { Card, CardBody } from '@patternfly/react-core/dist/dynamic/components/Card'; -import { Stack, StackItem } from '@patternfly/react-core/dist/dynamic/layouts/Stack'; -import { PageBreadcrumb } from '@patternfly/react-core/dist/dynamic/components/Page'; -import { PageSection } from '@patternfly/react-core/dist/dynamic/components/Page'; -import { Title } from '@patternfly/react-core/dist/dynamic/components/Title'; -import { Breadcrumb, BreadcrumbItem } from '@patternfly/react-core/dist/esm/components/Breadcrumb'; -import { Spinner } from '@patternfly/react-core/dist/esm/components/Spinner'; -import { Button } from '@patternfly/react-core/dist/dynamic/components/Button'; -import { Flex, FlexItem } from '@patternfly/react-core/dist/dynamic/layouts/Flex'; -import { Modal, ModalVariant } from '@patternfly/react-core/dist/esm/deprecated/components/Modal'; -import { EmptyState, EmptyStateBody, EmptyStateFooter, EmptyStateActions } from '@patternfly/react-core/dist/dynamic/components/EmptyState'; -import GithubIcon from '@patternfly/react-icons/dist/esm/icons/github-icon'; -import Image from 'next/image'; -import { useRouter } from 'next/navigation'; - -const InstructLabLogo: React.FC = () => InstructLab Logo; - -const DashboardLocal: React.FunctionComponent = () => { - const [branches, setBranches] = React.useState<{ name: string; creationDate: number }[]>([]); - 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 router = useRouter(); - - // Fetch branches from the API route - React.useEffect(() => { - const fetchBranches = async () => { - try { - const response = await fetch('/api/local/git/branches'); - const result = await response.json(); - if (response.ok) { - // Filter out 'main' branch - const filteredBranches = result.branches.filter((branch: { name: string }) => branch.name !== 'main'); - setBranches(filteredBranches); - } else { - console.error('Failed to fetch branches:', result.error); - } - } catch (error) { - console.error('Error fetching branches:', error); - } finally { - setIsLoading(false); - } - }; - - fetchBranches(); - }, []); - - const formatDateTime = (timestamp: number) => { - const date = new Date(timestamp); - return `${date.toLocaleDateString()} ${date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`; - }; - - // Disabling Merge for now, leaving the code for when we re-implement the feature. - // const handleMerge = async (branchName: string) => { - // setMergeStatus(null); // Clear previous status - // try { - // const response = await fetch('/api/local/git/branches', { - // method: 'POST', - // headers: { 'Content-Type': 'application/json' }, - // body: JSON.stringify({ branchName, action: 'merge' }) - // }); - // - // const result = await response.json(); - // if (response.ok) { - // setMergeStatus({ branch: branchName, message: result.message, success: true }); - // } else { - // setMergeStatus({ branch: branchName, message: result.error, success: false }); - // } - // } catch (error) { - // setMergeStatus({ branch: branchName, message: 'Merge failed due to an unexpected error.', success: false }); - // console.error('Error merging branch:', error); - // } - // }; - - const handleShowChanges = async (branchName: string) => { - try { - const response = await fetch('/api/local/git/branches', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ branchName, action: 'diff' }) - }); - - const result = await response.json(); - if (response.ok) { - setDiffData({ branch: branchName, changes: result.changes }); - setIsModalOpen(true); - } else { - console.error('Failed to get branch changes:', result.error); - } - } catch (error) { - console.error('Error fetching branch changes:', error); - } - }; - - return ( -
- - - Dashboard - - - - - Local Git Repository Branches - - - - - {isLoading ? ( - - ) : branches.length === 0 ? ( - - -
- InstructLab is a powerful and accessible tool for advancing generative AI through community collaboration and open-source principles. - By contributing your own data, you can help train and refine the language model.
-
- To get started, contribute a skill or contribute knowledge. -
-
- - - - - - - - - - - -
- ) : ( - - {branches.map((branch) => ( - - - - - - Branch Name: {branch.name} -
- Created on: {formatDateTime(branch.creationDate)} -
- - {/**/} - - -
-
-
-
- ))} -
- )} - - {mergeStatus && ( - -

{mergeStatus.message}

-
- )} - - setIsModalOpen(false)} - > - {diffData?.changes.length ? ( -
    - {diffData.changes.map((change) => ( -
  • - {change.file} - {change.status} -
  • - ))} -
- ) : ( -

No differences found.

- )} -
-
-
- ); -}; - -export { DashboardLocal }; diff --git a/src/components/GithubAccessPopup/index.tsx b/src/components/GithubAccessPopup/index.tsx index bfa634ac..66044980 100644 --- a/src/components/GithubAccessPopup/index.tsx +++ b/src/components/GithubAccessPopup/index.tsx @@ -17,7 +17,7 @@ const GithubAccessPopup: React.FunctionComponent = ({ onAccept }) => { const showPopupWarning = async () => { const res = await fetch('/api/envConfig'); const envConfig = await res.json(); - if (envConfig.DEPLOYMENT_TYPE === 'dev') { + if (envConfig.DEPLOYMENT_TYPE === 'native' || envConfig.ENABLE_DEV_MODE === 'true') { setIsOpen(false); onAccept(); } else { diff --git a/src/types/index.ts b/src/types/index.ts index 736124bf..a1f0ead9 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,3 +1,5 @@ +import { ValidatedOptions } from '@patternfly/react-core/dist/esm/helpers/constants'; + export interface Endpoint { id: string; url: string; @@ -79,3 +81,76 @@ export interface PullRequestUpdateData { title: string; body: string; } + +export interface SkillSeedExample { + immutable: boolean; + isExpanded: boolean; + context?: string; + isContextValid?: ValidatedOptions; + validationError?: string; + question: string; + isQuestionValid: ValidatedOptions; + questionValidationError?: string; + answer: string; + isAnswerValid: ValidatedOptions; + answerValidationError?: string; +} + +export interface SkillFormData { + email: string; + name: string; + submissionSummary: string; + documentOutline: string; + filePath: string; + seedExamples: SkillSeedExample[]; + titleWork: string; + licenseWork: string; + creators: string; +} + +export interface QuestionAndAnswerPair { + immutable: boolean; + question: string; + isQuestionValid: ValidatedOptions; + questionValidationError?: string; + answer: string; + isAnswerValid: ValidatedOptions; + answerValidationError?: string; +} + +export interface KnowledgeSeedExample { + immutable: boolean; + isExpanded: boolean; + context: string; + isContextValid: ValidatedOptions; + validationError?: string; + questionAndAnswers: QuestionAndAnswerPair[]; +} + +export interface KnowledgeFormData { + email: string; + name: string; + submissionSummary: string; + domain: string; + documentOutline: string; + filePath: string; + seedExamples: KnowledgeSeedExample[]; + knowledgeDocumentRepositoryUrl: string; + knowledgeDocumentCommit: string; + documentName: string; + titleWork: string; + linkWork: string; + revision: string; + licenseWork: string; + creators: string; +} + +export interface KnowledgeEditFormData { + isEditForm: boolean; + knowledgeVersion: number; + pullRequestNumber: number; + branchName: string; + yamlFile: PullRequestFile; + attributionFile: PullRequestFile; + knowledgeFormData: KnowledgeFormData; +}