From 543767af0423fc53422280249e2119842d44faa2 Mon Sep 17 00:00:00 2001 From: Gang Li Date: Thu, 7 Dec 2023 21:28:54 +0800 Subject: [PATCH] RemoteEdit: start to use react-hook-form react-hook-form has good support for complex form, like data management, validation support. Here pick it as our default form implementation. --- src/main/webui/package-lock.json | 16 +++ src/main/webui/package.json | 1 + .../src/app/components/ComponentConstants.js | 9 +- .../content/common/PackageTypeSelect.jsx | 20 ++-- .../content/common/PackageTypeSelect.test.jsx | 38 +++--- .../content/common/StoreControlPanels.jsx | 17 ++- .../components/content/remote/RemoteEdit.jsx | 109 ++++++++++-------- .../webui/src/app/components/styles/indy.css | 4 + src/main/webui/src/app/utils/AppUtils.js | 10 ++ src/main/webui/src/app/utils/AppUtils.test.js | 47 ++++++++ 10 files changed, 193 insertions(+), 78 deletions(-) diff --git a/src/main/webui/package-lock.json b/src/main/webui/package-lock.json index 453ef5d..298a38f 100644 --- a/src/main/webui/package-lock.json +++ b/src/main/webui/package-lock.json @@ -14,6 +14,7 @@ "react": "^18.2.0", "react-bootstrap": "^2.9.1", "react-dom": "^18.2.0", + "react-hook-form": "^7.48.2", "react-json-pretty": "^1.7.9", "react-router": "^6.16.0", "react-router-bootstrap": "^0.26.2", @@ -12092,6 +12093,21 @@ "react": "^18.2.0" } }, + "node_modules/react-hook-form": { + "version": "7.48.2", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.48.2.tgz", + "integrity": "sha512-H0T2InFQb1hX7qKtDIZmvpU1Xfn/bdahWBN1fH19gSe4bBEqTfmlr7H3XWTaVtiK4/tpPaI1F3355GPMZYge+A==", + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", diff --git a/src/main/webui/package.json b/src/main/webui/package.json index 13abc23..957f62f 100644 --- a/src/main/webui/package.json +++ b/src/main/webui/package.json @@ -23,6 +23,7 @@ "react": "^18.2.0", "react-bootstrap": "^2.9.1", "react-dom": "^18.2.0", + "react-hook-form": "^7.48.2", "react-json-pretty": "^1.7.9", "react-router": "^6.16.0", "react-router-bootstrap": "^0.26.2", diff --git a/src/main/webui/src/app/components/ComponentConstants.js b/src/main/webui/src/app/components/ComponentConstants.js index 40e3bd9..7a37da9 100644 --- a/src/main/webui/src/app/components/ComponentConstants.js +++ b/src/main/webui/src/app/components/ComponentConstants.js @@ -28,5 +28,12 @@ const hostedOptionLegend = [ const STORE_API_BASE_URL = "/api/admin/stores"; +const PATTERNS={ + URL: /^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_+.~#?&/=]*)$/u +}; -export {remoteOptionLegend, hostedOptionLegend, STORE_API_BASE_URL}; + +export {remoteOptionLegend, + hostedOptionLegend, + STORE_API_BASE_URL, + PATTERNS}; diff --git a/src/main/webui/src/app/components/content/common/PackageTypeSelect.jsx b/src/main/webui/src/app/components/content/common/PackageTypeSelect.jsx index 90661b6..2d47ad0 100644 --- a/src/main/webui/src/app/components/content/common/PackageTypeSelect.jsx +++ b/src/main/webui/src/app/components/content/common/PackageTypeSelect.jsx @@ -19,10 +19,11 @@ import {PropTypes} from 'prop-types'; import {jsonRest} from '#utils/RestClient.js'; import {Utils} from '#utils/AppUtils'; -export const PackageTypeSelect = ({packageType,vauleChangeHandler}) =>{ +export const PackageTypeSelect = ({register, formErrors}) =>{ const [state, setState] = useState({ pkgTypes: [] }); + const [selected, setSelected] = useState(); useEffect(()=>{ const fetchPkgTypes = async () =>{ @@ -37,18 +38,23 @@ export const PackageTypeSelect = ({packageType,vauleChangeHandler}) =>{ fetchPkgTypes(); }, []); - const selectedValue = packageType || "maven"; - const onChangeHandler = vauleChangeHandler || (()=>{}); + let registered = {}; + if(register){ + registered = register("packageType", {required: true}); + } return - setSelected(e.target.value)} {...registered}> + { state.pkgTypes.map(type => ) } - + {' '} + {formErrors && formErrors.packageType?.type === "required" && Package Type is required} ; }; PackageTypeSelect.propTypes = { - packageType: PropTypes.string, - vauleChangeHandler: PropTypes.func + register: PropTypes.func, + formErrors: PropTypes.object }; diff --git a/src/main/webui/src/app/components/content/common/PackageTypeSelect.test.jsx b/src/main/webui/src/app/components/content/common/PackageTypeSelect.test.jsx index 24bbec1..01cbd3a 100644 --- a/src/main/webui/src/app/components/content/common/PackageTypeSelect.test.jsx +++ b/src/main/webui/src/app/components/content/common/PackageTypeSelect.test.jsx @@ -41,36 +41,36 @@ describe('PackageTypeSelect tests', () => { expect(screen.getByRole("option", {name: "npm"})).toBeInTheDocument(); expect(screen.getByRole("option", {name: "generic-http"})).toBeInTheDocument(); - expect(screen.getByRole("option", {name: "maven"}).selected).toBe(true); - expect(screen.getByRole("combobox")).toHaveValue("maven"); + expect(screen.getByRole("option", {name: ""}).selected).toBe(true); + expect(screen.getByRole("pkgTypeSel")).toHaveValue(""); }); }); - it("Verify PackageTypeSelect for npm selected", async ()=>{ - render(); - await waitFor(()=>{ - expect(screen.getByRole("option", {name: "maven"})).toBeInTheDocument(); - expect(screen.getByRole("option", {name: "npm"})).toBeInTheDocument(); - expect(screen.getByRole("option", {name: "generic-http"})).toBeInTheDocument(); + // it("Verify PackageTypeSelect for npm selected", async ()=>{ + // render(); + // await waitFor(()=>{ + // expect(screen.getByRole("option", {name: "maven"})).toBeInTheDocument(); + // expect(screen.getByRole("option", {name: "npm"})).toBeInTheDocument(); + // expect(screen.getByRole("option", {name: "generic-http"})).toBeInTheDocument(); - expect(screen.getByRole("combobox")).toHaveValue("npm"); - expect(screen.getByRole("option", {name: "npm"}).selected).toBe(true); - }); - }); + // const pkgSelect = screen.getByRole("pkgTypeSel"); + // expect(pkgSelect).toHaveValue("npm"); + // expect(screen.getByRole("option", {name: "npm"}).selected).toBe(true); + // }); + // }); it("Verify PackageTypeSelect for value change", async ()=>{ const {selectOptions} = userEvent.setup(); let value = ""; - const vauleChangeHandler = e => { - value = e.target.value; - }; - render(); + render(); expect(value).toBe(""); await waitFor(() => { - expect(screen.getByRole("option", {name: "maven"}).selected).toBe(true); + expect(screen.getByRole("pkgTypeSel")).toHaveValue(""); + }); + await waitFor(() => { expect(screen.getByRole("option", {name: "npm"})).toBeInTheDocument(); - selectOptions(screen.getByRole("combobox"), "npm"); - expect(value).toBe("npm"); + selectOptions(screen.getByRole("pkgTypeSel"), "npm"); + expect(screen.getByRole("pkgTypeSel")).toHaveValue("npm"); }); }); diff --git a/src/main/webui/src/app/components/content/common/StoreControlPanels.jsx b/src/main/webui/src/app/components/content/common/StoreControlPanels.jsx index c461c51..2bb5e8c 100644 --- a/src/main/webui/src/app/components/content/common/StoreControlPanels.jsx +++ b/src/main/webui/src/app/components/content/common/StoreControlPanels.jsx @@ -66,9 +66,9 @@ StoreViewControlPanel.propTypes={ store: PropTypes.object }; -const StoreEditControlPanel = ({mode, store}) =>{ +const StoreEditControlPanel = ({mode, store, handleSubmit}) =>{ const navigate = useNavigate(); - const handleSave = () => { + const save = () => { const saveUrl = `${STORE_API_BASE_URL}/${store.packageType}/${store.type}/${store.name}`; const saveStore = async () => { let response = {}; @@ -115,6 +115,16 @@ const StoreEditControlPanel = ({mode, store}) =>{ } }; + let handleSave = () => save(); + if(handleSubmit && typeof handleSubmit === 'function'){ + // console.log(handleSubmit); + handleSave = handleSubmit(data=>{ + data.disabled = !data.enabled; + data.enabled = undefined; + Utils.rewriteTargetObject(data, store); + save(); + }); + } return
{' '} {' '} @@ -127,7 +137,8 @@ const StoreEditControlPanel = ({mode, store}) =>{ }; StoreEditControlPanel.propTypes={ mode: PropTypes.string, - store: PropTypes.object + store: PropTypes.object, + handleSubmit: PropTypes.func }; export {StoreViewControlPanel, StoreEditControlPanel}; diff --git a/src/main/webui/src/app/components/content/remote/RemoteEdit.jsx b/src/main/webui/src/app/components/content/remote/RemoteEdit.jsx index aeff95f..3138f24 100644 --- a/src/main/webui/src/app/components/content/remote/RemoteEdit.jsx +++ b/src/main/webui/src/app/components/content/remote/RemoteEdit.jsx @@ -16,6 +16,7 @@ import React, {useState, useEffect} from 'react'; import {useLocation, useParams} from 'react-router-dom'; +import {useForm} from 'react-hook-form'; import {PropTypes} from 'prop-types'; import {StoreEditControlPanel as EditControlPanel} from '../common/StoreControlPanels.jsx'; import {DisableTimeoutHint, DurationHint, Hint} from '../common/Hints.jsx'; @@ -24,14 +25,14 @@ import {PackageTypeSelect} from '../common/PackageTypeSelect.jsx'; import {Utils} from '#utils/AppUtils.js'; import {TimeUtils} from '#utils/TimeUtils.js'; import {jsonRest} from '#utils/RestClient.js'; -import {STORE_API_BASE_URL} from "../../ComponentConstants.js"; +import {STORE_API_BASE_URL, PATTERNS} from "../../ComponentConstants.js"; -const CertificateSection = ({store, handleValueChange}) =>
+const CertificateSection = ({store, register}) =>
{ store.useAuth &&
- handleValueChange(e, "key_password")}/> +
}
@@ -44,7 +45,8 @@ const CertificateSection = ({store, handleValueChange}) =>
handleValueChange(e, "key_certificate_pem")}> +
}
@@ -54,14 +56,15 @@ const CertificateSection = ({store, handleValueChange}) =>
handleValueChange(e, "server_certificate_pem")}> +
; CertificateSection.propTypes = { store: PropTypes.object.isRequired, - handleValueChange: PropTypes.func + register: PropTypes.func }; export default function RemoteEdit() { @@ -74,6 +77,12 @@ export default function RemoteEdit() { const [useX509, setUseX509] = useState(false); const location = useLocation(); const {packageType, name} = useParams(); + const { + register, + reset, + handleSubmit, + formState: {errors} + } = useForm(); const path = location.pathname; const mode = path.match(/.*\/new$/u)? 'new':'edit'; @@ -81,11 +90,12 @@ export default function RemoteEdit() { // Give a default packageType let store = {"packageType": "maven", "type": "remote"}; [pkgType, storeName] = [packageType, name]; + const storeAPIEndpoint = `${STORE_API_BASE_URL}/${pkgType}/remote/${storeName}`; useEffect(()=>{ if(mode === 'edit'){ const fetchStore = async () =>{ // get Store data - const response = await jsonRest.get(`${STORE_API_BASE_URL}/${pkgType}/remote/${storeName}`); + const response = await jsonRest.get(storeAPIEndpoint); if (response.ok){ const raw = await response.json(); const storeView = Utils.cloneObj(raw); @@ -109,6 +119,7 @@ export default function RemoteEdit() { storeView: cloned, store: raw }); + reset(raw); setUseProxy(storeView.useProxy); setUseAuth(storeView.useAuth); setUseX509(storeView.useX509); @@ -122,23 +133,12 @@ export default function RemoteEdit() { fetchStore(); } - }, [pkgType, storeName, mode]); + }, [storeAPIEndpoint, mode, reset]); if (mode === 'edit'){ store = state.store; } - const handleCheckChange = (event, field) => { - if (event.target.checked) { - store[field] = true; - } else { - store[field] = false; - } - }; - const handleValueChange = (event, field) => { - store[field] = event.target.value; - }; - const handleUseProxy = event=>{ setUseProxy(event.target.checked); }; @@ -150,9 +150,9 @@ export default function RemoteEdit() { }; return ( - +
e.preventDefault()}>
- +
@@ -163,9 +163,7 @@ export default function RemoteEdit() { { mode==='new'? - handleValueChange(e, "packageType")} /> + : {store.packageType} } @@ -175,14 +173,17 @@ export default function RemoteEdit() { { mode==='new'? - handleValueChange(e, "name")} /> + {' '} + {errors.name?.type === "required" && Name is required} + {errors.name?.type === "maxLength" && Name's length should be less than 50} : {store.name} }
- handleCheckChange(e, "disabled")}/>{' '} + {' '} { store.disabled && store.disableExpiration && @@ -190,7 +191,7 @@ export default function RemoteEdit() { }
- handleCheckChange(e, "authoritative_index")} />{' '} + {' '} Make the content index authoritative to this repository
@@ -198,31 +199,37 @@ export default function RemoteEdit() {
- handleValueChange(e, "disable_timeout")} /> + {' '} + {errors.disable_timeout && Not a valid number}
- handleValueChange(e, "url")}/> + {' '} + {errors.url?.type==="required" && Remote URL is required} + {errors.url?.type==="pattern" && Not a valid URL}
- handleCheckChange(e, "is_passthrough")} />{' '} + {' '}
{!store.is_passthrough &&
- handleValueChange(e, "cache_timeout_seconds")} /> +
- handleValueChange(e, "metadata_timeout_seconds")} /> + {'24h 36m 00s. Negative means never timeout, 0 means using default timeout by Indy settings.'}
@@ -232,17 +239,17 @@ export default function RemoteEdit() {
- handleValueChange(e,"prefetch_priority")} size="5"/> +
- handleCheckChange(e,"prefetch_rescan")} />{' '} + {' '}
- handleRadioChange(e, "prefetch_listing_type")} defaultValue="html"/> html{' '} - handleRadioChange(e, "prefetch_listing_type")} defaultValue="koji"/> koji + html{' '} + koji
*/}
@@ -250,17 +257,23 @@ export default function RemoteEdit() {
Description
- +
Capabilities
- handleCheckChange(e, "allow_releases")}/>{' '} + + + {' '}
- handleCheckChange(e, "allow_snapshots")}/>{' '} + + + {' '}
@@ -269,7 +282,7 @@ export default function RemoteEdit() {
- handleValueChange(e, "timeout_seconds")}/> +
@@ -288,11 +301,11 @@ export default function RemoteEdit() {
- handleValueChange(e,"proxy_host")} size="20"/> +
- handleValueChange(e,"proxy_port")} size="6"/> +
} @@ -308,21 +321,21 @@ export default function RemoteEdit() {
- handleValueChange(e, "user")} size="25"/> +
- handleValueChange(e, "password")} size="25"/> +
{ store.use_proxy &&
- handleValueChange(e, "proxy_user")} size="20"/> +
- handleValueChange(e, "proxy_password")} size="20"/> +
} @@ -333,14 +346,14 @@ export default function RemoteEdit() {
{ - useX509 && + useX509 && }
{ // + ); } diff --git a/src/main/webui/src/app/components/styles/indy.css b/src/main/webui/src/app/components/styles/indy.css index 8b8d102..cd4b900 100644 --- a/src/main/webui/src/app/components/styles/indy.css +++ b/src/main/webui/src/app/components/styles/indy.css @@ -454,3 +454,7 @@ label, .label{ font-family: Arial, Helvetica, sans-serif; box-sizing: border-box; } + +.alert { + color: red; +} \ No newline at end of file diff --git a/src/main/webui/src/app/utils/AppUtils.js b/src/main/webui/src/app/utils/AppUtils.js index 5a34954..e4c0f5f 100644 --- a/src/main/webui/src/app/utils/AppUtils.js +++ b/src/main/webui/src/app/utils/AppUtils.js @@ -144,5 +144,15 @@ export const Utils = { const allParams = [message]; params.forEach(p => allParams.push(p)); Reflect.apply(console.log, undefined, allParams); + }, + rewriteTargetObject: (origin, target) => { + for (const prop in origin) { + if (origin[prop] !== undefined){ + const val = origin[prop]; + if(typeof val !== 'string' || val.trim() !== ''){ + target[prop] = val; + } + } + } } }; diff --git a/src/main/webui/src/app/utils/AppUtils.test.js b/src/main/webui/src/app/utils/AppUtils.test.js index a51c7b2..845a0d9 100644 --- a/src/main/webui/src/app/utils/AppUtils.test.js +++ b/src/main/webui/src/app/utils/AppUtils.test.js @@ -179,4 +179,51 @@ describe('AppUtils tests', () => { expect(Utils.cloneObj({a: {b: "2"}, c: ["3", "4"]})).toEqual({c: ["3", "4"], a: {b: "2"}}); }); + it("Check rewriteTargetObject", ()=>{ + let origin = { + "name": "central", + "type": "remote", + "packageType": "maven", + "key": "maven:remote:central", + "url": "https://repo1.maven.org/maven2/" + }; + let target = {}; + Utils.rewriteTargetObject(origin, target); + expect(target).toEqual(origin); + + target = { + "name": "central", + "type": "remote", + "packageType": "maven", + "key": "maven:remote:central", + "url": "https://notexist.com/fake/" + }; + Utils.rewriteTargetObject(origin, target); + expect(target).toEqual(origin); + + origin ={ + "name": "central", + "type": "remote", + "packageType": "maven", + "key": "maven:remote:central", + "url": " ", + }; + target = { + "name": "central", + "type": "remote", + "packageType": "maven", + "key": "maven:remote:central", + "url": "https://repo2.maven.org/maven2/", + "user": "testUser" + }; + Utils.rewriteTargetObject(origin, target); + expect(target.name).toEqual(origin.name); + expect(target.type).toEqual(origin.type); + expect(target.packageType).toEqual(origin.packageType); + expect(target.url).not.toEqual(origin.url); + expect(target.url).toBe("https://repo2.maven.org/maven2/"); + expect(target.user).not.toEqual(origin.user); + expect(target.user).toBe("testUser"); + }); + });