From ea8be83c6f441ab9686a100c5ca6cfd118f8efc4 Mon Sep 17 00:00:00 2001 From: Brent Salisbury Date: Sun, 3 Nov 2024 16:54:59 -0500 Subject: [PATCH 1/3] Feature: enable the UI to operate with a local git repo only Signed-off-by: Brent Salisbury --- .env.example | 4 + package-lock.json | 631 +++++------------- package.json | 3 + src/app/api/local/clone-repo/route.ts | 45 ++ src/app/api/local/git/branches/route.ts | 123 ++++ src/app/api/local/pr/knowledge/route.ts | 73 ++ src/app/api/local/pr/skill/route.ts | 70 ++ .../configuration-local/page.tsx | 14 + .../contribute-local/knowledge/page.tsx | 14 + .../contribute-local/skill/page.tsx | 14 + src/app/experimental/dashboard-local/page.tsx | 17 + src/components/AppLayout.tsx | 8 +- .../CloneRepoLocal/CloneRepoLocal.module.css | 9 + .../CloneRepoLocal/CloneRepoLocal.tsx | 115 ++++ .../Knowledge/SubmitLocal/Submit.tsx | 112 ++++ .../ContributeLocal/Knowledge/index.tsx | 611 +++++++++++++++++ .../ContributeLocal/Knowledge/knowledge.css | 31 + .../Skill/SubmitLocal/SubmitLocal.tsx | 106 +++ .../ContributeLocal/Skill/index.tsx | 484 ++++++++++++++ .../ContributeLocal/Skill/skills.css | 18 + .../Experimental/DashboardLocal/index.tsx | 161 +++++ 21 files changed, 2213 insertions(+), 450 deletions(-) create mode 100644 src/app/api/local/clone-repo/route.ts create mode 100644 src/app/api/local/git/branches/route.ts create mode 100644 src/app/api/local/pr/knowledge/route.ts create mode 100644 src/app/api/local/pr/skill/route.ts create mode 100644 src/app/experimental/contribute-local/configuration-local/page.tsx create mode 100644 src/app/experimental/contribute-local/knowledge/page.tsx create mode 100644 src/app/experimental/contribute-local/skill/page.tsx create mode 100644 src/app/experimental/dashboard-local/page.tsx create mode 100644 src/components/Experimental/CloneRepoLocal/CloneRepoLocal.module.css create mode 100644 src/components/Experimental/CloneRepoLocal/CloneRepoLocal.tsx create mode 100644 src/components/Experimental/ContributeLocal/Knowledge/SubmitLocal/Submit.tsx create mode 100644 src/components/Experimental/ContributeLocal/Knowledge/index.tsx create mode 100644 src/components/Experimental/ContributeLocal/Knowledge/knowledge.css create mode 100644 src/components/Experimental/ContributeLocal/Skill/SubmitLocal/SubmitLocal.tsx create mode 100644 src/components/Experimental/ContributeLocal/Skill/index.tsx create mode 100644 src/components/Experimental/ContributeLocal/Skill/skills.css create mode 100644 src/components/Experimental/DashboardLocal/index.tsx diff --git a/.env.example b/.env.example index 9ad509e3..b52e8d0f 100644 --- a/.env.example +++ b/.env.example @@ -18,4 +18,8 @@ TAXONOMY_DOCUMENTS_REPO=github.com/instructlab-public/taxonomy-knowledge-docs NEXT_PUBLIC_AUTHENTICATION_ORG= NEXT_PUBLIC_TAXONOMY_REPO_OWNER= NEXT_PUBLIC_TAXONOMY_REPO= + 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/package-lock.json b/package-lock.json index 5ebd1426..3373f501 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,8 @@ "@patternfly/react-styles": "^6.0.0", "@patternfly/react-table": "^6.0.0", "axios": "^1.7.7", + "fs": "^0.0.1-security", + "isomorphic-git": "^1.27.1", "js-yaml": "^4.1.0", "next": "^15.0.2", "next-auth": "^4.24.10", @@ -31,6 +33,7 @@ "@next/eslint-plugin-next": "^14.2.3", "@playwright/test": "^1.47.2", "@redhat-cloud-services/eslint-config-redhat-cloud-services": "^2.0.4", + "@types/js-cookie": "^3.0.6", "@types/js-yaml": "^4.0.9", "@types/node": "^22.5.0", "@types/react": "18.3.1", @@ -341,15 +344,6 @@ "kuler": "^2.0.0" } }, - "node_modules/@emnapi/runtime": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.3.1.tgz", - "integrity": "sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -590,27 +584,6 @@ "@img/sharp-libvips-darwin-arm64": "1.0.4" } }, - "node_modules/@img/sharp-darwin-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", - "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.0.4" - } - }, "node_modules/@img/sharp-libvips-darwin-arm64": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", @@ -626,291 +599,6 @@ "url": "https://opencollective.com/libvips" } }, - "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", - "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", - "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", - "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", - "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", - "cpu": [ - "s390x" - ], - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", - "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", - "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", - "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-linux-arm": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", - "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.0.5" - } - }, - "node_modules/@img/sharp-linux-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", - "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.0.4" - } - }, - "node_modules/@img/sharp-linux-s390x": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", - "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", - "cpu": [ - "s390x" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.0.4" - } - }, - "node_modules/@img/sharp-linux-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", - "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.0.4" - } - }, - "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", - "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" - } - }, - "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", - "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.0.4" - } - }, - "node_modules/@img/sharp-wasm32": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", - "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", - "cpu": [ - "wasm32" - ], - "optional": true, - "dependencies": { - "@emnapi/runtime": "^1.2.0" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-ia32": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", - "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", - "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -1041,111 +729,6 @@ "node": ">= 10" } }, - "node_modules/@next/swc-darwin-x64": { - "version": "15.0.2", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.0.2.tgz", - "integrity": "sha512-KUpBVxIbjzFiUZhiLIpJiBoelqzQtVZbdNNsehhUn36e2YzKHphnK8eTUW1s/4aPy5kH/UTid8IuVbaOpedhpw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-gnu": { - "version": "15.0.2", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.0.2.tgz", - "integrity": "sha512-9J7TPEcHNAZvwxXRzOtiUvwtTD+fmuY0l7RErf8Yyc7kMpE47MIQakl+3jecmkhOoIyi/Rp+ddq7j4wG6JDskQ==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-musl": { - "version": "15.0.2", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.0.2.tgz", - "integrity": "sha512-BjH4ZSzJIoTTZRh6rG+a/Ry4SW0HlizcPorqNBixBWc3wtQtj4Sn9FnRZe22QqrPnzoaW0ctvSz4FaH4eGKMww==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-x64-gnu": { - "version": "15.0.2", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.0.2.tgz", - "integrity": "sha512-i3U2TcHgo26sIhcwX/Rshz6avM6nizrZPvrDVDY1bXcLH1ndjbO8zuC7RoHp0NSK7wjJMPYzm7NYL1ksSKFreA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-x64-musl": { - "version": "15.0.2", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.0.2.tgz", - "integrity": "sha512-AMfZfSVOIR8fa+TXlAooByEF4OB00wqnms1sJ1v+iu8ivwvtPvnkwdzzFMpsK5jA2S9oNeeQ04egIWVb4QWmtQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-arm64-msvc": { - "version": "15.0.2", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.0.2.tgz", - "integrity": "sha512-JkXysDT0/hEY47O+Hvs8PbZAeiCQVxKfGtr4GUpNAhlG2E0Mkjibuo8ryGD29Qb5a3IOnKYNoZlh/MyKd2Nbww==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-x64-msvc": { - "version": "15.0.2", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.0.2.tgz", - "integrity": "sha512-foaUL0NqJY/dX0Pi/UcZm5zsmSk5MtP/gxx3xOPyREkMFN+CTjctPfu3QaqrQHinaKdPnMWPJDKt4VjDfTBe/Q==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { "version": "5.1.1-v1", "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", @@ -1204,16 +787,16 @@ } }, "node_modules/@patternfly/react-core": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-5.4.1.tgz", - "integrity": "sha512-PJjwN4OCR7jTdWKi0RzuFdtlSQ8gBR+0REczuDHHPW8ky0bs1cIcqGsn5p/b6OgPlztl3UaXqRYLsroiEMasOw==", + "version": "5.4.8", + "resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-5.4.8.tgz", + "integrity": "sha512-4KRsQsH39VmTiFPLdN34QqNZg6gKrTamJxKtWEPO1VKA0TpoRMwpFEGk9BDyxipxYST6WzXznAaLCidGkCDlWw==", "dependencies": { - "@patternfly/react-icons": "^5.4.0", - "@patternfly/react-styles": "^5.4.0", - "@patternfly/react-tokens": "^5.4.0", - "focus-trap": "7.5.4", + "@patternfly/react-icons": "^5.4.2", + "@patternfly/react-styles": "^5.4.1", + "@patternfly/react-tokens": "^5.4.1", + "focus-trap": "7.6.0", "react-dropzone": "^14.2.3", - "tslib": "^2.6.3" + "tslib": "^2.7.0" }, "peerDependencies": { "react": "^17 || ^18", @@ -1234,6 +817,11 @@ "resolved": "https://registry.npmjs.org/@patternfly/react-styles/-/react-styles-5.4.1.tgz", "integrity": "sha512-XA8PXksD8uiA3RTwxdUwJXOCf+V6sVd+2HKapWAdRLvtSV+Sdk7NgCvalb4IAQncsddLopjPQD8gAHA298+N8w==" }, + "node_modules/@patternfly/react-core/node_modules/@patternfly/react-tokens": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/@patternfly/react-tokens/-/react-tokens-5.4.1.tgz", + "integrity": "sha512-eygdHE7Krta1mijAv/E8RHiKIgysD0eeNTo8EXUYC8/M4e5K6sqpr2p6rQBF8QiRMN8FnbXvZT3K2OQ28pYt9Q==" + }, "node_modules/@patternfly/react-icons": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-6.0.0.tgz", @@ -1282,24 +870,11 @@ "react-dom": "^17 || ^18" } }, - "node_modules/@patternfly/react-table/node_modules/@patternfly/react-tokens": { + "node_modules/@patternfly/react-tokens": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/@patternfly/react-tokens/-/react-tokens-6.0.0.tgz", "integrity": "sha512-xd0ynDkiIW2rp8jz4TNvR4Dyaw9kSMkZdsuYcLlFXCVmvX//Mnl4rhBnid/2j2TaqK0NbkyTTPnPY/BU7SfLVQ==" }, - "node_modules/@patternfly/react-table/node_modules/focus-trap": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.6.0.tgz", - "integrity": "sha512-1td0l3pMkWJLFipobUcGaf+5DTY4PLDDrcqoSaKP8ediO/CoWCCYk/fT/Y2A4e6TNB+Sh6clRJCjOPPnKoNHnQ==", - "dependencies": { - "tabbable": "^6.2.0" - } - }, - "node_modules/@patternfly/react-tokens": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@patternfly/react-tokens/-/react-tokens-5.4.0.tgz", - "integrity": "sha512-KONkwCVOMyklhuuaYeYgcAsGtCBQXnsBGZeolhOdSzr2Mj0RVSW0oMrQPgZuPVzhhC/kbqgClHJJl6xuG9xheA==" - }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -1427,6 +1002,12 @@ "hoist-non-react-statics": "^3.3.0" } }, + "node_modules/@types/js-cookie": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-3.0.6.tgz", + "integrity": "sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==", + "dev": true + }, "node_modules/@types/js-yaml": { "version": "4.0.9", "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", @@ -1985,6 +1566,11 @@ "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==", "license": "MIT" }, + "node_modules/async-lock": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.4.1.tgz", + "integrity": "sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==" + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -2169,6 +1755,11 @@ } ] }, + "node_modules/clean-git-ref": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/clean-git-ref/-/clean-git-ref-2.0.1.tgz", + "integrity": "sha512-bLSptAy2P0s6hU4PzuIMKmMJJSE6gLXGH1cntDu7bWJUksvuM+7ReOK61mozULErYvP6a15rnYl0zFDef+pyPw==" + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", @@ -2253,6 +1844,17 @@ "node": ">= 0.6" } }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -2363,6 +1965,20 @@ } } }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/deep-equal": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz", @@ -2457,6 +2073,11 @@ "node": ">=8" } }, + "node_modules/diff3": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/diff3/-/diff3-0.0.3.tgz", + "integrity": "sha512-iSq8ngPOt0K53A6eVr4d5Kn6GNrM2nQZtC740pzIriHtn4pOQ2lyzEXQMBeVcWERN0ye7fhBsk9PbLLQOnUx/g==" + }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -3770,9 +3391,9 @@ "license": "MIT" }, "node_modules/focus-trap": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.5.4.tgz", - "integrity": "sha512-N7kHdlgsO/v+iD/dMoJKtsSqs5Dz/dXZVebRgJw23LDk+jMi/974zyiOYDziY2JPp8xivq9BmUGwIJMiuSBi7w==", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.6.0.tgz", + "integrity": "sha512-1td0l3pMkWJLFipobUcGaf+5DTY4PLDDrcqoSaKP8ediO/CoWCCYk/fT/Y2A4e6TNB+Sh6clRJCjOPPnKoNHnQ==", "dependencies": { "tabbable": "^6.2.0" } @@ -3850,6 +3471,11 @@ "node": ">=12.20.0" } }, + "node_modules/fs": { + "version": "0.0.1-security", + "resolved": "https://registry.npmjs.org/fs/-/fs-0.0.1-security.tgz", + "integrity": "sha512-3XY9e1pP0CVEUCdj5BmfIZxRBTSDycnbqhIOGec9QYtmVH2fbLpj86CFWkrNOkt/Fvty4KZG5lTglL9j/gJ87w==" + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -4174,7 +3800,6 @@ "version": "5.3.1", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 4" @@ -4674,6 +4299,30 @@ "dev": true, "license": "ISC" }, + "node_modules/isomorphic-git": { + "version": "1.27.1", + "resolved": "https://registry.npmjs.org/isomorphic-git/-/isomorphic-git-1.27.1.tgz", + "integrity": "sha512-X32ph5zIWfT75QAqW2l3JCIqnx9/GWd17bRRehmn3qmWc34OYbSXY6Cxv0o9bIIY+CWugoN4nQFHNA+2uYf2nA==", + "dependencies": { + "async-lock": "^1.4.1", + "clean-git-ref": "^2.0.1", + "crc-32": "^1.2.0", + "diff3": "0.0.3", + "ignore": "^5.1.4", + "minimisted": "^2.0.0", + "pako": "^1.0.10", + "pify": "^4.0.1", + "readable-stream": "^3.4.0", + "sha.js": "^2.4.9", + "simple-get": "^4.0.1" + }, + "bin": { + "isogit": "cli.cjs" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/iterator.prototype": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.2.tgz", @@ -4960,6 +4609,17 @@ "node": ">= 0.6" } }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", @@ -4980,12 +4640,19 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minimisted": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minimisted/-/minimisted-2.0.1.tgz", + "integrity": "sha512-1oPjfuLQa2caorJUM8HV8lGgWCc0qqAO1MNv/k05G4qslmsndV/5WdNZrqCiyqiz3wohia2Ij2B7w2Dr7/IyrA==", + "dependencies": { + "minimist": "^1.2.5" + } + }, "node_modules/minipass": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", @@ -5373,7 +5040,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -5471,6 +5137,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -5573,6 +5244,14 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "engines": { + "node": ">=6" + } + }, "node_modules/playwright": { "version": "1.47.2", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.47.2.tgz", @@ -6091,6 +5770,18 @@ "node": ">= 0.4" } }, + "node_modules/sha.js": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "dependencies": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + }, + "bin": { + "sha.js": "bin.js" + } + }, "node_modules/sharp": { "version": "0.33.5", "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", @@ -6228,6 +5919,49 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/simple-swizzle": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", @@ -7142,7 +6876,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, "license": "ISC" }, "node_modules/yallist": { diff --git a/package.json b/package.json index 1f60f2fd..c73857dc 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,8 @@ "@patternfly/react-styles": "^6.0.0", "@patternfly/react-table": "^6.0.0", "axios": "^1.7.7", + "fs": "^0.0.1-security", + "isomorphic-git": "^1.27.1", "js-yaml": "^4.1.0", "next": "^15.0.2", "next-auth": "^4.24.10", @@ -39,6 +41,7 @@ "@next/eslint-plugin-next": "^14.2.3", "@playwright/test": "^1.47.2", "@redhat-cloud-services/eslint-config-redhat-cloud-services": "^2.0.4", + "@types/js-cookie": "^3.0.6", "@types/js-yaml": "^4.0.9", "@types/node": "^22.5.0", "@types/react": "18.3.1", diff --git a/src/app/api/local/clone-repo/route.ts b/src/app/api/local/clone-repo/route.ts new file mode 100644 index 00000000..309582c5 --- /dev/null +++ b/src/app/api/local/clone-repo/route.ts @@ -0,0 +1,45 @@ +// 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 new file mode 100644 index 00000000..9d880342 --- /dev/null +++ b/src/app/api/local/git/branches/route.ts @@ -0,0 +1,123 @@ +// 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 }); + + // Return the list of branches as JSON + return NextResponse.json({ branches }, { 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 } + ); + } +} + +// 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/local/pr/knowledge/route.ts b/src/app/api/local/pr/knowledge/route.ts new file mode 100644 index 00000000..a378d3fd --- /dev/null +++ b/src/app/api/local/pr/knowledge/route.ts @@ -0,0 +1,73 @@ +// src/app/api/local/pr/knowledge/route.ts + +import { NextResponse } from 'next/server'; +import { NextRequest } from 'next/server'; +import * as git from 'isomorphic-git'; +import fs from 'fs'; +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 KNOWLEDGE_DIR = 'knowledge'; + +export async function POST(req: NextRequest) { + 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 + + // Define branch name and file paths + const branchName = `knowledge-contribution-${Date.now()}`; + const newYamlFilePath = path.join(KNOWLEDGE_DIR, filePath, 'qna.yaml'); + const newAttributionFilePath = path.join(KNOWLEDGE_DIR, filePath, 'attribution.txt'); + + // Convert data to YAML and plain text formats + const yamlString = yaml.dump(content); + const attributionContent = ` +Title of work: ${attribution.title_of_work} +Link to work: ${attribution.link_to_work} +Revision: ${attribution.revision} +License of the work: ${attribution.license_of_the_work} +Creator names: ${attribution.creator_names} +`; + + // Initialize the repository if it doesn’t exist + await git.init({ fs, dir: REPO_DIR }); + + // Create a new branch + await git.branch({ fs, dir: REPO_DIR, ref: branchName }); + + // Checkout the new branch + await git.checkout({ fs, dir: REPO_DIR, ref: branchName }); + + // Write YAML file to the knowledge directory + const yamlFilePath = path.join(REPO_DIR, newYamlFilePath); + fs.mkdirSync(path.dirname(yamlFilePath), { recursive: true }); + fs.writeFileSync(yamlFilePath, yamlString); + + // Write attribution file to the knowledge directory + const attributionFilePath = path.join(REPO_DIR, newAttributionFilePath); + fs.writeFileSync(attributionFilePath, attributionContent); + + // Stage the files + await git.add({ fs, dir: REPO_DIR, filepath: newYamlFilePath }); + await git.add({ fs, dir: REPO_DIR, filepath: newAttributionFilePath }); + + // Commit the changes + await git.commit({ + fs, + dir: REPO_DIR, + message: `${submissionSummary}\n\nSigned-off-by: ${name} <${email}>`, + author: { + name: name, + email: email + } + }); + + // Respond with success message and branch name + return NextResponse.json({ message: 'Branch and commit created locally', branch: branchName }, { status: 201 }); + } catch (error) { + console.error('Failed to create local branch and commit:', error); + return NextResponse.json({ error: 'Failed to create local branch and commit' }, { status: 500 }); + } +} diff --git a/src/app/api/local/pr/skill/route.ts b/src/app/api/local/pr/skill/route.ts new file mode 100644 index 00000000..873da074 --- /dev/null +++ b/src/app/api/local/pr/skill/route.ts @@ -0,0 +1,70 @@ +// src/app/api/local/pr/skill/route.ts +import { NextResponse } from 'next/server'; +import { NextRequest } from 'next/server'; +import * as git from 'isomorphic-git'; +import fs from 'fs'; +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 SKILLS_DIR = 'compositional_skills'; + +export async function POST(req: NextRequest) { + 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 + + // Define file paths + const branchName = `skill-contribution-${Date.now()}`; + const newYamlFilePath = path.join(SKILLS_DIR, filePath, 'qna.yaml'); + const newAttributionFilePath = path.join(SKILLS_DIR, filePath, 'attribution.txt'); + + // Prepare file content + const yamlString = yaml.dump(content); + const attributionString = ` +Title of work: ${attribution.title_of_work} +License of the work: ${attribution.license_of_the_work} +Creator names: ${attribution.creator_names} +`; + + // Initialize the repository if it doesn’t exist + await git.init({ fs, dir: REPO_DIR }); + + // Create a new branch + await git.branch({ fs, dir: REPO_DIR, ref: branchName }); + + // Checkout the new branch + await git.checkout({ fs, dir: REPO_DIR, ref: branchName }); + + // Write the QnA YAML file + const yamlFilePath = path.join(REPO_DIR, newYamlFilePath); + fs.mkdirSync(path.dirname(yamlFilePath), { recursive: true }); + fs.writeFileSync(yamlFilePath, yamlString); + + // Write the attribution text file + const attributionFilePath = path.join(REPO_DIR, newAttributionFilePath); + fs.writeFileSync(attributionFilePath, attributionString); + + // Stage files + await git.add({ fs, dir: REPO_DIR, filepath: newYamlFilePath }); + await git.add({ fs, dir: REPO_DIR, filepath: newAttributionFilePath }); + + // Commit files + await git.commit({ + fs, + dir: REPO_DIR, + message: `${submissionSummary}\n\nSigned-off-by: ${name} <${email}>`, + author: { + name: name, + email: email + } + }); + + // Respond with success + return NextResponse.json({ message: 'Branch and commit created locally', branch: branchName }, { status: 201 }); + } catch (error) { + console.error('Failed to create local branch and commit:', error); + return NextResponse.json({ error: 'Failed to create local branch and commit' }, { status: 500 }); + } +} diff --git a/src/app/experimental/contribute-local/configuration-local/page.tsx b/src/app/experimental/contribute-local/configuration-local/page.tsx new file mode 100644 index 00000000..c7550df8 --- /dev/null +++ b/src/app/experimental/contribute-local/configuration-local/page.tsx @@ -0,0 +1,14 @@ +// 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 new file mode 100644 index 00000000..dc1a4cf2 --- /dev/null +++ b/src/app/experimental/contribute-local/knowledge/page.tsx @@ -0,0 +1,14 @@ +// 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 new file mode 100644 index 00000000..9bcdc347 --- /dev/null +++ b/src/app/experimental/contribute-local/skill/page.tsx @@ -0,0 +1,14 @@ +// 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 new file mode 100644 index 00000000..e1da680f --- /dev/null +++ b/src/app/experimental/dashboard-local/page.tsx @@ -0,0 +1,17 @@ +// 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/components/AppLayout.tsx b/src/components/AppLayout.tsx index 0999861c..417189cd 100644 --- a/src/components/AppLayout.tsx +++ b/src/components/AppLayout.tsx @@ -87,7 +87,13 @@ const AppLayout: React.FunctionComponent = ({ children }) => { }, isExperimentalEnabled && { path: '/experimental', - label: 'Experimental Features' + 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' } + ] } ].filter(Boolean) as Route[]; diff --git a/src/components/Experimental/CloneRepoLocal/CloneRepoLocal.module.css b/src/components/Experimental/CloneRepoLocal/CloneRepoLocal.module.css new file mode 100644 index 00000000..f975b732 --- /dev/null +++ b/src/components/Experimental/CloneRepoLocal/CloneRepoLocal.module.css @@ -0,0 +1,9 @@ +/* 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 new file mode 100644 index 00000000..a7e2cd1e --- /dev/null +++ b/src/components/Experimental/CloneRepoLocal/CloneRepoLocal.tsx @@ -0,0 +1,115 @@ +// 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/SubmitLocal/Submit.tsx b/src/components/Experimental/ContributeLocal/Knowledge/SubmitLocal/Submit.tsx new file mode 100644 index 00000000..93ca97df --- /dev/null +++ b/src/components/Experimental/ContributeLocal/Knowledge/SubmitLocal/Submit.tsx @@ -0,0 +1,112 @@ +// src/components/Experimental/ContributeLocal/Knowledge/SubmitLocal/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 { KnowledgeSchemaVersion } from '@/types/const'; +import { dumpYaml } from '@/utils/yamlConfig'; +import { validateFields } from '@/components/Contribute/Knowledge/validation'; + +interface Props { + disableAction: boolean; + knowledgeFormData: KnowledgeFormData; + setActionGroupAlertContent: React.Dispatch>; + githubUsername: string | undefined; + resetForm: () => void; +} + +const Submit: React.FC = ({ disableAction, knowledgeFormData, setActionGroupAlertContent, githubUsername, resetForm }) => { + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + if (!validateFields(knowledgeFormData, setActionGroupAlertContent)) return; + + // Strip leading slash and ensure trailing slash in the file path + let sanitizedFilePath = knowledgeFormData.filePath!.startsWith('/') ? knowledgeFormData.filePath!.slice(1) : knowledgeFormData.filePath; + sanitizedFilePath = sanitizedFilePath!.endsWith('/') ? sanitizedFilePath : `${sanitizedFilePath}/`; + + const knowledgeYamlData: KnowledgeYamlData = { + created_by: githubUsername!, + version: KnowledgeSchemaVersion, + domain: knowledgeFormData.domain!, + document_outline: knowledgeFormData.documentOutline!, + seed_examples: knowledgeFormData.seedExamples.map((example) => ({ + context: example.context, + questions_and_answers: example.questionAndAnswers.map((questionAndAnswer) => ({ + question: questionAndAnswer.question, + answer: questionAndAnswer.answer + })) + })), + document: { + repo: knowledgeFormData.knowledgeDocumentRepositoryUrl!, + commit: knowledgeFormData.knowledgeDocumentCommit!, + patterns: knowledgeFormData.documentName!.split(',').map((pattern) => pattern.trim()) + } + }; + + const yamlString = dumpYaml(knowledgeYamlData); + + const attributionData: AttributionData = { + title_of_work: knowledgeFormData.titleWork!, + link_to_work: knowledgeFormData.linkWork!, + revision: knowledgeFormData.revision!, + license_of_the_work: knowledgeFormData.licenseWork!, + creator_names: knowledgeFormData.creators! + }; + + const waitForSubmissionAlert: ActionGroupAlertContent = { + title: 'Knowledge contribution submission in progress.!', + message: `Once the submission is successful, it will provide the link to the newly created Pull Request.`, + success: true, + waitAlert: true, + timeout: false + }; + setActionGroupAlertContent(waitForSubmissionAlert); + + const name = knowledgeFormData.name; + const email = knowledgeFormData.email; + const submissionSummary = knowledgeFormData.submissionSummary; + const documentOutline = knowledgeFormData.documentOutline; + const response = await fetch('/api/local/pr/knowledge', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + content: yamlString, + attribution: attributionData, + name, + email, + submissionSummary, + documentOutline, + filePath: sanitizedFilePath + }) + }); + + if (!response.ok) { + const actionGroupAlertContent: ActionGroupAlertContent = { + title: `Failed data submission`, + message: response.statusText, + success: false + }; + setActionGroupAlertContent(actionGroupAlertContent); + return; + } + + const result = await response.json(); + const actionGroupAlertContent: ActionGroupAlertContent = { + title: 'Knowledge contribution submitted successfully!', + message: `Thank you for your contribution!`, + url: `${result.html_url}`, + success: true + }; + setActionGroupAlertContent(actionGroupAlertContent); + resetForm(); + }; + return ( + + ); +}; + +export default Submit; diff --git a/src/components/Experimental/ContributeLocal/Knowledge/index.tsx b/src/components/Experimental/ContributeLocal/Knowledge/index.tsx new file mode 100644 index 00000000..fcf32c80 --- /dev/null +++ b/src/components/Experimental/ContributeLocal/Knowledge/index.tsx @@ -0,0 +1,611 @@ +// src/components/Experimental/ContributeLocal/Knowledge/index.tsx +'use client'; +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 { useSession } from 'next-auth/react'; +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 AttributionInformation from '@/components/Contribute/Knowledge/AttributionInformation/AttributionInformation'; +import Submit from './SubmitLocal/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 { TextContent } from '@patternfly/react-core/dist/dynamic/components/Text'; +import { Title } from '@patternfly/react-core/dist/dynamic/components/Title'; +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 '@/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 { 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'; + +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; +} + +export interface ActionGroupAlertContent { + title: string; + message: string; + waitAlert?: boolean; + url?: string; + success: boolean; + timeout?: number | boolean; +} + +export interface KnowledgeFormProps { + knowledgeEditFormData?: KnowledgeEditFormData; +} + +export const KnowledgeFormLocal: React.FunctionComponent = ({ knowledgeEditFormData }) => { + const [deploymentType, setDeploymentType] = useState(); + + const { data: session } = useSession(); + const [githubUsername, setGithubUsername] = useState(''); + // Author Information + const [email, setEmail] = useState(''); + const [name, setName] = useState(''); + + // Knowledge Information + const [submissionSummary, setSubmissionSummary] = useState(''); + const [domain, setDomain] = useState(''); + const [documentOutline, setDocumentOutline] = useState(''); + + // File Path Information + const [filePath, setFilePath] = useState(''); + + const [knowledgeDocumentRepositoryUrl, setKnowledgeDocumentRepositoryUrl] = useState(''); + const [knowledgeDocumentCommit, setKnowledgeDocumentCommit] = useState(''); + // This used to be 'patterns' but I am not totally sure what this variable actually is... + const [documentName, setDocumentName] = useState(''); + + // Attribution Information + // State + const [titleWork, setTitleWork] = useState(''); + const [linkWork, setLinkWork] = useState(''); + const [revision, setRevision] = useState(''); + const [licenseWork, setLicenseWork] = useState(''); + const [creators, setCreators] = useState(''); + + const [actionGroupAlertContent, setActionGroupAlertContent] = useState(); + + const [disableAction, setDisableAction] = useState(true); + const [reset, setReset] = useState(false); + + const router = useRouter(); + + const emptySeedExample: SeedExample = { + immutable: true, + isExpanded: false, + context: '', + isContextValid: ValidatedOptions.default, + questionAndAnswers: [ + { + immutable: true, + question: '', + isQuestionValid: ValidatedOptions.default, + answer: '', + isAnswerValid: ValidatedOptions.default + }, + { + immutable: true, + question: '', + isQuestionValid: ValidatedOptions.default, + answer: '', + isAnswerValid: ValidatedOptions.default + }, + { + immutable: true, + question: '', + isQuestionValid: ValidatedOptions.default, + answer: '', + isAnswerValid: ValidatedOptions.default + } + ] + }; + + const [seedExamples, setSeedExamples] = useState([ + emptySeedExample, + emptySeedExample, + emptySeedExample, + emptySeedExample, + emptySeedExample + ]); + + useEffect(() => { + const getEnvVariables = async () => { + const res = await fetch('/api/envConfig'); + const envConfig = await res.json(); + setDeploymentType(envConfig.DEPLOYMENT_TYPE); + }; + getEnvVariables(); + }, []); + + useEffect(() => { + if (session?.user?.name && session?.user?.email) { + setName(session?.user?.name); + setEmail(session?.user?.email); + } + }, [session?.user]); + + useMemo(() => { + const fetchUsername = async () => { + if (session?.accessToken) { + try { + const headers = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${session.accessToken}`, + Accept: 'application/vnd.github+json', + 'X-GitHub-Api-Version': '2022-11-28' + }; + + const fetchedUsername = await getGitHubUsername(headers); + setGithubUsername(fetchedUsername); + } catch (error) { + console.error('Failed to fetch GitHub username:', error); + } + } + }; + + fetchUsername(); + }, [session?.accessToken]); + + useEffect(() => { + // Set all elements from the knowledgeFormData to the state + if (knowledgeEditFormData) { + setEmail(knowledgeEditFormData.knowledgeFormData.email); + setName(knowledgeEditFormData.knowledgeFormData.name); + setSubmissionSummary(knowledgeEditFormData.knowledgeFormData.submissionSummary); + setDomain(knowledgeEditFormData.knowledgeFormData.domain); + setDocumentOutline(knowledgeEditFormData.knowledgeFormData.documentOutline); + setFilePath(knowledgeEditFormData.knowledgeFormData.filePath); + setKnowledgeDocumentRepositoryUrl(knowledgeEditFormData.knowledgeFormData.knowledgeDocumentRepositoryUrl); + setKnowledgeDocumentCommit(knowledgeEditFormData.knowledgeFormData.knowledgeDocumentCommit); + setDocumentName(knowledgeEditFormData.knowledgeFormData.documentName); + setTitleWork(knowledgeEditFormData.knowledgeFormData.titleWork); + setLinkWork(knowledgeEditFormData.knowledgeFormData.linkWork); + setRevision(knowledgeEditFormData.knowledgeFormData.revision); + setLicenseWork(knowledgeEditFormData.knowledgeFormData.licenseWork); + setCreators(knowledgeEditFormData.knowledgeFormData.creators); + setSeedExamples(knowledgeEditFormData.knowledgeFormData.seedExamples); + } + }, [knowledgeEditFormData]); + + const validateContext = (context: string) => { + // Split the context into words based on spaces + const contextStr = context.trim(); + if (contextStr.length == 0) { + setDisableAction(true); + return { msg: 'Context is required', status: ValidatedOptions.error }; + } + const tokens = contextStr.split(/\s+/); + if (tokens.length > 0 && tokens.length <= 500) { + setDisableAction(!checkKnowledgeFormCompletion(knowledgeFormData)); + return { msg: 'Valid Input', status: ValidatedOptions.success }; + } + setDisableAction(true); + const errorMsg = 'Context must be less than 500 words. Current word count: ' + tokens.length; + return { msg: errorMsg, status: ValidatedOptions.error }; + }; + + const validateQuestion = (question: string) => { + const questionStr = question.trim(); + if (questionStr.length == 0) { + setDisableAction(true); + return { msg: 'Question is required', status: ValidatedOptions.error }; + } + const tokens = questionStr.split(/\s+/); + if (tokens.length > 0 && tokens.length < 250) { + setDisableAction(!checkKnowledgeFormCompletion(knowledgeFormData)); + return { msg: 'Valid input', status: ValidatedOptions.success }; + } + setDisableAction(true); + return { msg: 'Question must be less than 250 words. Current word count: ' + tokens.length, status: ValidatedOptions.error }; + }; + + const validateAnswer = (answer: string) => { + const answerStr = answer.trim(); + if (answerStr.length == 0) { + setDisableAction(true); + return { msg: 'Answer is required', status: ValidatedOptions.error }; + } + const tokens = answerStr.split(/\s+/); + if (tokens.length > 0 && tokens.length < 250) { + setDisableAction(!checkKnowledgeFormCompletion(knowledgeFormData)); + return { msg: 'Valid input', status: ValidatedOptions.success }; + } + setDisableAction(true); + return { msg: 'Answer must be less than 250 words. Current word count: ' + tokens.length, status: ValidatedOptions.error }; + }; + + const handleContextInputChange = (seedExampleIndex: number, contextValue: string): void => { + setSeedExamples( + seedExamples.map((seedExample: SeedExample, index: number) => + index === seedExampleIndex + ? { + ...seedExample, + context: contextValue + } + : seedExample + ) + ); + }; + + const handleContextBlur = (seedExampleIndex: number): void => { + const updatedSeedExamples = seedExamples.map((seedExample: SeedExample, index: number): SeedExample => { + if (index === seedExampleIndex) { + const { msg, status } = validateContext(seedExample.context); + return { + ...seedExample, + isContextValid: status, + validationError: msg + }; + } + return seedExample; + }); + setSeedExamples(updatedSeedExamples); + }; + + const handleQuestionInputChange = (seedExampleIndex: number, questionAndAnswerIndex: number, questionValue: string): void => { + setSeedExamples( + seedExamples.map((seedExample: SeedExample, index: number) => + index === seedExampleIndex + ? { + ...seedExample, + questionAndAnswers: seedExample.questionAndAnswers.map((questionAndAnswerPair: QuestionAndAnswerPair, index: number) => + index === questionAndAnswerIndex + ? { + ...questionAndAnswerPair, + question: questionValue + } + : questionAndAnswerPair + ) + } + : seedExample + ) + ); + }; + + const handleQuestionBlur = (seedExampleIndex: number, questionAndAnswerIndex: number): void => { + setSeedExamples( + seedExamples.map((seedExample: SeedExample, index: number) => + index === seedExampleIndex + ? { + ...seedExample, + questionAndAnswers: seedExample.questionAndAnswers.map((questionAndAnswerPair: QuestionAndAnswerPair, index: number) => { + if (index === questionAndAnswerIndex) { + const { msg, status } = validateQuestion(questionAndAnswerPair.question); + return { + ...questionAndAnswerPair, + isQuestionValid: status, + questionValidationError: msg + }; + } + return questionAndAnswerPair; + }) + } + : seedExample + ) + ); + }; + + const handleAnswerInputChange = (seedExampleIndex: number, questionAndAnswerIndex: number, answerValue: string): void => { + setSeedExamples( + seedExamples.map((seedExample: SeedExample, index: number) => + index === seedExampleIndex + ? { + ...seedExample, + questionAndAnswers: seedExample.questionAndAnswers.map((questionAndAnswerPair: QuestionAndAnswerPair, index: number) => + index === questionAndAnswerIndex + ? { + ...questionAndAnswerPair, + answer: answerValue + } + : questionAndAnswerPair + ) + } + : seedExample + ) + ); + }; + + const handleAnswerBlur = (seedExampleIndex: number, questionAndAnswerIndex: number): void => { + setSeedExamples( + seedExamples.map((seedExample: SeedExample, index: number) => + index === seedExampleIndex + ? { + ...seedExample, + questionAndAnswers: seedExample.questionAndAnswers.map((questionAndAnswerPair: QuestionAndAnswerPair, index: number) => { + if (index === questionAndAnswerIndex) { + const { msg, status } = validateAnswer(questionAndAnswerPair.answer); + return { + ...questionAndAnswerPair, + isAnswerValid: status, + answerValidationError: msg + }; + } + return questionAndAnswerPair; + }) + } + : seedExample + ) + ); + }; + + const onCloseActionGroupAlert = () => { + setActionGroupAlertContent(undefined); + }; + + const resetForm = (): void => { + setEmail(''); + setName(''); + setDocumentOutline(''); + setSubmissionSummary(''); + setDomain(''); + setKnowledgeDocumentRepositoryUrl(''); + setKnowledgeDocumentCommit(''); + setDocumentName(''); + setTitleWork(''); + setLinkWork(''); + setLicenseWork(''); + setCreators(''); + setRevision(''); + setFilePath(''); + setSeedExamples([emptySeedExample, emptySeedExample, emptySeedExample, emptySeedExample, emptySeedExample]); + setDisableAction(true); + + // setReset is just reset button, value has no impact. + setReset(reset ? false : true); + }; + + const autoFillForm = (): void => { + setEmail(autoFillKnowledgeFields.email); + setName(autoFillKnowledgeFields.name); + setDocumentOutline(autoFillKnowledgeFields.documentOutline); + setSubmissionSummary(autoFillKnowledgeFields.submissionSummary); + setDomain(autoFillKnowledgeFields.domain); + setKnowledgeDocumentRepositoryUrl(autoFillKnowledgeFields.knowledgeDocumentRepositoryUrl); + setKnowledgeDocumentCommit(autoFillKnowledgeFields.knowledgeDocumentCommit); + setDocumentName(autoFillKnowledgeFields.documentName); + setTitleWork(autoFillKnowledgeFields.titleWork); + setLinkWork(autoFillKnowledgeFields.linkWork); + setLicenseWork(autoFillKnowledgeFields.licenseWork); + setCreators(autoFillKnowledgeFields.creators); + setRevision(autoFillKnowledgeFields.revision); + setSeedExamples(autoFillKnowledgeFields.seedExamples); + }; + + const knowledgeFormData: KnowledgeFormData = { + email: email, + name: name, + submissionSummary: submissionSummary, + domain: domain, + documentOutline: documentOutline, + filePath: filePath, + seedExamples: seedExamples, + knowledgeDocumentRepositoryUrl: knowledgeDocumentRepositoryUrl, + knowledgeDocumentCommit: knowledgeDocumentCommit, + documentName: documentName, + titleWork: titleWork, + linkWork: linkWork, + revision: revision, + licenseWork: licenseWork, + creators: creators + }; + + useEffect(() => { + setDisableAction(!checkKnowledgeFormCompletion(knowledgeFormData)); + }, [knowledgeFormData]); + + const handleCancel = () => { + router.push('/dashboard'); + }; + + return ( + + + + Dashboard + Knowledge Contribution + + + + + + Knowledge Contribution + + + + + {deploymentType === 'dev' && ( + + )} + +
+ + + + + + + + + + + + + {actionGroupAlertContent && ( + } + > +

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

+
+ )} + + + {knowledgeEditFormData?.isEditForm && ( + + )} + {!knowledgeEditFormData?.isEditForm && ( + + )} + + + + + +
+
+ ); +}; + +export default KnowledgeFormLocal; diff --git a/src/components/Experimental/ContributeLocal/Knowledge/knowledge.css b/src/components/Experimental/ContributeLocal/Knowledge/knowledge.css new file mode 100644 index 00000000..9f65e32c --- /dev/null +++ b/src/components/Experimental/ContributeLocal/Knowledge/knowledge.css @@ -0,0 +1,31 @@ +/* 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; +} diff --git a/src/components/Experimental/ContributeLocal/Skill/SubmitLocal/SubmitLocal.tsx b/src/components/Experimental/ContributeLocal/Skill/SubmitLocal/SubmitLocal.tsx new file mode 100644 index 00000000..f90bcca7 --- /dev/null +++ b/src/components/Experimental/ContributeLocal/Skill/SubmitLocal/SubmitLocal.tsx @@ -0,0 +1,106 @@ +import React from 'react'; +import { Button } from '@patternfly/react-core/dist/dynamic/components/Button'; +import { ActionGroupAlertContent, SkillFormData } from '..'; +import { AttributionData, SkillYamlData } from '@/types'; +import { SkillSchemaVersion } from '@/types/const'; +import { dumpYaml } from '@/utils/yamlConfig'; +import { validateFields } from '@/components/Contribute/Skill/validation'; + +interface Props { + disableAction: boolean; + skillFormData: SkillFormData; + setActionGroupAlertContent: React.Dispatch>; + githubUsername: string | undefined; + resetForm: () => void; +} + +// temporary location of these validation functions. Once the Skills form has been refactored then these can be moved out to the utils file. + +const Submit: React.FC = ({ disableAction, skillFormData, setActionGroupAlertContent, githubUsername, resetForm }) => { + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + if (!validateFields(skillFormData, setActionGroupAlertContent)) return; + + console.log('skillFormData :' + skillFormData); + // Strip leading slash and ensure trailing slash in the file path + let sanitizedFilePath = skillFormData.filePath!.startsWith('/') ? skillFormData.filePath!.slice(1) : skillFormData.filePath; + sanitizedFilePath = sanitizedFilePath!.endsWith('/') ? sanitizedFilePath : `${sanitizedFilePath}/`; + + const skillYamlData: SkillYamlData = { + created_by: githubUsername!, + version: SkillSchemaVersion, + task_description: skillFormData.documentOutline!, + seed_examples: skillFormData.seedExamples.map((example) => ({ + context: example.context, + question: example.question, + answer: example.answer + })) + }; + + const yamlString = dumpYaml(skillYamlData); + + const attributionData: AttributionData = { + title_of_work: skillFormData.titleWork!, + license_of_the_work: skillFormData.licenseWork!, + creator_names: skillFormData.creators!, + link_to_work: '', + revision: '' + }; + + const waitForSubmissionAlert: ActionGroupAlertContent = { + title: 'Skill contribution submission in progress.!', + message: `Once the submission is successful, it will provide the link to the newly created Pull Request.`, + success: true, + waitAlert: true, + timeout: false + }; + setActionGroupAlertContent(waitForSubmissionAlert); + + const name = skillFormData.name; + const email = skillFormData.email; + const submissionSummary = skillFormData.submissionSummary; + const documentOutline = skillFormData.documentOutline; + const response = await fetch('/api/local/pr/skill/', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + content: yamlString, + attribution: attributionData, + name, + email, + submissionSummary, + documentOutline, + filePath: sanitizedFilePath + }) + }); + + if (!response.ok) { + const actionGroupAlertContent: ActionGroupAlertContent = { + title: `Failed data submission`, + message: response.statusText, + success: false + }; + setActionGroupAlertContent(actionGroupAlertContent); + return; + } + + const result = await response.json(); + const actionGroupAlertContent: ActionGroupAlertContent = { + title: 'Skill contribution submitted successfully!', + message: `Thank you for your contribution!`, + url: `${result.html_url}`, + success: true + }; + setActionGroupAlertContent(actionGroupAlertContent); + resetForm(); + }; + return ( + + ); +}; + +export default Submit; diff --git a/src/components/Experimental/ContributeLocal/Skill/index.tsx b/src/components/Experimental/ContributeLocal/Skill/index.tsx new file mode 100644 index 00000000..476ccfd2 --- /dev/null +++ b/src/components/Experimental/ContributeLocal/Skill/index.tsx @@ -0,0 +1,484 @@ +// src/components/Experimental/ContributeLocal/Skill/index.tsx +'use client'; +import React, { useEffect, useMemo, useState } from 'react'; +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 { useSession } from 'next-auth/react'; +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 { 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 { TextContent } from '@patternfly/react-core/dist/dynamic/components/Text'; +import { Title } from '@patternfly/react-core/dist/dynamic/components/Title'; +import { checkSkillFormCompletion } from '@/components/Contribute/Skill/validation'; +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 { Button } from '@patternfly/react-core/dist/esm/components/Button/Button'; +import { useRouter } from 'next/navigation'; +import SkillsSeedExample from '@/components/Contribute/Skill/SkillsSeedExample/SkillsSeedExample'; +import SkillsInformation from '@/components/Contribute/Skill/SkillsInformation/SkillsInformation'; +import SkillsDescriptionContent from '@/components/Contribute/Skill/SkillsDescription/SkillsDescriptionContent'; +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; + pullRequestNumber: number; + branchName: string; + yamlFile: PullRequestFile; + attributionFile: PullRequestFile; + skillFormData: SkillFormData; +} + +export interface ActionGroupAlertContent { + title: string; + message: string; + waitAlert?: boolean; + url?: string; + success: boolean; + timeout?: number | boolean; +} + +export interface SkillFormProps { + skillEditFormData?: SkillEditFormData; +} + +export const SkillFormLocal: React.FunctionComponent = ({ skillEditFormData }) => { + const [deploymentType, setDeploymentType] = useState(); + + const { data: session } = useSession(); + const [githubUsername, setGithubUsername] = useState(''); + // Author Information + const [email, setEmail] = useState(''); + const [name, setName] = useState(''); + + // Skills Information + const [submissionSummary, setSubmissionSummary] = useState(''); + const [documentOutline, setDocumentOutline] = useState(''); + + // File Path Information + const [filePath, setFilePath] = useState(''); + + // Attribution Information + // State + const [titleWork, setTitleWork] = useState(''); + const [licenseWork, setLicenseWork] = useState(''); + const [creators, setCreators] = useState(''); + + const [actionGroupAlertContent, setActionGroupAlertContent] = useState(); + + const [disableAction, setDisableAction] = useState(true); + const [reset, setReset] = useState(false); + + const router = useRouter(); + + const emptySeedExample: SeedExample = { + immutable: true, + isExpanded: false, + context: '', + isContextValid: ValidatedOptions.default, + question: '', + isQuestionValid: ValidatedOptions.default, + answer: '', + isAnswerValid: ValidatedOptions.default + }; + + const [seedExamples, setSeedExamples] = useState([ + emptySeedExample, + emptySeedExample, + emptySeedExample, + emptySeedExample, + emptySeedExample + ]); + + useEffect(() => { + const getEnvVariables = async () => { + const res = await fetch('/api/envConfig'); + const envConfig = await res.json(); + setDeploymentType(envConfig.DEPLOYMENT_TYPE); + }; + getEnvVariables(); + }, []); + + useEffect(() => { + if (session?.user?.name && session?.user?.email) { + setName(session?.user?.name); + setEmail(session?.user?.email); + } + }, [session?.user]); + + useMemo(() => { + const fetchUsername = async () => { + if (session?.accessToken) { + try { + const header = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${session.accessToken}`, + Accept: 'application/vnd.github+json', + 'X-GitHub-Api-Version': '2022-11-28' + }; + const fetchedUsername = await getGitHubUsername(header); + setGithubUsername(fetchedUsername); + } catch (error) { + console.error('Failed to fetch GitHub username:', error); + } + } + }; + + fetchUsername(); + }, [session?.accessToken]); + + useEffect(() => { + // Set all elements from the skillFormData to the state + if (skillEditFormData) { + setEmail(skillEditFormData.skillFormData.email); + setName(skillEditFormData.skillFormData.name); + setSubmissionSummary(skillEditFormData.skillFormData.submissionSummary); + setDocumentOutline(skillEditFormData.skillFormData.documentOutline); + setFilePath(skillEditFormData.skillFormData.filePath); + setTitleWork(skillEditFormData.skillFormData.titleWork); + setLicenseWork(skillEditFormData.skillFormData.licenseWork); + setCreators(skillEditFormData.skillFormData.creators); + setSeedExamples(skillEditFormData.skillFormData.seedExamples); + } + }, [skillEditFormData]); + + const validateContext = (context: string): ValidatedOptions => { + // Context is optional + console.log('context', context); + return ValidatedOptions.success; + }; + + const validateQuestion = (question: string): ValidatedOptions => { + if (question.length > 0 && question.length < 250) { + setDisableAction(!checkSkillFormCompletion(skillFormData)); + return ValidatedOptions.success; + } + setDisableAction(true); + return ValidatedOptions.error; + }; + + const validateAnswer = (answer: string): ValidatedOptions => { + if (answer.length > 0 && answer.length < 250) { + setDisableAction(!checkSkillFormCompletion(skillFormData)); + return ValidatedOptions.success; + } + setDisableAction(true); + return ValidatedOptions.error; + }; + + const handleContextInputChange = (seedExampleIndex: number, contextValue: string): void => { + setSeedExamples( + seedExamples.map((seedExample: SeedExample, index: number) => + index === seedExampleIndex + ? { + ...seedExample, + context: contextValue + } + : seedExample + ) + ); + }; + + const handleContextBlur = (seedExampleIndex: number): void => { + setSeedExamples( + seedExamples.map((seedExample: SeedExample, index: number) => + index === seedExampleIndex + ? { + ...seedExample, + isContextValid: validateContext(seedExample.context ? seedExample.context : '') + } + : seedExample + ) + ); + }; + + const handleAnswerInputChange = (seedExampleIndex: number, answerValue: string): void => { + setSeedExamples( + seedExamples.map((seedExample: SeedExample, index: number) => + index === seedExampleIndex + ? { + ...seedExample, + answer: answerValue + } + : seedExample + ) + ); + }; + + const handleAnswerBlur = (seedExampleIndex: number): void => { + setSeedExamples( + seedExamples.map((seedExample: SeedExample, index: number) => + index === seedExampleIndex + ? { + ...seedExample, + isAnswerValid: validateAnswer(seedExample.answer) + } + : seedExample + ) + ); + }; + + const handleQuestionInputChange = (seedExampleIndex: number, questionValue: string): void => { + setSeedExamples( + seedExamples.map((seedExample: SeedExample, index: number) => + index === seedExampleIndex + ? { + ...seedExample, + question: questionValue + } + : seedExample + ) + ); + }; + + const handleQuestionBlur = (seedExampleIndex: number): void => { + setSeedExamples( + seedExamples.map((seedExample: SeedExample, index: number) => + index === seedExampleIndex + ? { + ...seedExample, + isQuestionValid: validateQuestion(seedExample.question) + } + : seedExample + ) + ); + }; + + const addSeedExample = (): void => { + const seedExample = emptySeedExample; + seedExample.immutable = false; + seedExample.isExpanded = true; + setSeedExamples([...seedExamples, seedExample]); + setDisableAction(true); + }; + + const deleteSeedExample = (seedExampleIndex: number): void => { + setSeedExamples(seedExamples.filter((_, index: number) => index !== seedExampleIndex)); + setDisableAction(!checkSkillFormCompletion(skillFormData)); + }; + + const onCloseActionGroupAlert = () => { + setActionGroupAlertContent(undefined); + }; + + const resetForm = (): void => { + setEmail(''); + setName(''); + setDocumentOutline(''); + setSubmissionSummary(''); + setTitleWork(''); + setLicenseWork(''); + setCreators(''); + setFilePath(''); + setSeedExamples([emptySeedExample, emptySeedExample, emptySeedExample, emptySeedExample, emptySeedExample]); + setDisableAction(true); + + // setReset is just reset button, value has no impact. + setReset(reset ? false : true); + }; + + const autoFillForm = (): void => { + setEmail(autoFillSkillsFields.email); + setName(autoFillSkillsFields.name); + setDocumentOutline(autoFillSkillsFields.documentOutline); + setSubmissionSummary(autoFillSkillsFields.submissionSummary); + setTitleWork(autoFillSkillsFields.titleWork); + setLicenseWork(autoFillSkillsFields.licenseWork); + setCreators(autoFillSkillsFields.creators); + setFilePath(autoFillSkillsFields.filePath); + setSeedExamples(autoFillSkillsFields.seedExamples); + }; + + const skillFormData: SkillFormData = { + email: email, + name: name, + submissionSummary: submissionSummary, + documentOutline: documentOutline, + filePath: filePath, + seedExamples: seedExamples, + titleWork: titleWork, + licenseWork: licenseWork, + creators: creators + }; + + useEffect(() => { + setDisableAction(!checkSkillFormCompletion(skillFormData)); + }, [skillFormData]); + + const handleCancel = () => { + router.push('/dashboard'); + }; + + return ( + + + + Dashboard + Skill Contribution + + + + + + Skill Contribution + + + + + {deploymentType === 'dev' && ( + + )} +
+ + + + + + + + + + + {actionGroupAlertContent && ( + } + > +

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

+
+ )} + + + {skillEditFormData?.isEditForm && ( + + )} + {!skillEditFormData?.isEditForm && ( + + )} + + + + + +
+
+ ); +}; + +export default SkillFormLocal; diff --git a/src/components/Experimental/ContributeLocal/Skill/skills.css b/src/components/Experimental/ContributeLocal/Skill/skills.css new file mode 100644 index 00000000..877e5d7a --- /dev/null +++ b/src/components/Experimental/ContributeLocal/Skill/skills.css @@ -0,0 +1,18 @@ +/* Skill CSS */ + +.form-s { + width: 80%; + margin-bottom: 50px; + background-color: white; +} + +.submit:hover, +.download-yaml:hover, +.download-attribution:hover { + background-color: #45a049; +} + +.heading { + text-align: left; + font-size: medium; +} diff --git a/src/components/Experimental/DashboardLocal/index.tsx b/src/components/Experimental/DashboardLocal/index.tsx new file mode 100644 index 00000000..18808c51 --- /dev/null +++ b/src/components/Experimental/DashboardLocal/index.tsx @@ -0,0 +1,161 @@ +// src/components/Experimental/DashboardLocal/index.tsx +import * as React from 'react'; +import { Card, CardTitle, 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/components/Modal'; + +const DashboardLocal: React.FunctionComponent = () => { + const [branches, setBranches] = React.useState([]); + const [isLoading, setIsLoading] = React.useState(true); + const [mergeStatus, setMergeStatus] = 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); + + // 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) { + setBranches(result.branches); + } else { + console.error('Failed to fetch branches:', result.error); + } + } catch (error) { + console.error('Error fetching branches:', error); + } finally { + setIsLoading(false); + } + }; + + fetchBranches(); + }, []); + + 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 + + + + + Git Repository Branches + +

Listing all branches from the local repository:

+
+ + + {isLoading ? ( + + ) : branches.length === 0 ? ( +

No branches found in the repository.

+ ) : ( + + {branches.map((branch) => ( + + + + + + Branch Name: {branch} + + + {branch !== 'main' && ( + <> + + + + )} + + + + + + ))} + + )} + + {mergeStatus && ( + +

{mergeStatus.message}

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

No differences found.

+ )} +
+
+
+ ); +}; + +export { DashboardLocal }; From de48901ae3568a0b0d4d8eccd3916f4e56305d1b Mon Sep 17 00:00:00 2001 From: Brent Salisbury Date: Sun, 3 Nov 2024 23:48:13 -0500 Subject: [PATCH 2/3] Add creation date and an empty state to the local repo dashboard Signed-off-by: Brent Salisbury --- package-lock.json | 112 ++++++++++++++++-- package.json | 1 - src/app/api/local/git/branches/route.ts | 16 ++- .../Experimental/DashboardLocal/index.tsx | 98 ++++++++++++--- 4 files changed, 199 insertions(+), 28 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3373f501..61002013 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,7 +33,6 @@ "@next/eslint-plugin-next": "^14.2.3", "@playwright/test": "^1.47.2", "@redhat-cloud-services/eslint-config-redhat-cloud-services": "^2.0.4", - "@types/js-cookie": "^3.0.6", "@types/js-yaml": "^4.0.9", "@types/node": "^22.5.0", "@types/react": "18.3.1", @@ -729,6 +728,111 @@ "node": ">= 10" } }, + "node_modules/@next/swc-darwin-x64": { + "version": "15.0.2", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.0.2.tgz", + "integrity": "sha512-KUpBVxIbjzFiUZhiLIpJiBoelqzQtVZbdNNsehhUn36e2YzKHphnK8eTUW1s/4aPy5kH/UTid8IuVbaOpedhpw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "15.0.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.0.2.tgz", + "integrity": "sha512-9J7TPEcHNAZvwxXRzOtiUvwtTD+fmuY0l7RErf8Yyc7kMpE47MIQakl+3jecmkhOoIyi/Rp+ddq7j4wG6JDskQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "15.0.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.0.2.tgz", + "integrity": "sha512-BjH4ZSzJIoTTZRh6rG+a/Ry4SW0HlizcPorqNBixBWc3wtQtj4Sn9FnRZe22QqrPnzoaW0ctvSz4FaH4eGKMww==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "15.0.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.0.2.tgz", + "integrity": "sha512-i3U2TcHgo26sIhcwX/Rshz6avM6nizrZPvrDVDY1bXcLH1ndjbO8zuC7RoHp0NSK7wjJMPYzm7NYL1ksSKFreA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "15.0.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.0.2.tgz", + "integrity": "sha512-AMfZfSVOIR8fa+TXlAooByEF4OB00wqnms1sJ1v+iu8ivwvtPvnkwdzzFMpsK5jA2S9oNeeQ04egIWVb4QWmtQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "15.0.2", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.0.2.tgz", + "integrity": "sha512-JkXysDT0/hEY47O+Hvs8PbZAeiCQVxKfGtr4GUpNAhlG2E0Mkjibuo8ryGD29Qb5a3IOnKYNoZlh/MyKd2Nbww==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "15.0.2", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.0.2.tgz", + "integrity": "sha512-foaUL0NqJY/dX0Pi/UcZm5zsmSk5MtP/gxx3xOPyREkMFN+CTjctPfu3QaqrQHinaKdPnMWPJDKt4VjDfTBe/Q==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { "version": "5.1.1-v1", "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", @@ -1002,12 +1106,6 @@ "hoist-non-react-statics": "^3.3.0" } }, - "node_modules/@types/js-cookie": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-3.0.6.tgz", - "integrity": "sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==", - "dev": true - }, "node_modules/@types/js-yaml": { "version": "4.0.9", "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", diff --git a/package.json b/package.json index c73857dc..b17ba967 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,6 @@ "@next/eslint-plugin-next": "^14.2.3", "@playwright/test": "^1.47.2", "@redhat-cloud-services/eslint-config-redhat-cloud-services": "^2.0.4", - "@types/js-cookie": "^3.0.6", "@types/js-yaml": "^4.0.9", "@types/node": "^22.5.0", "@types/react": "18.3.1", diff --git a/src/app/api/local/git/branches/route.ts b/src/app/api/local/git/branches/route.ts index 9d880342..5175eec3 100644 --- a/src/app/api/local/git/branches/route.ts +++ b/src/app/api/local/git/branches/route.ts @@ -16,9 +16,21 @@ export async function GET() { // List all branches in the repository const branches = await git.listBranches({ fs, dir: REPO_DIR }); + const branchDetails = []; - // Return the list of branches as JSON - return NextResponse.json({ branches }, { status: 200 }); + 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 }); diff --git a/src/components/Experimental/DashboardLocal/index.tsx b/src/components/Experimental/DashboardLocal/index.tsx index 18808c51..1d71c404 100644 --- a/src/components/Experimental/DashboardLocal/index.tsx +++ b/src/components/Experimental/DashboardLocal/index.tsx @@ -1,6 +1,6 @@ // src/components/Experimental/DashboardLocal/index.tsx import * as React from 'react'; -import { Card, CardTitle, CardBody } from '@patternfly/react-core/dist/dynamic/components/Card'; +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'; @@ -10,13 +10,24 @@ 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/components/Modal'; +import { + EmptyState, + EmptyStateBody, + EmptyStateHeader, + 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 DashboardLocal: React.FunctionComponent = () => { - const [branches, setBranches] = React.useState([]); + const [branches, setBranches] = React.useState<{ name: string; creationDate: number }[]>([]); const [isLoading, setIsLoading] = React.useState(true); const [mergeStatus, setMergeStatus] = 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(() => { @@ -25,7 +36,9 @@ const DashboardLocal: React.FunctionComponent = () => { const response = await fetch('/api/local/git/branches'); const result = await response.json(); if (response.ok) { - setBranches(result.branches); + // 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); } @@ -39,6 +52,11 @@ const DashboardLocal: React.FunctionComponent = () => { fetchBranches(); }, []); + const formatDateTime = (timestamp: number) => { + const date = new Date(timestamp); + return `${date.toLocaleDateString()} ${date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`; + }; + const handleMerge = async (branchName: string) => { setMergeStatus(null); // Clear previous status try { @@ -89,37 +107,81 @@ const DashboardLocal: React.FunctionComponent = () => { - Git Repository Branches + Local Git Repository Branches -

Listing all branches from the local repository:

{isLoading ? ( ) : branches.length === 0 ? ( -

No branches found in the repository.

+ + + InstructLab Logo + + } + /> + +
+ 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} + Branch Name: {branch.name} +
+ Created on: {formatDateTime(branch.creationDate)}
- {branch !== 'main' && ( - <> - - - - )} + +
From 81f44fd0fbc57c2982fdc1e6cb425d3301ed2574 Mon Sep 17 00:00:00 2001 From: Brent Salisbury Date: Wed, 6 Nov 2024 20:33:51 -0500 Subject: [PATCH 3/3] Use the user's email for the created_by field for local submissions - Also redirect the submission success popup to the local dashboard Signed-off-by: Brent Salisbury --- package-lock.json | 324 +++++++++++++++++- package.json | 1 + .../Knowledge/SubmitLocal/Submit.tsx | 13 +- .../ContributeLocal/Knowledge/index.tsx | 4 +- .../Skill/SubmitLocal/SubmitLocal.tsx | 15 +- .../ContributeLocal/Skill/index.tsx | 4 +- 6 files changed, 334 insertions(+), 27 deletions(-) diff --git a/package-lock.json b/package-lock.json index 61002013..ea69db6e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "node-fetch": "^3.3.2", "react": "18.3.1", "react-dom": "18.3.1", + "sharp": "^0.33.5", "uuid": "^11.0.2", "winston": "^3.16.0" }, @@ -343,6 +344,15 @@ "kuler": "^2.0.0" } }, + "node_modules/@emnapi/runtime": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.3.1.tgz", + "integrity": "sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -583,6 +593,27 @@ "@img/sharp-libvips-darwin-arm64": "1.0.4" } }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", + "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.0.4" + } + }, "node_modules/@img/sharp-libvips-darwin-arm64": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", @@ -598,6 +629,291 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", + "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", + "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", + "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", + "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", + "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", + "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", + "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", + "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.0.5" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", + "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", + "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", + "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", + "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", + "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", + "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", + "cpu": [ + "wasm32" + ], + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.2.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", + "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", + "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -2166,7 +2482,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", - "optional": true, "engines": { "node": ">=8" } @@ -5885,7 +6200,6 @@ "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", "hasInstallScript": true, - "optional": true, "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", @@ -5923,7 +6237,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", - "optional": true, "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" @@ -5936,7 +6249,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "optional": true, "dependencies": { "color-name": "~1.1.4" }, @@ -5947,14 +6259,12 @@ "node_modules/sharp/node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "optional": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "node_modules/sharp/node_modules/semver": { "version": "7.6.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "optional": true, "bin": { "semver": "bin/semver.js" }, diff --git a/package.json b/package.json index b17ba967..b9dfe2f6 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "node-fetch": "^3.3.2", "react": "18.3.1", "react-dom": "18.3.1", + "sharp": "^0.33.5", "uuid": "^11.0.2", "winston": "^3.16.0" }, diff --git a/src/components/Experimental/ContributeLocal/Knowledge/SubmitLocal/Submit.tsx b/src/components/Experimental/ContributeLocal/Knowledge/SubmitLocal/Submit.tsx index 93ca97df..442e9ad4 100644 --- a/src/components/Experimental/ContributeLocal/Knowledge/SubmitLocal/Submit.tsx +++ b/src/components/Experimental/ContributeLocal/Knowledge/SubmitLocal/Submit.tsx @@ -11,11 +11,11 @@ interface Props { disableAction: boolean; knowledgeFormData: KnowledgeFormData; setActionGroupAlertContent: React.Dispatch>; - githubUsername: string | undefined; + email: string; resetForm: () => void; } -const Submit: React.FC = ({ disableAction, knowledgeFormData, setActionGroupAlertContent, githubUsername, resetForm }) => { +const Submit: React.FC = ({ disableAction, knowledgeFormData, setActionGroupAlertContent, email, resetForm }) => { const handleSubmit = async (event: React.FormEvent) => { event.preventDefault(); if (!validateFields(knowledgeFormData, setActionGroupAlertContent)) return; @@ -25,7 +25,7 @@ const Submit: React.FC = ({ disableAction, knowledgeFormData, setActionGr sanitizedFilePath = sanitizedFilePath!.endsWith('/') ? sanitizedFilePath : `${sanitizedFilePath}/`; const knowledgeYamlData: KnowledgeYamlData = { - created_by: githubUsername!, + created_by: email, version: KnowledgeSchemaVersion, domain: knowledgeFormData.domain!, document_outline: knowledgeFormData.documentOutline!, @@ -54,7 +54,7 @@ const Submit: React.FC = ({ disableAction, knowledgeFormData, setActionGr }; const waitForSubmissionAlert: ActionGroupAlertContent = { - title: 'Knowledge contribution submission in progress.!', + title: 'Knowledge contribution submission in progress!', message: `Once the submission is successful, it will provide the link to the newly created Pull Request.`, success: true, waitAlert: true, @@ -63,7 +63,6 @@ const Submit: React.FC = ({ disableAction, knowledgeFormData, setActionGr setActionGroupAlertContent(waitForSubmissionAlert); const name = knowledgeFormData.name; - const email = knowledgeFormData.email; const submissionSummary = knowledgeFormData.submissionSummary; const documentOutline = knowledgeFormData.documentOutline; const response = await fetch('/api/local/pr/knowledge', { @@ -92,11 +91,11 @@ const Submit: React.FC = ({ disableAction, knowledgeFormData, setActionGr return; } - const result = await response.json(); + await response.json(); const actionGroupAlertContent: ActionGroupAlertContent = { title: 'Knowledge contribution submitted successfully!', message: `Thank you for your contribution!`, - url: `${result.html_url}`, + url: '/experimental/dashboard-local/', success: true }; setActionGroupAlertContent(actionGroupAlertContent); diff --git a/src/components/Experimental/ContributeLocal/Knowledge/index.tsx b/src/components/Experimental/ContributeLocal/Knowledge/index.tsx index fcf32c80..fac134f1 100644 --- a/src/components/Experimental/ContributeLocal/Knowledge/index.tsx +++ b/src/components/Experimental/ContributeLocal/Knowledge/index.tsx @@ -568,7 +568,7 @@ export const KnowledgeFormLocal: React.FunctionComponent = ( actionGroupAlertContent.url && actionGroupAlertContent.url.trim().length > 0 && ( - View your pull request + View your new branch )}

@@ -592,7 +592,7 @@ export const KnowledgeFormLocal: React.FunctionComponent = ( disableAction={disableAction} knowledgeFormData={knowledgeFormData} setActionGroupAlertContent={setActionGroupAlertContent} - githubUsername={githubUsername} + email={email} resetForm={resetForm} /> )} diff --git a/src/components/Experimental/ContributeLocal/Skill/SubmitLocal/SubmitLocal.tsx b/src/components/Experimental/ContributeLocal/Skill/SubmitLocal/SubmitLocal.tsx index f90bcca7..7cf9967a 100644 --- a/src/components/Experimental/ContributeLocal/Skill/SubmitLocal/SubmitLocal.tsx +++ b/src/components/Experimental/ContributeLocal/Skill/SubmitLocal/SubmitLocal.tsx @@ -10,13 +10,11 @@ interface Props { disableAction: boolean; skillFormData: SkillFormData; setActionGroupAlertContent: React.Dispatch>; - githubUsername: string | undefined; + email: string; resetForm: () => void; } -// temporary location of these validation functions. Once the Skills form has been refactored then these can be moved out to the utils file. - -const Submit: React.FC = ({ disableAction, skillFormData, setActionGroupAlertContent, githubUsername, resetForm }) => { +const Submit: React.FC = ({ disableAction, skillFormData, setActionGroupAlertContent, email, resetForm }) => { const handleSubmit = async (event: React.FormEvent) => { event.preventDefault(); if (!validateFields(skillFormData, setActionGroupAlertContent)) return; @@ -27,7 +25,7 @@ const Submit: React.FC = ({ disableAction, skillFormData, setActionGroupA sanitizedFilePath = sanitizedFilePath!.endsWith('/') ? sanitizedFilePath : `${sanitizedFilePath}/`; const skillYamlData: SkillYamlData = { - created_by: githubUsername!, + created_by: email, version: SkillSchemaVersion, task_description: skillFormData.documentOutline!, seed_examples: skillFormData.seedExamples.map((example) => ({ @@ -48,7 +46,7 @@ const Submit: React.FC = ({ disableAction, skillFormData, setActionGroupA }; const waitForSubmissionAlert: ActionGroupAlertContent = { - title: 'Skill contribution submission in progress.!', + title: 'Skill contribution submission in progress!', message: `Once the submission is successful, it will provide the link to the newly created Pull Request.`, success: true, waitAlert: true, @@ -57,7 +55,6 @@ const Submit: React.FC = ({ disableAction, skillFormData, setActionGroupA setActionGroupAlertContent(waitForSubmissionAlert); const name = skillFormData.name; - const email = skillFormData.email; const submissionSummary = skillFormData.submissionSummary; const documentOutline = skillFormData.documentOutline; const response = await fetch('/api/local/pr/skill/', { @@ -86,11 +83,11 @@ const Submit: React.FC = ({ disableAction, skillFormData, setActionGroupA return; } - const result = await response.json(); + await response.json(); const actionGroupAlertContent: ActionGroupAlertContent = { title: 'Skill contribution submitted successfully!', message: `Thank you for your contribution!`, - url: `${result.html_url}`, + url: '/experimental/dashboard-local/', success: true }; setActionGroupAlertContent(actionGroupAlertContent); diff --git a/src/components/Experimental/ContributeLocal/Skill/index.tsx b/src/components/Experimental/ContributeLocal/Skill/index.tsx index 476ccfd2..32838497 100644 --- a/src/components/Experimental/ContributeLocal/Skill/index.tsx +++ b/src/components/Experimental/ContributeLocal/Skill/index.tsx @@ -441,7 +441,7 @@ export const SkillFormLocal: React.FunctionComponent = ({ skillE actionGroupAlertContent.url && actionGroupAlertContent.url.trim().length > 0 && ( - View your pull request + View your new branch )}

@@ -465,7 +465,7 @@ export const SkillFormLocal: React.FunctionComponent = ({ skillE disableAction={disableAction} skillFormData={skillFormData} setActionGroupAlertContent={setActionGroupAlertContent} - githubUsername={githubUsername} + email={email} resetForm={resetForm} /> )}