diff --git a/CHANGELOG.md b/CHANGELOG.md index c048a1d..52935be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Show deletion progress for buckets/entries with non-blocking deletion, [PR-156](https://github.com/reductstore/web-console/pull/156) +- Use Monaco Editor for JSON query editor with syntax highlighting auto-completion, [PR-158](https://github.com/reductstore/web-console/issues/158) - Support for changing replication mode (enabled, paused, disabled), [PR-157](https://github.com/reductstore/web-console/pull/157) ## 1.12.1 - 2025-11-17 diff --git a/package-lock.json b/package-lock.json index 2182159..eaa9a05 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,14 +8,15 @@ "name": "web-console", "version": "1.12.1", "dependencies": { + "@monaco-editor/react": "^4.7.0", + "@reductstore/reduct-query-monaco": "^1.0.1", "chart.js": "^4.5.0", "chartjs-adapter-dayjs-4": "^1.0.4", "chartjs-plugin-zoom": "^2.2.0", - "codemirror": "^5.65.18", "json5": "^2.2.3", "mime-types": "^3.0.1", + "monaco-editor": "^0.55.1", "react-chartjs-2": "^5.3.0", - "react-codemirror2": "^8.0.1", "zustand": "^4.5.6" }, "devDependencies": { @@ -3464,6 +3465,29 @@ "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==", "dev": true }, + "node_modules/@monaco-editor/loader": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.7.0.tgz", + "integrity": "sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA==", + "license": "MIT", + "dependencies": { + "state-local": "^1.0.6" + } + }, + "node_modules/@monaco-editor/react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.7.0.tgz", + "integrity": "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==", + "license": "MIT", + "dependencies": { + "@monaco-editor/loader": "^1.5.0" + }, + "peerDependencies": { + "monaco-editor": ">= 0.25.0 < 1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "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", @@ -3812,6 +3836,15 @@ "react-dom": ">=16.9.0" } }, + "node_modules/@reductstore/reduct-query-monaco": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@reductstore/reduct-query-monaco/-/reduct-query-monaco-1.0.1.tgz", + "integrity": "sha512-f8bklEbuKuJJrS55v6Jh/gsprz6YiAd0lnjff/+yeeyJaGD64RMN0wYeMpt2dtp/BV4jINI647SoyzXfJP9iMA==", + "license": "MIT", + "peerDependencies": { + "monaco-editor": ">=0.40.0" + } + }, "node_modules/@rollup/plugin-babel": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", @@ -4939,7 +4972,7 @@ "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "dev": true + "devOptional": true }, "node_modules/@types/ws": { "version": "8.5.10", @@ -7140,12 +7173,6 @@ "node": ">= 4.0" } }, - "node_modules/codemirror": { - "version": "5.65.18", - "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.18.tgz", - "integrity": "sha512-Gaz4gHnkbHMGgahNt3CA5HBk5lLQBqmD/pBgeB4kQU6OedZmqMBjlRF0LSrp2tJ4wlLNPm2FfaUd1pDy0mdlpA==", - "peer": true - }, "node_modules/collect-v8-coverage": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", @@ -8305,6 +8332,15 @@ "url": "https://github.com/fb55/domhandler?sponsor=1" } }, + "node_modules/dompurify": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz", + "integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/domutils": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", @@ -14472,6 +14508,18 @@ "tmpl": "1.0.5" } }, + "node_modules/marked": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz", + "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/mdn-data": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz", @@ -14703,6 +14751,17 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/monaco-editor": { + "version": "0.55.1", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz", + "integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==", + "license": "MIT", + "peer": true, + "dependencies": { + "dompurify": "3.2.7", + "marked": "14.0.0" + } + }, "node_modules/moo": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz", @@ -17856,15 +17915,6 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/react-codemirror2": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/react-codemirror2/-/react-codemirror2-8.0.1.tgz", - "integrity": "sha512-ZALowE5sGK1t66i0Fm1hoJLWT/iZHNjaAmcjwgYl9gyl2v1sqWwxzWHLmgq6K9KCk7p5+I+U3jMF1vsLaIkrmA==", - "peerDependencies": { - "codemirror": "5.x", - "react": ">=15.5 <=18.x" - } - }, "node_modules/react-dev-utils": { "version": "12.0.1", "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz", @@ -18056,7 +18106,6 @@ "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==", - "dev": true, "peer": true, "dependencies": { "loose-envify": "^1.1.0", @@ -18994,7 +19043,6 @@ "version": "0.20.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz", "integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==", - "dev": true, "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1" @@ -19488,6 +19536,12 @@ "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==", "dev": true }, + "node_modules/state-local": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz", + "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==", + "license": "MIT" + }, "node_modules/static-eval": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/static-eval/-/static-eval-2.0.2.tgz", diff --git a/package.json b/package.json index 448ff14..9345222 100644 --- a/package.json +++ b/package.json @@ -69,14 +69,15 @@ ] }, "dependencies": { + "@monaco-editor/react": "^4.7.0", + "@reductstore/reduct-query-monaco": "^1.0.1", "chart.js": "^4.5.0", "chartjs-adapter-dayjs-4": "^1.0.4", "chartjs-plugin-zoom": "^2.2.0", - "codemirror": "^5.65.18", "json5": "^2.2.3", "mime-types": "^3.0.1", + "monaco-editor": "^0.55.1", "react-chartjs-2": "^5.3.0", - "react-codemirror2": "^8.0.1", "zustand": "^4.5.6" } } diff --git a/src/App.css b/src/App.css index 08c7f09..23a4ca0 100644 --- a/src/App.css +++ b/src/App.css @@ -3,6 +3,11 @@ --primary-bg: #231b49; --primary-fg: #cccccc; --primary-hover: #ffffff; + + /* Input colors (Ant Design defaults) */ + --input-border: #d9d9d9; + --input-focus: #231b49; + --input-error: #ff4d4f; } /* --- Menu Styles --- */ diff --git a/src/Components/JsonEditor/JsonQueryEditor.css b/src/Components/JsonEditor/JsonQueryEditor.css new file mode 100644 index 0000000..810c693 --- /dev/null +++ b/src/Components/JsonEditor/JsonQueryEditor.css @@ -0,0 +1,114 @@ +.jsonQueryEditor { + border: 1px solid var(--input-border); + border-radius: 6px; + padding-top: 8px; + background: #ffffff; + overflow: visible; +} + +.jsonQueryEditorContainer { + display: flex; + flex-direction: column; + height: 100%; +} + +.jsonQueryEditorToolbar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + padding: 6px 8px; + border-top: 1px solid var(--input-border); + background: #fafafa; + border-radius: 0 0 6px 6px; +} + +.jsonQueryEditorValidation { + display: flex; + align-items: center; + gap: 6px; + font-size: 12px; + color: #595959; + font-weight: 500; + flex: 1; + min-width: 0; + line-height: 1.3; +} + +.jsonQueryEditorValidationMuted { + color: #8c8c8c; +} + +.jsonQueryEditorValidationOk { + font-weight: 600; + color: #52c41a; +} + +.jsonQueryEditorValidationWarning { + font-weight: 600; + color: #faad14; +} + +.jsonQueryEditorValidationError { + font-weight: 600; + color: var(--input-error); +} + +.jsonQueryEditorToolbarActions { + display: flex; + align-items: center; + gap: 4px; +} + +.jsonQueryEditorBody { + flex: 1; + min-height: 0; +} + +.jsonQueryEditorBody .monaco-editor { + border-radius: 6px; +} + +.jsonQueryEditorBody .monaco-editor .margin { + border-radius: 6px; +} + +.jsonQueryEditorBody .monaco-editor .overflow-guard { + border-radius: 6px; +} + +.jsonQueryEditorPlaceholder { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + background: #fafafa; + color: #8c8c8c; + font-size: 12px; + text-align: center; + padding: 8px; +} + +.jsonQueryEditorModal .ant-modal-content { + display: flex; + flex-direction: column; + height: 90vh; +} + +.jsonQueryEditorModal .ant-modal-body { + flex: 1; + min-height: 0; + padding: 0; +} + +.jsonQueryEditorModalContent { + height: 100%; +} + +.jsonQueryEditor:focus-within { + border-color: var(--input-focus); +} + +.jsonQueryEditor.hasError { + border-color: var(--input-error); +} diff --git a/src/Components/JsonEditor/JsonQueryEditor.test.tsx b/src/Components/JsonEditor/JsonQueryEditor.test.tsx new file mode 100644 index 0000000..8f6caa0 --- /dev/null +++ b/src/Components/JsonEditor/JsonQueryEditor.test.tsx @@ -0,0 +1,105 @@ +import React from "react"; +import { render, screen, fireEvent } from "@testing-library/react"; +import type { Client } from "reduct-js"; +import { JsonQueryEditor } from "./JsonQueryEditor"; +import { mockJSDOM } from "../../Helpers/TestHelpers"; + +jest.mock("@monaco-editor/react", () => ({ + __esModule: true, + default: ({ + value, + onChange, + }: { + value?: string; + onChange?: (value: string) => void; + }) => ( +