diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 4a6e7660c7d..e557a5103cd 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -56,6 +56,7 @@ "eslint-plugin-simple-import-sort": "^12.1.1", "eslint-plugin-unused-imports": "^4.1.3", "fake-indexeddb": "^6.0.0", + "fast-json-patch": "^3.1.1", "fuse.js": "^7.0.0", "humanize-duration": "^3.27.2", "i18next": "^23.15.1", @@ -7943,6 +7944,11 @@ "node": ">= 6" } }, + "node_modules/fast-json-patch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/fast-json-patch/-/fast-json-patch-3.1.1.tgz", + "integrity": "sha512-vf6IHUX2SBcA+5/+4883dsIjpBTqmfBjmYiWK1savxQmFk4JfBMLa7ynTYOs1Rolp/T1betJxHiGD3g1Mn8lUQ==" + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 4581660e9f5..c663345fddb 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -57,6 +57,7 @@ "eslint-plugin-simple-import-sort": "^12.1.1", "eslint-plugin-unused-imports": "^4.1.3", "fake-indexeddb": "^6.0.0", + "fast-json-patch": "^3.1.1", "fuse.js": "^7.0.0", "humanize-duration": "^3.27.2", "i18next": "^23.15.1", diff --git a/frontend/src/components/common/Resource/EditButton.tsx b/frontend/src/components/common/Resource/EditButton.tsx index 6eb1d0c98bf..7ce00b66650 100644 --- a/frontend/src/components/common/Resource/EditButton.tsx +++ b/frontend/src/components/common/Resource/EditButton.tsx @@ -15,10 +15,13 @@ */ import { Icon } from '@iconify/react'; +import { compare } from 'fast-json-patch'; import React from 'react'; import { useTranslation } from 'react-i18next'; import { useDispatch } from 'react-redux'; import { useLocation } from 'react-router-dom'; +import { patch } from '../../../lib/k8s/api/v1/clusterRequests'; +import { KubeObjectEndpoint } from '../../../lib/k8s/api/v2/KubeObjectEndpoint'; import { KubeObject } from '../../../lib/k8s/KubeObject'; import { KubeObjectInterface } from '../../../lib/k8s/KubeObject'; import { CallbackActionOptions, clusterAction } from '../../../redux/clusterActionSlice'; @@ -51,6 +54,10 @@ export default function EditButton(props: EditButtonProps) { const dispatchHeadlampEditEvent = useEventCallback(HeadlampEventType.EDIT_RESOURCE); const activityId = 'edit-' + item.metadata.uid; + // Store the original resource snapshot (firstDraft) for JSON Patch comparison + // This is set when the editor is opened + const originalResourceRef = React.useRef(null); + function makeErrorMessage(err: any) { const status: number = err.status; switch (status) { @@ -63,7 +70,41 @@ export default function EditButton(props: EditButtonProps) { async function updateFunc(newItem: KubeObjectInterface) { try { - await item.update(newItem); + if (!originalResourceRef.current) { + throw new Error('Original resource snapshot not found'); + } + + // Calculate JSON Patch: diff between original (when editor opened) and new (user edited) + const patches = compare(originalResourceRef.current, newItem); + + if (patches.length === 0) { + // No changes detected + Activity.close(activityId); + return; + } + + // Build the API URL + const apiInfo = item._class().apiEndpoint.apiInfo[0]; + const endpoint: KubeObjectEndpoint = { + group: apiInfo.group, + version: apiInfo.version, + resource: apiInfo.resource, + }; + + const namespace = item.getNamespace(); + const name = item.getName(); + const urlParts = [KubeObjectEndpoint.toUrl(endpoint, namespace), name]; + const url = urlParts.filter(Boolean).join('/'); + + // Use the patch function with JSON Patch content type + // Override the default 'application/merge-patch+json' to 'application/json-patch+json' + await patch(url, patches, true, { + cluster: item._clusterName, + headers: { + 'Content-Type': 'application/json-patch+json', + }, + }); + Activity.close(activityId); } catch (err) { Activity.update(activityId, { minimized: false }); @@ -128,6 +169,9 @@ export default function EditButton(props: EditButtonProps) { if (afterConfirm) { afterConfirm(); } + const editableObject = item.getEditableObject(); + originalResourceRef.current = editableObject; + Activity.launch({ id: activityId, title: t('translation|Edit') + ': ' + item.metadata.name, @@ -136,7 +180,7 @@ export default function EditButton(props: EditButtonProps) { content: ( Activity.close(activityId)} onSave={handleSave} diff --git a/plugins/headlamp-plugin/package-lock.json b/plugins/headlamp-plugin/package-lock.json index fbfc1cfa892..d7bf60f6eee 100644 --- a/plugins/headlamp-plugin/package-lock.json +++ b/plugins/headlamp-plugin/package-lock.json @@ -65,6 +65,7 @@ "eslint-plugin-react-hooks": "^4.6.2", "eslint-plugin-simple-import-sort": "^12.1.1", "eslint-plugin-unused-imports": "^4.1.3", + "fast-json-patch": "^3.1.1", "fs-extra": "^11.2.0", "fuse.js": "^7.0.0", "humanize-duration": "^3.27.2", @@ -8061,6 +8062,11 @@ "integrity": "sha512-FRWsaZRWEJ1ESVNbDWmsAlqDk96gPQezzLghafp5J4GUKjbCz3OkAHuZs5TuPEtkbVQERysLp9xv6c24fBm8Aw==", "license": "MIT" }, + "node_modules/fast-json-patch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/fast-json-patch/-/fast-json-patch-3.1.1.tgz", + "integrity": "sha512-vf6IHUX2SBcA+5/+4883dsIjpBTqmfBjmYiWK1savxQmFk4JfBMLa7ynTYOs1Rolp/T1betJxHiGD3g1Mn8lUQ==" + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", diff --git a/plugins/headlamp-plugin/package.json b/plugins/headlamp-plugin/package.json index 630336ccc4a..1aa43ce069b 100644 --- a/plugins/headlamp-plugin/package.json +++ b/plugins/headlamp-plugin/package.json @@ -70,6 +70,7 @@ "eslint-plugin-react-hooks": "^4.6.2", "eslint-plugin-simple-import-sort": "^12.1.1", "eslint-plugin-unused-imports": "^4.1.3", + "fast-json-patch": "^3.1.1", "fs-extra": "^11.2.0", "fuse.js": "^7.0.0", "humanize-duration": "^3.27.2", diff --git a/plugins/headlamp-plugin/template/package-lock.json b/plugins/headlamp-plugin/template/package-lock.json index 7d5816238b7..9685dc46a7c 100644 --- a/plugins/headlamp-plugin/template/package-lock.json +++ b/plugins/headlamp-plugin/template/package-lock.json @@ -1780,11 +1780,10 @@ } }, "node_modules/@kinvolk/headlamp-plugin": { - "version": "0.13.0-alpha.10", - "resolved": "https://registry.npmjs.org/@kinvolk/headlamp-plugin/-/headlamp-plugin-0.13.0-alpha.10.tgz", - "integrity": "sha512-fBOvwA9OjMELLrJV5AN7UeNWLwKIGeeGcq99MNX3eKKZYvg5Fh6dJ80pLO1EtSwlTMNSHwv2q3Ljo4N7Cm1E6Q==", + "version": "0.13.0-alpha.11", + "resolved": "https://registry.npmjs.org/@kinvolk/headlamp-plugin/-/headlamp-plugin-0.13.0-alpha.11.tgz", + "integrity": "sha512-7HiZiiuJdiIcFLfrQafbWTdna4hnyPBfaHYiMXGYwXIpuYX4SMiEGuRw2KvV4I+81sanV91ekQel1YTL0SGXZQ==", "dev": true, - "license": "Apache 2.0", "dependencies": { "@apidevtools/swagger-parser": "^10.0.3", "@emotion/react": "^11.11.1", @@ -1866,7 +1865,7 @@ "react-dropzone": "^14.2.9", "react-hotkeys-hook": "^4.5.1", "react-i18next": "^15.0.2", - "react-markdown": "^9.0.1", + "react-markdown": "^10.1.0", "react-redux": "^9.1.2", "react-router": "^5.3.0", "react-router-dom": "^5.3.0", @@ -8159,11 +8158,10 @@ } }, "node_modules/eslint-plugin-jsx-a11y/node_modules/axe-core": { - "version": "4.10.3", - "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.3.tgz", - "integrity": "sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==", + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.0.tgz", + "integrity": "sha512-ilYanEU8vxxBexpJd8cWM4ElSQq4QctCLKih0TSfjIfCQTeyH/6zVrmIJfLPrKTKJRbiG+cfnZbQIjAlJmF1jQ==", "dev": true, - "license": "MPL-2.0", "engines": { "node": ">=4" } @@ -12364,7 +12362,6 @@ "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-4.22.0.tgz", "integrity": "sha512-IGBwjF7tNk3cwypFNH/7bfzBcgSCbaMOD3GsaY1AU/JRrnHnYgEM0+9kQt52iZxjNsjBtJYtao146V+f8jFZNw==", "dev": true, - "license": "MIT", "engines": { "node": ">=10" }, @@ -13869,11 +13866,10 @@ "license": "MIT" }, "node_modules/react-markdown": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-9.1.0.tgz", - "integrity": "sha512-xaijuJB0kzGiUdG7nc2MOMDUDBWPyGAjZtUrow9XxUeua8IqeP+VlIfAZ3bphpcLTnSZXz6z9jcVC/TCwbfgdw==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz", + "integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==", "dev": true, - "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0",