diff --git a/content/pages/editMetadata.json b/content/pages/editMetadata.json index ff17820ca..ed40f9b78 100644 --- a/content/pages/editMetadata.json +++ b/content/pages/editMetadata.json @@ -64,7 +64,72 @@ "computeHelp": "For a compute dataset, your file should match the file type required by the algorithm, and should not exceed 1 GB in file size. ", "prominentHelp": true, "type": "files", - "required": true + "required": true, + "innerFields": [ + { + "value": "headers", + "title": "Headers", + "label": "Headers", + "placeholder_value": "Authorization", + "help": "This HEADERS will be stored encrypted after publishing.", + "type": "headers", + "required": true + } + ] + }, + { + "value": "graphql", + "title": "Graphql", + "label": "URL", + "placeholder": "e.g. http://172.15.0.15:8000/subgraphs/name/oceanprotocol/ocean-subgraph", + "help": "This URL will be stored encrypted after publishing.", + "computeHelp": "For a compute dataset, your file should match the file type required by the algorithm, and should not exceed 1 GB in file size. ", + "prominentHelp": true, + "type": "files", + "required": true, + "headers": true, + "innerFields": [ + { + "value": "headers", + "title": "Headers", + "label": "Headers", + "placeholder_value": "Authorization", + "help": "This HEADERS will be stored encrypted after publishing.", + "type": "headers", + "required": true + }, + { + "value": "query", + "title": "Query", + "label": "Query", + "placeholder": "query{\n nfts(\n orderBy: createdTimestamp,\n orderDirection:desc\n ){\n id\n symbol\n createdTimestamp\n }\n}", + "help": "This QUERY will be stored encrypted after publishing.", + "type": "codeeditor", + "required": true + } + ] + }, + { + "value": "smartcontract", + "title": "Smart Contract", + "label": "Address", + "placeholder": "e.g. 0x8149276f275EEFAc110D74AFE8AFECEaeC7d1593", + "help": "This ADDRESS will be stored encrypted after publishing. **Please make sure that the endpoint is accessible over the internet and is not protected by a firewall or by credentials.**", + "computeHelp": "For a compute dataset, your file should match the file type required by the algorithm, and should not exceed 1 GB in file size. ", + "prominentHelp": true, + "type": "files", + "required": true, + "innerFields": [ + { + "value": "abi", + "title": "ABI", + "label": "ABI", + "placeholder": "{\n 'inputs': [],\n 'name': 'swapOceanFee',\n 'outputs': [{'internalType': 'uint256', 'name': '', 'type': 'uint256'}],\n 'stateMutability': 'view',\n 'type': 'function'\n}", + "help": "This ABI will be stored encrypted after publishing.", + "type": "codeeditor", + "required": true + } + ] } ], "sortOptions": false, diff --git a/content/publish/form.json b/content/publish/form.json index 463ef8e16..6738fc12b 100644 --- a/content/publish/form.json +++ b/content/publish/form.json @@ -138,7 +138,72 @@ "computeHelp": "For a compute dataset, your file should match the file type required by the algorithm, and should not exceed 1 GB in file size. ", "prominentHelp": true, "type": "files", - "required": true + "required": true, + "methods": true, + "innerFields": [ + { + "value": "headers", + "title": "Headers", + "label": "Headers", + "placeholder_value": "Authorization", + "help": "This HEADERS will be stored encrypted after publishing.", + "type": "headers", + "required": false + } + ] + }, + { + "value": "graphql", + "title": "Graphql", + "label": "URL", + "placeholder": "e.g. http://172.15.0.15:8000/subgraphs/name/oceanprotocol/ocean-subgraph", + "help": "This URL will be stored encrypted after publishing.", + "computeHelp": "For a compute dataset, your file should match the file type required by the algorithm, and should not exceed 1 GB in file size. ", + "prominentHelp": true, + "type": "files", + "required": true, + "innerFields": [ + { + "value": "headers", + "title": "Headers", + "label": "Headers", + "placeholder_value": "Authorization", + "help": "This HEADERS will be stored encrypted after publishing.", + "type": "headers", + "required": false + }, + { + "value": "query", + "title": "Query", + "label": "Query", + "placeholder": "query{\n nfts(\n orderBy: createdTimestamp,\n orderDirection:desc\n ){\n id\n symbol\n createdTimestamp\n }\n}", + "help": "This QUERY will be stored encrypted after publishing.", + "type": "codeeditor", + "required": true + } + ] + }, + { + "value": "smartcontract", + "title": "Smartcontract", + "label": "Address", + "placeholder": "e.g. 0x8149276f275EEFAc110D74AFE8AFECEaeC7d1593", + "help": "This ADDRESS will be stored encrypted after publishing.", + "computeHelp": "For a compute dataset, your file should match the file type required by the algorithm, and should not exceed 1 GB in file size. ", + "prominentHelp": true, + "type": "files", + "required": true, + "innerFields": [ + { + "value": "abi", + "title": "ABI", + "label": "ABI", + "placeholder": "{\n 'inputs': [],\n 'name': 'swapOceanFee',\n 'outputs': [{'internalType': 'uint256', 'name': '', 'type': 'uint256'}],\n 'stateMutability': 'view',\n 'type': 'function'\n}", + "help": "This ABI will be stored encrypted after publishing.", + "type": "codeeditor", + "required": true + } + ] } ], "sortOptions": false, diff --git a/package-lock.json b/package-lock.json index 46c5d2a93..7ca443ca6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,8 @@ "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { + "@codemirror/lang-json": "^6.0.1", + "@codemirror/language": "^6.3.1", "@coingecko/cryptoformat": "^0.5.4", "@loadable/component": "^5.15.2", "@oceanprotocol/art": "^3.2.0", @@ -17,6 +19,7 @@ "@oceanprotocol/typographies": "^0.1.0", "@oceanprotocol/use-dark-mode": "^2.4.3", "@tippyjs/react": "^4.2.6", + "@uiw/react-codemirror": "^4.19.5", "@urql/exchange-refocus": "^1.0.0", "@walletconnect/web3-provider": "^1.8.0", "axios": "^1.2.0", @@ -58,7 +61,7 @@ "yup": "^0.32.11" }, "devDependencies": { - "@storybook/addon-essentials": "^6.5.15", + "@storybook/addon-essentials": "^6.5.13", "@storybook/builder-webpack5": "^6.5.13", "@storybook/manager-webpack5": "^6.5.13", "@storybook/react": "^6.5.13", @@ -76,6 +79,7 @@ "@types/remove-markdown": "^0.3.1", "@typescript-eslint/eslint-plugin": "^5.43.0", "@typescript-eslint/parser": "^5.43.0", + "@uiw/codemirror-themes": "^4.19.1", "apollo": "^2.34.0", "cross-env": "^7.0.3", "eslint": "^8.35.0", @@ -2039,6 +2043,102 @@ "node": ">=0.1.95" } }, + "node_modules/@codemirror/autocomplete": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.4.0.tgz", + "integrity": "sha512-HLF2PnZAm1s4kGs30EiqKMgD7XsYaQ0XJnMR0rofEWQ5t5D60SfqpDIkIh1ze5tiEbyUWm8+VJ6W1/erVvBMIA==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.6.0", + "@lezer/common": "^1.0.0" + }, + "peerDependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@codemirror/commands": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.1.3.tgz", + "integrity": "sha512-wUw1+vb34Ultv0Q9m/OVB7yizGXgtoDbkI5f5ErM8bebwLyUYjicdhJTKhTvPTpgkv8dq/BK0lQ3K5pRf2DAJw==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.2.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-json": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.1.tgz", + "integrity": "sha512-+T1flHdgpqDDlJZ2Lkil/rLiRy684WMLc74xUnjJH48GQdfJo/pudlTRreZmKwzP8/tGdKf83wlbAdOCzlJOGQ==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@lezer/json": "^1.0.0" + } + }, + "node_modules/@codemirror/language": { + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.3.2.tgz", + "integrity": "sha512-g42uHhOcEMAXjmozGG+rdom5UsbyfMxQFh7AbkeoaNImddL6Xt4cQDL0+JxmG7+as18rUAvZaqzP/TjsciVIrA==", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "node_modules/@codemirror/lint": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.1.0.tgz", + "integrity": "sha512-mdvDQrjRmYPvQ3WrzF6Ewaao+NWERYtpthJvoQ3tK3t/44Ynhk8ZGjTSL9jMEv8CgSMogmt75X8ceOZRDSXHtQ==", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/search": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.2.3.tgz", + "integrity": "sha512-V9n9233lopQhB1dyjsBK2Wc1i+8hcCqxl1wQ46c5HWWLePoe4FluV3TGHoZ04rBRlGjNyz9DTmpJErig8UE4jw==", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/state": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.2.0.tgz", + "integrity": "sha512-69QXtcrsc3RYtOtd+GsvczJ319udtBf1PTrr2KbLWM/e2CXUPnh0Nz9AUo8WfhSQ7GeL8dPVNUmhQVgpmuaNGA==" + }, + "node_modules/@codemirror/theme-one-dark": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.0.tgz", + "integrity": "sha512-AiTHtFRu8+vWT9wWUWDM+cog6ZwgivJogB1Tm/g40NIpLwph7AnmxrSzWfvJN5fBVufsuwBxecQCNmdcR5D7Aw==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/highlight": "^1.0.0" + } + }, + "node_modules/@codemirror/view": { + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.7.2.tgz", + "integrity": "sha512-HeK2GyycxceaQVyvYVYXmn1vUKYYBsHCcfGRSsFO+3fRRtwXx2STK0YiFBmiWx2vtU9gUAJgIUXUN8a0osI8Ng==", + "dependencies": { + "@codemirror/state": "^6.1.4", + "style-mod": "^4.0.0", + "w3c-keyname": "^2.2.4" + } + }, "node_modules/@coingecko/cryptoformat": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/@coingecko/cryptoformat/-/cryptoformat-0.5.4.tgz", @@ -4112,6 +4212,36 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@lezer/common": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.0.2.tgz", + "integrity": "sha512-SVgiGtMnMnW3ActR8SXgsDhw7a0w0ChHSYAyAUxxrOiJ1OqYWEKk/xJd84tTSPo1mo6DXLObAJALNnd0Hrv7Ng==" + }, + "node_modules/@lezer/highlight": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.1.3.tgz", + "integrity": "sha512-3vLKLPThO4td43lYRBygmMY18JN3CPh9w+XS2j8WC30vR4yZeFG4z1iFe4jXE43NtGqe//zHW5q8ENLlHvz9gw==", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@lezer/json": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.0.tgz", + "integrity": "sha512-zbAuUY09RBzCoCA3lJ1+ypKw5WSNvLqGMtasdW6HvVOqZoCpPr8eWrsGnOVWGKGn8Rh21FnrKRVlJXrGAVUqRw==", + "dependencies": { + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/lr": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.2.5.tgz", + "integrity": "sha512-f9319YG1A/3ysgUE3bqCHEd7g+3ZZ71MWlwEc42mpnLVYXgfJJgtu1XAyBB4Kz8FmqmnFe9caopDqKeMMMAU6g==", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, "node_modules/@loadable/component": { "version": "5.15.2", "resolved": "https://registry.npmjs.org/@loadable/component/-/component-5.15.2.tgz", @@ -17736,6 +17866,67 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@uiw/codemirror-extensions-basic-setup": { + "version": "4.19.5", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.19.5.tgz", + "integrity": "sha512-1zt7ZPJ01xKkSW/KDy0FZNga0bngN1fC594wCVG7FBi60ehfcAucpooQ+JSPScKXopxcb+ugPKZvVLzr9/OfzA==", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + }, + "peerDependencies": { + "@codemirror/autocomplete": ">=6.0.0", + "@codemirror/commands": ">=6.0.0", + "@codemirror/language": ">=6.0.0", + "@codemirror/lint": ">=6.0.0", + "@codemirror/search": ">=6.0.0", + "@codemirror/state": ">=6.0.0", + "@codemirror/view": ">=6.0.0" + } + }, + "node_modules/@uiw/codemirror-themes": { + "version": "4.19.5", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-themes/-/codemirror-themes-4.19.5.tgz", + "integrity": "sha512-BWCTwQJaGiOc+nYqPLQDjmCtIojaCEKx2aO1bOTyGw0fisKwGw9Csll+bi9ujqA+vk6qYJmXI0P5K7kVs8fbdA==", + "dev": true, + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + }, + "peerDependencies": { + "@codemirror/language": ">=6.0.0", + "@codemirror/state": ">=6.0.0", + "@codemirror/view": ">=6.0.0" + } + }, + "node_modules/@uiw/react-codemirror": { + "version": "4.19.5", + "resolved": "https://registry.npmjs.org/@uiw/react-codemirror/-/react-codemirror-4.19.5.tgz", + "integrity": "sha512-ZCHh8d7beXbF8/t7F1+yHht6A9Y6CdKeOkZq4A09lxJEnyTQrj1FMf2zvfaqc7K23KNjkTCtSlbqKKbVDgrWaw==", + "dependencies": { + "@babel/runtime": "^7.18.6", + "@codemirror/commands": "^6.1.0", + "@codemirror/state": "^6.1.1", + "@codemirror/theme-one-dark": "^6.0.0", + "@uiw/codemirror-extensions-basic-setup": "4.19.5", + "codemirror": "^6.0.0" + }, + "peerDependencies": { + "@babel/runtime": ">=7.11.0", + "@codemirror/state": ">=6.0.0", + "@codemirror/theme-one-dark": ">=6.0.0", + "@codemirror/view": ">=6.0.0", + "codemirror": ">=6.0.0", + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, "node_modules/@urql/core": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@urql/core/-/core-3.0.3.tgz", @@ -22190,6 +22381,20 @@ "node": ">=0.10.0" } }, + "node_modules/codemirror": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.1.tgz", + "integrity": "sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + } + }, "node_modules/collapse-white-space": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-1.0.6.tgz", @@ -23070,6 +23275,11 @@ "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", "dev": true }, + "node_modules/crelt": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.5.tgz", + "integrity": "sha512-+BO9wPPi+DWTDcNYhr/W90myha8ptzftZT+LwcmUbbok0rcP/fequmFYCw8NMoH7pkAZQzU78b3kYrlua5a9eA==" + }, "node_modules/cross-env": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", @@ -43370,6 +43580,11 @@ "webpack": "^4.0.0 || ^5.0.0" } }, + "node_modules/style-mod": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.0.0.tgz", + "integrity": "sha512-OPhtyEjyyN9x3nhPsu76f52yUGXiZcgvsrFVtvTkyGRQJ0XK+GPc6ov1z+lRpbeabka+MYEQxOYRnt5nF30aMw==" + }, "node_modules/style-to-object": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.3.0.tgz", @@ -45470,6 +45685,11 @@ "browser-process-hrtime": "^1.0.0" } }, + "node_modules/w3c-keyname": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.6.tgz", + "integrity": "sha512-f+fciywl1SJEniZHD6H+kUO8gOnwIr7f4ijKA6+ZvJFjeGi1r4PDLl53Ayud9O/rk64RqgoQine0feoeOU0kXg==" + }, "node_modules/w3c-xmlserializer": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-3.0.0.tgz", @@ -48351,6 +48571,96 @@ "minimist": "^1.2.0" } }, + "@codemirror/autocomplete": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.4.0.tgz", + "integrity": "sha512-HLF2PnZAm1s4kGs30EiqKMgD7XsYaQ0XJnMR0rofEWQ5t5D60SfqpDIkIh1ze5tiEbyUWm8+VJ6W1/erVvBMIA==", + "requires": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.6.0", + "@lezer/common": "^1.0.0" + } + }, + "@codemirror/commands": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.1.3.tgz", + "integrity": "sha512-wUw1+vb34Ultv0Q9m/OVB7yizGXgtoDbkI5f5ErM8bebwLyUYjicdhJTKhTvPTpgkv8dq/BK0lQ3K5pRf2DAJw==", + "requires": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.2.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0" + } + }, + "@codemirror/lang-json": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.1.tgz", + "integrity": "sha512-+T1flHdgpqDDlJZ2Lkil/rLiRy684WMLc74xUnjJH48GQdfJo/pudlTRreZmKwzP8/tGdKf83wlbAdOCzlJOGQ==", + "requires": { + "@codemirror/language": "^6.0.0", + "@lezer/json": "^1.0.0" + } + }, + "@codemirror/language": { + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.3.2.tgz", + "integrity": "sha512-g42uHhOcEMAXjmozGG+rdom5UsbyfMxQFh7AbkeoaNImddL6Xt4cQDL0+JxmG7+as18rUAvZaqzP/TjsciVIrA==", + "requires": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "@codemirror/lint": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.1.0.tgz", + "integrity": "sha512-mdvDQrjRmYPvQ3WrzF6Ewaao+NWERYtpthJvoQ3tK3t/44Ynhk8ZGjTSL9jMEv8CgSMogmt75X8ceOZRDSXHtQ==", + "requires": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "crelt": "^1.0.5" + } + }, + "@codemirror/search": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.2.3.tgz", + "integrity": "sha512-V9n9233lopQhB1dyjsBK2Wc1i+8hcCqxl1wQ46c5HWWLePoe4FluV3TGHoZ04rBRlGjNyz9DTmpJErig8UE4jw==", + "requires": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "crelt": "^1.0.5" + } + }, + "@codemirror/state": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.2.0.tgz", + "integrity": "sha512-69QXtcrsc3RYtOtd+GsvczJ319udtBf1PTrr2KbLWM/e2CXUPnh0Nz9AUo8WfhSQ7GeL8dPVNUmhQVgpmuaNGA==" + }, + "@codemirror/theme-one-dark": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.0.tgz", + "integrity": "sha512-AiTHtFRu8+vWT9wWUWDM+cog6ZwgivJogB1Tm/g40NIpLwph7AnmxrSzWfvJN5fBVufsuwBxecQCNmdcR5D7Aw==", + "requires": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/highlight": "^1.0.0" + } + }, + "@codemirror/view": { + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.7.2.tgz", + "integrity": "sha512-HeK2GyycxceaQVyvYVYXmn1vUKYYBsHCcfGRSsFO+3fRRtwXx2STK0YiFBmiWx2vtU9gUAJgIUXUN8a0osI8Ng==", + "requires": { + "@codemirror/state": "^6.1.4", + "style-mod": "^4.0.0", + "w3c-keyname": "^2.2.4" + } + }, "@coingecko/cryptoformat": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/@coingecko/cryptoformat/-/cryptoformat-0.5.4.tgz", @@ -49918,6 +50228,36 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "@lezer/common": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.0.2.tgz", + "integrity": "sha512-SVgiGtMnMnW3ActR8SXgsDhw7a0w0ChHSYAyAUxxrOiJ1OqYWEKk/xJd84tTSPo1mo6DXLObAJALNnd0Hrv7Ng==" + }, + "@lezer/highlight": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.1.3.tgz", + "integrity": "sha512-3vLKLPThO4td43lYRBygmMY18JN3CPh9w+XS2j8WC30vR4yZeFG4z1iFe4jXE43NtGqe//zHW5q8ENLlHvz9gw==", + "requires": { + "@lezer/common": "^1.0.0" + } + }, + "@lezer/json": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.0.tgz", + "integrity": "sha512-zbAuUY09RBzCoCA3lJ1+ypKw5WSNvLqGMtasdW6HvVOqZoCpPr8eWrsGnOVWGKGn8Rh21FnrKRVlJXrGAVUqRw==", + "requires": { + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "@lezer/lr": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.2.5.tgz", + "integrity": "sha512-f9319YG1A/3ysgUE3bqCHEd7g+3ZZ71MWlwEc42mpnLVYXgfJJgtu1XAyBB4Kz8FmqmnFe9caopDqKeMMMAU6g==", + "requires": { + "@lezer/common": "^1.0.0" + } + }, "@loadable/component": { "version": "5.15.2", "resolved": "https://registry.npmjs.org/@loadable/component/-/component-5.15.2.tgz", @@ -60444,6 +60784,44 @@ "eslint-visitor-keys": "^3.3.0" } }, + "@uiw/codemirror-extensions-basic-setup": { + "version": "4.19.5", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.19.5.tgz", + "integrity": "sha512-1zt7ZPJ01xKkSW/KDy0FZNga0bngN1fC594wCVG7FBi60ehfcAucpooQ+JSPScKXopxcb+ugPKZvVLzr9/OfzA==", + "requires": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + } + }, + "@uiw/codemirror-themes": { + "version": "4.19.5", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-themes/-/codemirror-themes-4.19.5.tgz", + "integrity": "sha512-BWCTwQJaGiOc+nYqPLQDjmCtIojaCEKx2aO1bOTyGw0fisKwGw9Csll+bi9ujqA+vk6qYJmXI0P5K7kVs8fbdA==", + "dev": true, + "requires": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + } + }, + "@uiw/react-codemirror": { + "version": "4.19.5", + "resolved": "https://registry.npmjs.org/@uiw/react-codemirror/-/react-codemirror-4.19.5.tgz", + "integrity": "sha512-ZCHh8d7beXbF8/t7F1+yHht6A9Y6CdKeOkZq4A09lxJEnyTQrj1FMf2zvfaqc7K23KNjkTCtSlbqKKbVDgrWaw==", + "requires": { + "@babel/runtime": "^7.18.6", + "@codemirror/commands": "^6.1.0", + "@codemirror/state": "^6.1.1", + "@codemirror/theme-one-dark": "^6.0.0", + "@uiw/codemirror-extensions-basic-setup": "4.19.5", + "codemirror": "^6.0.0" + } + }, "@urql/core": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@urql/core/-/core-3.0.3.tgz", @@ -64050,6 +64428,20 @@ "integrity": "sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==", "dev": true }, + "codemirror": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.1.tgz", + "integrity": "sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==", + "requires": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + } + }, "collapse-white-space": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-1.0.6.tgz", @@ -64778,6 +65170,11 @@ "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", "dev": true }, + "crelt": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.5.tgz", + "integrity": "sha512-+BO9wPPi+DWTDcNYhr/W90myha8ptzftZT+LwcmUbbok0rcP/fequmFYCw8NMoH7pkAZQzU78b3kYrlua5a9eA==" + }, "cross-env": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", @@ -80572,6 +80969,11 @@ "schema-utils": "^3.0.0" } }, + "style-mod": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.0.0.tgz", + "integrity": "sha512-OPhtyEjyyN9x3nhPsu76f52yUGXiZcgvsrFVtvTkyGRQJ0XK+GPc6ov1z+lRpbeabka+MYEQxOYRnt5nF30aMw==" + }, "style-to-object": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.3.0.tgz", @@ -82187,6 +82589,11 @@ "browser-process-hrtime": "^1.0.0" } }, + "w3c-keyname": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.6.tgz", + "integrity": "sha512-f+fciywl1SJEniZHD6H+kUO8gOnwIr7f4ijKA6+ZvJFjeGi1r4PDLl53Ayud9O/rk64RqgoQine0feoeOU0kXg==" + }, "w3c-xmlserializer": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-3.0.0.tgz", diff --git a/package.json b/package.json index cd257e70c..d84e2133e 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,8 @@ "storybook:build": "cross-env NODE_ENV=test build-storybook" }, "dependencies": { + "@codemirror/lang-json": "^6.0.1", + "@codemirror/language": "^6.3.1", "@coingecko/cryptoformat": "^0.5.4", "@loadable/component": "^5.15.2", "@oceanprotocol/art": "^3.2.0", @@ -30,6 +32,7 @@ "@oceanprotocol/typographies": "^0.1.0", "@oceanprotocol/use-dark-mode": "^2.4.3", "@tippyjs/react": "^4.2.6", + "@uiw/react-codemirror": "^4.19.5", "@urql/exchange-refocus": "^1.0.0", "@walletconnect/web3-provider": "^1.8.0", "axios": "^1.2.0", @@ -71,7 +74,7 @@ "yup": "^0.32.11" }, "devDependencies": { - "@storybook/addon-essentials": "^6.5.15", + "@storybook/addon-essentials": "^6.5.13", "@storybook/builder-webpack5": "^6.5.13", "@storybook/manager-webpack5": "^6.5.13", "@storybook/react": "^6.5.13", @@ -89,6 +92,7 @@ "@types/remove-markdown": "^0.3.1", "@typescript-eslint/eslint-plugin": "^5.43.0", "@typescript-eslint/parser": "^5.43.0", + "@uiw/codemirror-themes": "^4.19.1", "apollo": "^2.34.0", "cross-env": "^7.0.3", "eslint": "^8.35.0", diff --git a/src/@utils/codemirror.ts b/src/@utils/codemirror.ts new file mode 100644 index 000000000..b2ad4d694 --- /dev/null +++ b/src/@utils/codemirror.ts @@ -0,0 +1,36 @@ +import { createTheme } from '@uiw/codemirror-themes' +import { json } from '@codemirror/lang-json' + +export function checkJson(text: string) { + try { + JSON.parse(text) + return true + } catch (error) { + return false + } +} +declare type Theme = 'light' | 'dark' +export const oceanTheme = (marketTheme: Theme, field) => { + let textColor = 'var(--font-color-text)' + if ( + (field.name === 'files[0].abi' || + field.name === 'services[0].files[0].abi') && + !checkJson(field.value) + ) { + textColor = 'var(--brand-alert-red)' + } + + return createTheme({ + theme: marketTheme, + settings: { + background: 'var(--background-content)', + foreground: textColor, + selection: 'var(--background-highlight)', + lineHighlight: 'transparent', + gutterBackground: 'var(--background-body)', + gutterForeground: 'var(--font-color-text)' + }, + styles: [] + }) +} +export const extensions = [json()] diff --git a/src/@utils/ddo.ts b/src/@utils/ddo.ts index d477e201b..61e95b1a4 100644 --- a/src/@utils/ddo.ts +++ b/src/@utils/ddo.ts @@ -1,4 +1,20 @@ -import { Asset, DDO, Service } from '@oceanprotocol/lib' +import { + ComputeEditForm, + MetadataEditForm +} from '@components/Asset/Edit/_types' +import { FormPublishData } from '@components/Publish/_types' +import { + Arweave, + Asset, + DDO, + FileInfo, + GraphqlQuery, + Ipfs, + Service, + Smartcontract, + UrlFile +} from '@oceanprotocol/lib' +import { checkJson } from './codemirror' export function isValidDid(did: string): boolean { const regex = /did:op:[A-Za-z0-9]{64}/ @@ -70,3 +86,105 @@ export function secondsToString(numberOfSeconds: number): string { ? `${seconds} second${numberEnding(seconds)}` : 'less than a second' } + +// this is required to make it work properly for preview/publish/edit/debug. +// TODO: find a way to only have FileInfo interface instead of FileExtended +interface FileExtended extends FileInfo { + url?: string + query?: string + transactionId?: string + address?: string + abi?: string + headers?: { key: string; value: string }[] +} + +export function normalizeFile( + storageType: string, + file: FileExtended, + chainId: number +) { + let fileObj + const headersProvider = {} + const headers = file[0]?.headers || file?.headers + if (headers && headers.length > 0) { + headers.map((el) => { + headersProvider[el.key] = el.value + return el + }) + } + switch (storageType) { + case 'ipfs': { + fileObj = { + type: storageType, + hash: file[0]?.url || file?.url + } as Ipfs + break + } + case 'arweave': { + fileObj = { + type: storageType, + transactionId: + file[0]?.url || + file?.url || + file[0]?.transactionId || + file?.transactionId + } as Arweave + break + } + case 'graphql': { + fileObj = { + type: storageType, + url: file[0]?.url || file?.url, + query: file[0]?.query || file?.query, + headers: headersProvider + } as GraphqlQuery + break + } + case 'smartcontract': { + // clean obj + fileObj = { + chainId, + type: storageType, + address: file[0]?.address || file?.address || file[0]?.url || file?.url, + abi: checkJson(file[0]?.abi || file?.abi) + ? JSON.parse(file[0]?.abi || file?.abi) + : file[0]?.abi || file?.abi + } as Smartcontract + break + } + default: { + fileObj = { + type: 'url', + index: 0, + url: file ? file[0]?.url || file?.url : null, + headers: headersProvider, + method: file.method + } as UrlFile + break + } + } + return fileObj +} + +export function previewDebugPatch( + values: FormPublishData | Partial | ComputeEditForm, + chainId: number +) { + // handle file's object property dynamically + // without braking Yup and type validation + const buildValuesPreview = JSON.parse(JSON.stringify(values)) + + // fallback for edit mode under "edit compute settings" + if (!buildValuesPreview.services) return buildValuesPreview + + const valuesService = buildValuesPreview.services + ? buildValuesPreview.services[0] + : buildValuesPreview + valuesService.files[0] = normalizeFile( + valuesService.files[0].type, + valuesService.files[0], + chainId + ) + + return buildValuesPreview +} diff --git a/src/@utils/order.ts b/src/@utils/order.ts index c97c770d4..9f83eec48 100644 --- a/src/@utils/order.ts +++ b/src/@utils/order.ts @@ -22,6 +22,7 @@ import { consumeMarketFixedSwapFee } from '../../app.config' import { toast } from 'react-toastify' +import { getEncryptedFiles, getFileInfo } from './provider' async function initializeProvider( asset: AssetExtended, @@ -63,6 +64,11 @@ export async function order( const datatoken = new Datatoken(web3) const config = getOceanConfig(asset.chainId) + const filesEncrypted = await getEncryptedFiles( + asset.services[0].files, + asset.services[0].serviceEndpoint + ) + const initializeData = await initializeProvider( asset, accountId, diff --git a/src/@utils/provider.ts b/src/@utils/provider.ts index 92e30d62b..a33f4fccd 100644 --- a/src/@utils/provider.ts +++ b/src/@utils/provider.ts @@ -1,5 +1,7 @@ import { Arweave, + GraphqlQuery, + Smartcontract, ComputeAlgorithm, ComputeAsset, ComputeEnvironment, @@ -11,9 +13,24 @@ import { ProviderInstance, UrlFile } from '@oceanprotocol/lib' +import { QueryHeader } from '@shared/FormInput/InputElement/Headers' import Web3 from 'web3' +import { AbiItem } from 'web3-utils/types' import { getValidUntilTime } from './compute' +export async function getEncryptedFiles( + files: any, + providerUrl: string +): Promise { + try { + // https://github.com/oceanprotocol/provider/blob/v4main/API.md#encrypt-endpoint + const response = await ProviderInstance.encrypt(files, providerUrl) + return response + } catch (error) { + console.error('Error parsing json: ' + error.message) + } +} + export async function initializeProviderForCompute( dataset: AssetExtended, algorithm: AssetExtended, @@ -38,6 +55,11 @@ export async function initializeProviderForCompute( ) try { + const filesEncrypted = await getEncryptedFiles( + dataset.services[0].files, + dataset.services[0].serviceEndpoint + ) + return await ProviderInstance.initializeCompute( [computeAsset], computeAlgo, @@ -52,20 +74,6 @@ export async function initializeProviderForCompute( } } -// TODO: Why do we have these one line functions ?!?!?! -export async function getEncryptedFiles( - files: any, - providerUrl: string -): Promise { - try { - // https://github.com/oceanprotocol/provider/blob/v4main/API.md#encrypt-endpoint - const response = await ProviderInstance.encrypt(files, providerUrl) - return response - } catch (error) { - console.error('Error parsing json: ' + error.message) - } -} - export async function getFileDidInfo( did: string, serviceId: string, @@ -88,36 +96,73 @@ export async function getFileDidInfo( export async function getFileInfo( file: string, providerUrl: string, - storageType: string + storageType: string, + query?: string, + headers?: QueryHeader[], + abi?: string, + chainId?: number, + method?: string ): Promise { try { let response + const headersProvider = {} + if (headers?.length > 0) { + headers.map((el) => { + headersProvider[el.key] = el.value + return el + }) + } + switch (storageType) { case 'ipfs': { const fileIPFS: Ipfs = { - type: 'ipfs', + type: storageType, hash: file } - response = await ProviderInstance.getFileInfo(fileIPFS, providerUrl) - break } case 'arweave': { const fileArweave: Arweave = { - type: 'arweave', + type: storageType, transactionId: file } - response = await ProviderInstance.getFileInfo(fileArweave, providerUrl) break } + case 'graphql': { + const fileGraphql: GraphqlQuery = { + type: storageType, + url: file, + headers: headersProvider, + query + } + + response = await ProviderInstance.getFileInfo(fileGraphql, providerUrl) + break + } + case 'smartcontract': { + // clean obj + const fileSmartContract: Smartcontract = { + chainId, + type: storageType, + address: file, + abi: JSON.parse(abi) as AbiItem + } + + response = await ProviderInstance.getFileInfo( + fileSmartContract, + providerUrl + ) + break + } default: { const fileUrl: UrlFile = { type: 'url', index: 0, url: file, - method: 'get' + headers: headersProvider, + method } response = await ProviderInstance.getFileInfo(fileUrl, providerUrl) diff --git a/src/@utils/url/index.ts b/src/@utils/url/index.ts index 313ccdbbf..8722439ff 100644 --- a/src/@utils/url/index.ts +++ b/src/@utils/url/index.ts @@ -9,6 +9,7 @@ export function sanitizeUrl(url: string) { // check if the url is a google domain export const isGoogleUrl = (url: string): boolean => { if (!url || !isUrl(url)) return + const googleUrl = new URL(url) return googleUrl.hostname.endsWith('google.com') } diff --git a/src/@utils/yup.ts b/src/@utils/yup.ts index 6848e235a..f6f6ec7b6 100644 --- a/src/@utils/yup.ts +++ b/src/@utils/yup.ts @@ -1,6 +1,7 @@ import { isCID } from '@utils/ipfs' import isUrl from 'is-url-superb' import * as Yup from 'yup' +import web3 from 'web3' import { isGoogleUrl } from './url/index' export function testLinks(isEdit?: boolean) { @@ -15,16 +16,18 @@ export function testLinks(isEdit?: boolean) { validField = true break case 'url': + case 'graphql': validField = isUrl(value?.toString() || '') // if we're in publish, the field must be valid if (!validField) { validField = false errorMessage = 'Must be a valid url.' } - // we allow submit if we're in the edit page and the field is empty + // we allow submit on empty sample field if ( - (!value?.toString() && isEdit) || - (!value?.toString() && context.path === 'services[0].links[0].url') + !value?.toString() && + (context.path === 'links[0].url' || + context.path === 'services[0].links[0].url') ) { validField = true } @@ -40,11 +43,17 @@ export function testLinks(isEdit?: boolean) { errorMessage = !value?.toString() ? 'CID required.' : 'CID not valid.' break case 'arweave': - validField = !value?.toString().includes('http') + validField = value && !value?.toString().includes('http') errorMessage = !value?.toString() ? 'Transaction ID required.' : 'Transaction ID not valid.' break + case 'smartcontract': + validField = web3.utils.isAddress(value?.toString()) + errorMessage = !value?.toString() + ? 'Address required.' + : 'Address not valid.' + break } if (!validField) { diff --git a/src/components/@shared/FormInput/InputElement/FilesInput/index.module.css b/src/components/@shared/FormInput/InputElement/FilesInput/index.module.css new file mode 100644 index 000000000..9869c689f --- /dev/null +++ b/src/components/@shared/FormInput/InputElement/FilesInput/index.module.css @@ -0,0 +1,3 @@ +.textblock { + margin-top: var(--spacer); +} diff --git a/src/components/@shared/FormInput/InputElement/FilesInput/index.test.tsx b/src/components/@shared/FormInput/InputElement/FilesInput/index.test.tsx index d9281e4a7..89782a9d5 100644 --- a/src/components/@shared/FormInput/InputElement/FilesInput/index.test.tsx +++ b/src/components/@shared/FormInput/InputElement/FilesInput/index.test.tsx @@ -20,7 +20,7 @@ const mockMeta = { value: '' } -const mockField = { +const mockFieldUrl = { value: 'https://hello.com', checked: false, onChange: jest.fn(), @@ -28,6 +28,39 @@ const mockField = { name: 'url' } +const mockFieldIpfs = { + value: 'bafkreicxccbk4blsx5qtovqfgsuutxjxom47dvyzyz3asi2ggjg5ipwlc4', + checked: false, + onChange: jest.fn(), + onBlur: jest.fn(), + name: 'ipfs' +} + +const mockFieldArwave = { + value: 'T6NL8Zc0LCbT3bF9HacAGQC4W0_hW7b3tXbm8OtWtlA', + checked: false, + onChange: jest.fn(), + onBlur: jest.fn(), + name: 'arweave' +} + +const mockFieldGraphQL = { + value: + 'https://v4.subgraph.mumbai.oceanprotocol.com/subgraphs/name/oceanprotocol/ocean-subgraph', + checked: false, + onChange: jest.fn(), + onBlur: jest.fn(), + name: 'graphql' +} + +const mockFieldSM = { + value: '0x564955E9d25B49afE5Abd66966Ab4Bc9Ad55Fedb', + checked: false, + onChange: jest.fn(), + onBlur: jest.fn(), + name: 'smartcontract' +} + const mockHelpers = { setValue: jest.fn(), setTouched: jest.fn() @@ -35,7 +68,13 @@ const mockHelpers = { const mockForm = { values: { - services: [{ providerUrl: 'https://provider.url' }] + services: [ + { + providerUrl: { + url: 'https://v4.provider.mainnet.oceanprotocol.com' + } + } + ] }, errors: {}, touched: {}, @@ -46,8 +85,12 @@ const mockForm = { } describe('@shared/FormInput/InputElement/FilesInput', () => { - it('renders without crashing', async () => { - ;(useField as jest.Mock).mockReturnValue([mockField, mockMeta, mockHelpers]) + it('renders URL without crashing', async () => { + ;(useField as jest.Mock).mockReturnValue([ + mockFieldUrl, + mockMeta, + mockHelpers + ]) ;(checkValidProvider as jest.Mock).mockReturnValue(true) ;(getFileInfo as jest.Mock).mockReturnValue([ { @@ -59,7 +102,7 @@ describe('@shared/FormInput/InputElement/FilesInput', () => { } ]) - render() + render() expect(screen.getByText('Validate')).toBeInTheDocument() fireEvent.click(screen.getByText('Validate')) @@ -85,7 +128,7 @@ describe('@shared/FormInput/InputElement/FilesInput', () => { mockMeta, mockHelpers ]) - render() + render() expect(screen.getByText('https://hello.com')).toBeInTheDocument() }) @@ -106,7 +149,7 @@ describe('@shared/FormInput/InputElement/FilesInput', () => { mockMeta, mockHelpers ]) - render() + render() expect(screen.getByText('✓ File confirmed')).toBeInTheDocument() }) @@ -127,28 +170,10 @@ describe('@shared/FormInput/InputElement/FilesInput', () => { mockMeta, mockHelpers ]) - render() + render() expect(screen.getByText('✓ File confirmed')).toBeInTheDocument() }) - it('renders fileinfo without contentType', () => { - ;(useField as jest.Mock).mockReturnValue([ - { - value: [ - { - valid: true, - url: 'https://hello.com', - type: 'url', - contentLength: 100 - } - ] - }, - mockMeta, - mockHelpers - ]) - render() - }) - it('renders fileinfo placeholder when hideUrl is passed', () => { ;(useField as jest.Mock).mockReturnValue([ { @@ -163,9 +188,60 @@ describe('@shared/FormInput/InputElement/FilesInput', () => { mockMeta, mockHelpers ]) - render() + render() expect( screen.getByText('https://oceanprotocol/placeholder') ).toBeInTheDocument() }) + + it('renders fileinfo when graphql is valid', () => { + ;(useField as jest.Mock).mockReturnValue([ + { + value: [ + { + type: 'graphql', + valid: true, + url: 'https://v4.subgraph.mumbai.oceanprotocol.com/subgraphs/name/oceanprotocol/ocean-subgraph', + query: + 'query{\n nfts(orderBy: createdTimestamp,orderDirection:desc){\n id\n symbol\n createdTimestamp\n }\n }', + checksum: false + } + ] + }, + mockMeta, + mockHelpers + ]) + render() + }) + + it('renders fileinfo when smart contract is valid', () => { + ;(useField as jest.Mock).mockReturnValue([ + { + value: [ + { + chainId: 80001, + type: 'smartcontract', + address: '0x564955E9d25B49afE5Abd66966Ab4Bc9Ad55Fedb', + abi: { + inputs: [], + name: 'swapOceanFee', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256' + } + ], + stateMutability: 'view', + type: 'function' + }, + valid: true + } + ] + }, + mockMeta, + mockHelpers + ]) + render() + }) }) diff --git a/src/components/@shared/FormInput/InputElement/FilesInput/index.tsx b/src/components/@shared/FormInput/InputElement/FilesInput/index.tsx index 7d4094617..f91064fdf 100644 --- a/src/components/@shared/FormInput/InputElement/FilesInput/index.tsx +++ b/src/components/@shared/FormInput/InputElement/FilesInput/index.tsx @@ -1,23 +1,37 @@ import React, { ReactElement, useEffect, useState } from 'react' -import { useField } from 'formik' -import FileInfo from './Info' +import { Field, useField } from 'formik' +import FileInfoDetails from './Info' import UrlInput from '../URLInput' -import { InputProps } from '@shared/FormInput' +import Input, { InputProps } from '@shared/FormInput' import { getFileInfo, checkValidProvider } from '@utils/provider' -import { LoggerInstance } from '@oceanprotocol/lib' +import { LoggerInstance, FileInfo } from '@oceanprotocol/lib' import { useAsset } from '@context/Asset' +import styles from './index.module.css' +import { useWeb3 } from '@context/Web3' +import InputHeaders from '../Headers' +import Button from '@shared/atoms/Button' +import Loader from '@shared/atoms/Loader' +import { checkJson } from '@utils/codemirror' import { isGoogleUrl } from '@utils/url/index' +import isUrl from 'is-url-superb' +import MethodInput from '../MethodInput' export default function FilesInput(props: InputProps): ReactElement { const [field, meta, helpers] = useField(props.name) const [isLoading, setIsLoading] = useState(false) + const [disabledButton, setDisabledButton] = useState(true) const { asset } = useAsset() + const { chainId } = useWeb3() const providerUrl = props.form?.values?.services ? props.form?.values?.services[0].providerUrl.url : asset.services[0].serviceEndpoint const storageType = field.value[0].type + const query = field.value[0].query || undefined + const abi = field.value[0].abi || undefined + const headers = field.value[0].headers || undefined + const method = field.value[0].method || undefined async function handleValidation(e: React.SyntheticEvent, url: string) { // File example 'https://oceanprotocol.com/tech-whitepaper.pdf' @@ -26,8 +40,7 @@ export default function FilesInput(props: InputProps): ReactElement { try { setIsLoading(true) - // TODO: handled on provider - if (isGoogleUrl(url)) { + if (isUrl(url) && isGoogleUrl(url)) { throw Error( 'Google Drive is not a supported hosting service. Please use an alternative.' ) @@ -40,17 +53,39 @@ export default function FilesInput(props: InputProps): ReactElement { '✗ Provider cannot be reached, please check status.oceanprotocol.com and try again later.' ) - const checkedFile = await getFileInfo(url, providerUrl, storageType) + const checkedFile = await getFileInfo( + url, + providerUrl, + storageType, + query, + headers, + abi, + chainId, + method + ) // error if something's not right from response if (!checkedFile) throw Error('Could not fetch file info. Is your network down?') if (checkedFile[0].valid === false) - throw Error('✗ No valid file detected. Check your URL and try again.') + throw Error( + `✗ No valid file detected. Check your ${props.label} and details, and try again.` + ) // if all good, add file to formik state - helpers.setValue([{ url, type: storageType, ...checkedFile[0] }]) + helpers.setValue([ + { + url, + providerUrl, + type: storageType, + query, + headers, + abi, + chainId, + ...checkedFile[0] + } + ]) } catch (error) { props.form.setFieldError(`${field.name}[0].url`, error.message) LoggerInstance.error(error.message) @@ -59,6 +94,10 @@ export default function FilesInput(props: InputProps): ReactElement { } } + async function handleMethod(method: string) { + helpers.setValue([{ ...props.value[0], method }]) + } + function handleClose() { helpers.setTouched(false) helpers.setValue([ @@ -66,21 +105,95 @@ export default function FilesInput(props: InputProps): ReactElement { ]) } + useEffect(() => { + storageType === 'graphql' && setDisabledButton(!providerUrl || !query) + + storageType === 'smartcontract' && + setDisabledButton(!providerUrl || !abi || !checkJson(abi)) + + storageType === 'url' && setDisabledButton(!providerUrl) + + if (meta.error?.length > 0) { + const { url } = meta.error[0] as unknown as FileInfo + url && setDisabledButton(true) + } + }, [storageType, providerUrl, headers, query, abi, meta]) + return ( <> {field?.value?.[0]?.valid === true || field?.value?.[0]?.type === 'hidden' ? ( - + ) : ( - + <> + {props.methods && storageType === 'url' ? ( + + ) : ( + + )} + + {props.innerFields && ( + <> +
+ {props.innerFields && + props.innerFields.map((innerField: any, i: number) => { + return ( + <> + + + ) + })} +
+ + + + )} + )} ) diff --git a/src/components/@shared/FormInput/InputElement/Headers/index.module.css b/src/components/@shared/FormInput/InputElement/Headers/index.module.css new file mode 100644 index 000000000..99dba4c0c --- /dev/null +++ b/src/components/@shared/FormInput/InputElement/Headers/index.module.css @@ -0,0 +1,13 @@ +.headersContainer { + display: flex; + justify-content: space-between; + gap: 0 calc(var(--spacer) / 4); + margin-bottom: var(--spacer); +} + +.headersAddedContainer { + display: flex; + justify-content: space-between; + gap: 0 calc(var(--spacer) / 4); + margin-bottom: var(--spacer); +} diff --git a/src/components/@shared/FormInput/InputElement/Headers/index.tsx b/src/components/@shared/FormInput/InputElement/Headers/index.tsx new file mode 100644 index 000000000..362ffd449 --- /dev/null +++ b/src/components/@shared/FormInput/InputElement/Headers/index.tsx @@ -0,0 +1,135 @@ +import React, { ChangeEvent, ReactElement, useEffect, useState } from 'react' +import InputElement from '../../InputElement' +import Label from '../../Label' +import styles from './index.module.css' +import Tooltip from '@shared/atoms/Tooltip' +import Markdown from '@shared/Markdown' +import Button from '@shared/atoms/Button' +import { InputProps } from '@shared/FormInput' + +export interface QueryHeader { + key: string + value: string +} + +export default function InputHeaders(props: InputProps): ReactElement { + const { label, help, prominentHelp, form, field } = props + + const [currentKey, setCurrentKey] = useState('') + const [currentValue, setCurrentValue] = useState('') + const [disabledButton, setDisabledButton] = useState(true) + + const [headers, setHeaders] = useState([] as QueryHeader[]) + + const addHeader = () => { + setHeaders((prev) => [ + ...prev, + { + key: currentKey, + value: currentValue + } + ]) + setCurrentKey('') + setCurrentValue('') + } + + const removeHeader = (i: number) => { + const newHeaders = headers.filter((header, index) => index !== i) + setHeaders(newHeaders) + setCurrentKey('') + setCurrentValue('') + } + + function handleChange(e: ChangeEvent) { + const checkType = e.target.name.search('key') + checkType > 0 + ? setCurrentKey(e.target.value) + : setCurrentValue(e.target.value) + + return e + } + + useEffect(() => { + form.setFieldValue(`${field.name}`, headers) + }, [headers]) + + useEffect(() => { + setDisabledButton(!currentKey || !currentValue) + }, [currentKey, currentValue]) + + return ( +
+ + +
+ + + + + +
+ + {headers.length > 0 && + headers.map((header, i) => { + return ( +
+ + + + + +
+ ) + })} +
+ ) +} diff --git a/src/components/@shared/FormInput/InputElement/MethodInput/index.module.css b/src/components/@shared/FormInput/InputElement/MethodInput/index.module.css new file mode 100644 index 000000000..cf5adad6c --- /dev/null +++ b/src/components/@shared/FormInput/InputElement/MethodInput/index.module.css @@ -0,0 +1,44 @@ +.input { + composes: input from '@shared/FormInput/InputElement/index.module.css'; +} + +.inputMethod { + composes: input from '@shared/FormInput/InputElement/index.module.css'; + cursor: pointer; + outline: 0; + margin: 0; + display: inline-block; + min-width: 7rem; + padding: calc(var(--spacer) / 3) var(--spacer); + font-size: var(--font-size-base); + font-family: var(--font-family-base); + font-weight: var(--font-weight-bold); + text-transform: uppercase; + border-radius: var(--border-radius); + transition: 0.2s ease-out; + color: var(--brand-white); + box-shadow: 0 9px 18px 0 rgb(0 0 0 / 10%); + -webkit-user-select: none; + user-select: none; + text-align: center; + border-top-right-radius: var(--border-radius); + border-top-left-radius: 0; + border-bottom-left-radius: 0; + margin-top: 0; + margin-left: -1px; + width: -webkit-fit-content; + width: -moz-fit-content; + width: fit-content; + white-space: nowrap; + background: var(--brand-gradient); + border: 0; +} + +.hasError { + color: var(--brand-alert-red); + border-color: var(--brand-alert-red); +} + +.error { + composes: error from '@shared/FormInput/index.module.css'; +} diff --git a/src/components/@shared/FormInput/InputElement/MethodInput/index.test.tsx b/src/components/@shared/FormInput/InputElement/MethodInput/index.test.tsx new file mode 100644 index 000000000..7f9ffd15e --- /dev/null +++ b/src/components/@shared/FormInput/InputElement/MethodInput/index.test.tsx @@ -0,0 +1,58 @@ +import { fireEvent, render, screen } from '@testing-library/react' +import React from 'react' +import MethodInput, { MethodInputProps } from './index' +import { useField } from 'formik' + +jest.mock('formik') + +const props: MethodInputProps = { + handleButtonClick: jest.fn(), + isLoading: false, + name: 'Hello Name' +} + +const mockMeta = { + touched: false, + error: '', + initialError: '', + initialTouched: false, + initialValue: '', + value: '' +} + +describe('@shared/FormInput/InputElement/MethodInput', () => { + it('renders without crashing', () => { + const mockField = { + value: '', + checked: false, + onChange: jest.fn(), + onBlur: jest.fn(), + name: 'url', + method: 'get' + } + ;(useField as jest.Mock).mockReturnValue([mockField, mockMeta]) + + render() + expect(screen.getByRole('textbox')).toBeInTheDocument() + + fireEvent.change(screen.getByRole('textbox'), { + target: { value: 'https://google.com' } + }) + }) + + it('renders button enabled with value', () => { + const mockField = { + value: 'https://google.com', + checked: false, + onChange: jest.fn(), + onBlur: jest.fn(), + name: 'url', + method: 'get' + } + ;(useField as jest.Mock).mockReturnValue([mockField, mockMeta]) + + render() + expect(screen.getByRole('textbox')).toBeInTheDocument() + fireEvent.click(screen.getByRole('textbox')) + }) +}) diff --git a/src/components/@shared/FormInput/InputElement/MethodInput/index.tsx b/src/components/@shared/FormInput/InputElement/MethodInput/index.tsx new file mode 100644 index 000000000..3e97d491a --- /dev/null +++ b/src/components/@shared/FormInput/InputElement/MethodInput/index.tsx @@ -0,0 +1,69 @@ +import React, { ReactElement, useEffect, useState } from 'react' +import Button from '@shared/atoms/Button' +import { ErrorMessage, useField } from 'formik' +import Loader from '@shared/atoms/Loader' +import styles from './index.module.css' +import InputGroup from '@shared/FormInput/InputGroup' +import InputElement from '@shared/FormInput/InputElement' +import isUrl from 'is-url-superb' +import { isCID } from '@utils/ipfs' + +export interface MethodInputProps { + handleButtonClick(method: string): void + isLoading: boolean + name: string + checkUrl?: boolean + storageType?: string + hideButton?: boolean +} + +export default function MethodInput({ + handleButtonClick, + isLoading, + name, + checkUrl, + storageType, + ...props +}: MethodInputProps): ReactElement { + const [field, meta] = useField(name) + const [methodSelected, setMethod] = useState(field?.value[0]?.method || 'get') + + return ( + <> + + + + { + setMethod(e.currentTarget.value) + handleButtonClick(e.currentTarget.value) + }} + type="select" + options={['get', 'post']} + /> + + + {meta.touched && meta.error && ( +
+ +
+ )} + + ) +} diff --git a/src/components/@shared/FormInput/InputElement/URLInput/index.tsx b/src/components/@shared/FormInput/InputElement/URLInput/index.tsx index dafd5b52e..44df17d79 100644 --- a/src/components/@shared/FormInput/InputElement/URLInput/index.tsx +++ b/src/components/@shared/FormInput/InputElement/URLInput/index.tsx @@ -7,7 +7,7 @@ import InputGroup from '@shared/FormInput/InputGroup' import InputElement from '@shared/FormInput/InputElement' import isUrl from 'is-url-superb' import { isCID } from '@utils/ipfs' - +import web3 from 'web3' export interface URLInputProps { submitText: string handleButtonClick(e: React.SyntheticEvent, data: string): void @@ -15,6 +15,7 @@ export interface URLInputProps { name: string checkUrl?: boolean storageType?: string + hideButton?: boolean } export default function URLInput({ @@ -24,11 +25,12 @@ export default function URLInput({ name, checkUrl, storageType, + hideButton, ...props }: URLInputProps): ReactElement { const [field, meta] = useField(name) const [isButtonDisabled, setIsButtonDisabled] = useState(true) - + const inputValues = (props as any)?.value useEffect(() => { if (!field?.value) return @@ -37,10 +39,15 @@ export default function URLInput({ field.value === '' || (checkUrl && storageType === 'url' && !isUrl(field.value)) || (checkUrl && storageType === 'ipfs' && !isCID(field.value)) || + (checkUrl && + storageType === 'graphql' && + !isCID(field.value) && + !inputValues[0]?.query) || field.value.includes('javascript:') || + (storageType === 'smartcontract' && !inputValues[0]?.abi) || meta?.error ) - }, [field?.value, meta?.error]) + }, [field?.value, meta?.error, inputValues]) return ( <> @@ -56,17 +63,19 @@ export default function URLInput({ type="url" /> - + {!hideButton && ( + + )} {meta.touched && meta.error && ( diff --git a/src/components/@shared/FormInput/InputElement/index.module.css b/src/components/@shared/FormInput/InputElement/index.module.css index 6fdbf9584..5c52b57ee 100644 --- a/src/components/@shared/FormInput/InputElement/index.module.css +++ b/src/components/@shared/FormInput/InputElement/index.module.css @@ -47,7 +47,7 @@ display: none; } -.textarea { +.codemirror, .textarea { composes: input; height: auto; } diff --git a/src/components/@shared/FormInput/InputElement/index.tsx b/src/components/@shared/FormInput/InputElement/index.tsx index 9430b5d74..c21e29ba7 100644 --- a/src/components/@shared/FormInput/InputElement/index.tsx +++ b/src/components/@shared/FormInput/InputElement/index.tsx @@ -1,4 +1,5 @@ -import React, { ReactElement, useCallback, useState } from 'react' +import React, { ReactElement } from 'react' +import CodeMirror from '@uiw/react-codemirror' import styles from './index.module.css' import { InputProps } from '..' import FilesInput from './FilesInput' @@ -12,6 +13,9 @@ import InputRadio from './Radio' import ContainerInput from '@shared/FormInput/InputElement/ContainerInput' import TagsAutoComplete from './TagsAutoComplete' import TabsFile from '@shared/atoms/TabsFile' +import useDarkMode from '@oceanprotocol/use-dark-mode' +import appConfig from '../../../../../app.config' +import { extensions, oceanTheme } from '@utils/codemirror' const cx = classNames.bind(styles) @@ -56,6 +60,7 @@ export default function InputElement({ ...props }: InputProps): ReactElement { const styleClasses = cx({ select: true, [size]: size }) + const darkMode = useDarkMode(false, appConfig?.darkModeConfig) switch (props.type) { case 'select': { @@ -100,8 +105,31 @@ export default function InputElement({ }) }) - return + return ( + + ) } + + case 'codeeditor': + return ( + { + form.setFieldValue(`${props.name}`, value) + }} + /> + ) + case 'textarea': return