Merge branch 'master' into users/languye/improve-filter-view

This commit is contained in:
Laurent Nguyen 2024-04-30 20:00:51 +02:00
commit 1f4d0f21f0
37 changed files with 584 additions and 2481 deletions

View File

@ -76,6 +76,10 @@ module.exports = {
"^dnd-core$": "dnd-core/dist/cjs",
"^react-dnd$": "react-dnd/dist/cjs",
"^react-dnd-html5-backend$": "react-dnd-html5-backend/dist/cjs",
"d3-force": "<rootDir>/node_modules/d3-force/dist/d3-force.min.js",
"d3-quadtree": "<rootDir>/node_modules/d3-quadtree/dist/d3-quadtree.min.js",
"d3-scale-chromatic": "<rootDir>/node_modules/d3-scale-chromatic/dist/d3-scale-chromatic.min.js",
"d3-zoom": "<rootDir>/node_modules/d3-zoom/dist/d3-zoom.min.js",
},
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
@ -130,7 +134,6 @@ module.exports = {
// The test environment that will be used for testing
// testEnvironment: "jest-environment-jsdom",
modulePaths: ["node_modules", "<rootDir>/src"],
// Options that will be passed to the testEnvironment

582
package-lock.json generated
View File

@ -61,7 +61,7 @@
"copy-webpack-plugin": "11.0.0",
"crossroads": "0.12.2",
"css-element-queries": "1.1.1",
"d3": "6.1.1",
"d3": "7.8.5",
"datatables.net-colreorder-dt": "1.7.0",
"datatables.net-dt": "1.13.8",
"date-fns": "1.29.0",
@ -194,14 +194,14 @@
"sinon": "3.2.1",
"style-loader": "0.23.0",
"ts-loader": "9.2.4",
"typedoc": "0.21.5",
"typedoc": "0.22.15",
"typescript": "4.3.5",
"url-loader": "4.1.1",
"wait-on": "4.0.2",
"webpack": "5.88.2",
"webpack-bundle-analyzer": "4.9.1",
"webpack-cli": "5.1.4",
"webpack-dev-server": "4.15.1"
"webpack-dev-server": "4.15.2"
}
},
"canvas": {
@ -17283,40 +17283,43 @@
}
},
"node_modules/d3": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/d3/-/d3-6.1.1.tgz",
"integrity": "sha512-bJYW9wlS2uvP2EoMkcPptrUzLMHQKCbiSW+/la8iGSLZgs4KbI/f3Fch4RtnUA9PA+/nPlwyFYzTwDjX80Of8w==",
"version": "7.8.5",
"resolved": "https://registry.npmjs.org/d3/-/d3-7.8.5.tgz",
"integrity": "sha512-JgoahDG51ncUfJu6wX/1vWQEqOflgXyl4MaHqlcSruTez7yhaRKR9i8VjjcQGeS2en/jnFivXuaIMnseMMt0XA==",
"dependencies": {
"d3-array": "2",
"d3-axis": "2",
"d3-brush": "2",
"d3-chord": "2",
"d3-color": "2",
"d3-contour": "2",
"d3-delaunay": "5",
"d3-dispatch": "2",
"d3-drag": "2",
"d3-dsv": "2",
"d3-ease": "2",
"d3-fetch": "2",
"d3-force": "2",
"d3-format": "2",
"d3-geo": "2",
"d3-hierarchy": "2",
"d3-interpolate": "2",
"d3-path": "2",
"d3-polygon": "2",
"d3-quadtree": "2",
"d3-random": "2",
"d3-scale": "3",
"d3-scale-chromatic": "2",
"d3-selection": "2",
"d3-shape": "2",
"d3-time": "2",
"d3-time-format": "3",
"d3-timer": "2",
"d3-transition": "2",
"d3-zoom": "2"
"d3-array": "3",
"d3-axis": "3",
"d3-brush": "3",
"d3-chord": "3",
"d3-color": "3",
"d3-contour": "4",
"d3-delaunay": "6",
"d3-dispatch": "3",
"d3-drag": "3",
"d3-dsv": "3",
"d3-ease": "3",
"d3-fetch": "3",
"d3-force": "3",
"d3-format": "3",
"d3-geo": "3",
"d3-hierarchy": "3",
"d3-interpolate": "3",
"d3-path": "3",
"d3-polygon": "3",
"d3-quadtree": "3",
"d3-random": "3",
"d3-scale": "4",
"d3-scale-chromatic": "3",
"d3-selection": "3",
"d3-shape": "3",
"d3-time": "3",
"d3-time-format": "4",
"d3-timer": "3",
"d3-transition": "3",
"d3-zoom": "3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-array": {
@ -17328,9 +17331,12 @@
}
},
"node_modules/d3-axis": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-2.1.0.tgz",
"integrity": "sha512-z/G2TQMyuf0X3qP+Mh+2PimoJD41VOCjViJzT0BHeL/+JQAofkiWZbWxlwFGb1N8EN+Cl/CW+MUKbVzr1689Cw=="
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz",
"integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-brush": {
"version": "2.1.0",
@ -17345,11 +17351,14 @@
}
},
"node_modules/d3-chord": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-2.0.0.tgz",
"integrity": "sha512-D5PZb7EDsRNdGU4SsjQyKhja8Zgu+SHZfUSO5Ls8Wsn+jsAKUUGkcshLxMg9HDFxG3KqavGWaWkJ8EpU8ojuig==",
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz",
"integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==",
"dependencies": {
"d3-path": "1 - 2"
"d3-path": "1 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-collection": {
@ -17363,19 +17372,36 @@
"integrity": "sha512-SPXi0TSKPD4g9tw0NMZFnR95XVgUZiBH+uUTqQuDu1OsE2zomHU7ho0FISciaPvosimixwHFl3WHLGabv6dDgQ=="
},
"node_modules/d3-contour": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-2.0.0.tgz",
"integrity": "sha512-9unAtvIaNk06UwqBmvsdHX7CZ+NPDZnn8TtNH1myW93pWJkhsV25JcgnYAu0Ck5Veb1DHiCv++Ic5uvJ+h50JA==",
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz",
"integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==",
"dependencies": {
"d3-array": "2"
"d3-array": "^3.2.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-contour/node_modules/d3-array": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
"integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
"dependencies": {
"internmap": "1 - 2"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-delaunay": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-5.3.0.tgz",
"integrity": "sha512-amALSrOllWVLaHTnDLHwMIiz0d1bBu9gZXd1FiLfXf8sHcX9jrcj81TVZOqD4UX7MgBZZ07c8GxzEgBpJqc74w==",
"version": "6.0.4",
"resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz",
"integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==",
"dependencies": {
"delaunator": "4"
"delaunator": "5"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-dispatch": {
@ -17393,24 +17419,46 @@
}
},
"node_modules/d3-dsv": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-2.0.0.tgz",
"integrity": "sha512-E+Pn8UJYx9mViuIUkoc93gJGGYut6mSDKy2+XaPwccwkRGlR+LO97L2VCCRjQivTwLHkSnAJG7yo00BWY6QM+w==",
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz",
"integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==",
"dependencies": {
"commander": "2",
"iconv-lite": "0.4",
"commander": "7",
"iconv-lite": "0.6",
"rw": "1"
},
"bin": {
"csv2json": "bin/dsv2json",
"csv2tsv": "bin/dsv2dsv",
"dsv2dsv": "bin/dsv2dsv",
"dsv2json": "bin/dsv2json",
"json2csv": "bin/json2dsv",
"json2dsv": "bin/json2dsv",
"json2tsv": "bin/json2dsv",
"tsv2csv": "bin/dsv2dsv",
"tsv2json": "bin/dsv2json"
"csv2json": "bin/dsv2json.js",
"csv2tsv": "bin/dsv2dsv.js",
"dsv2dsv": "bin/dsv2dsv.js",
"dsv2json": "bin/dsv2json.js",
"json2csv": "bin/json2dsv.js",
"json2dsv": "bin/json2dsv.js",
"json2tsv": "bin/json2dsv.js",
"tsv2csv": "bin/dsv2dsv.js",
"tsv2json": "bin/dsv2json.js"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-dsv/node_modules/commander": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
"integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
"engines": {
"node": ">= 10"
}
},
"node_modules/d3-dsv/node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/d3-ease": {
@ -17419,21 +17467,27 @@
"integrity": "sha512-68/n9JWarxXkOWMshcT5IcjbB+agblQUaIsbnXmrzejn2O82n3p2A9R2zEB9HIEFWKFwPAEDDN8gR0VdSAyyAQ=="
},
"node_modules/d3-fetch": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-2.0.0.tgz",
"integrity": "sha512-TkYv/hjXgCryBeNKiclrwqZH7Nb+GaOwo3Neg24ZVWA3MKB+Rd+BY84Nh6tmNEMcjUik1CSUWjXYndmeO6F7sw==",
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz",
"integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==",
"dependencies": {
"d3-dsv": "1 - 2"
"d3-dsv": "1 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-force": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/d3-force/-/d3-force-2.1.1.tgz",
"integrity": "sha512-nAuHEzBqMvpFVMf9OX75d00OxvOXdxY+xECIXjW6Gv8BRrXu6gAWbv/9XKrvfJ5i5DCokDW7RYE50LRoK092ew==",
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz",
"integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==",
"dependencies": {
"d3-dispatch": "1 - 2",
"d3-quadtree": "1 - 2",
"d3-timer": "1 - 2"
"d3-dispatch": "1 - 3",
"d3-quadtree": "1 - 3",
"d3-timer": "1 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-format": {
@ -17442,11 +17496,14 @@
"integrity": "sha512-Ab3S6XuE/Q+flY96HXT0jOXcM4EAClYFnRGY5zsjRGNy6qCYrQsMffs7cV5Q9xejb35zxW5hf/guKw34kvIKsA=="
},
"node_modules/d3-geo": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-2.0.2.tgz",
"integrity": "sha512-8pM1WGMLGFuhq9S+FpPURxic+gKzjluCD/CHTuUF3mXMeiCo0i6R0tO1s4+GArRFde96SLcW/kOFRjoAosPsFA==",
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz",
"integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==",
"dependencies": {
"d3-array": "^2.5.0"
"d3-array": "2.5.0 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-glyphedge": {
@ -17460,9 +17517,12 @@
"integrity": "sha512-KS3fUT2ReD4RlGCjvCEm1RgMtp2NFZumdMu4DBzQK8AZv3fXRM6Xm8I4fSU07UXvH4xxg03NwWKWdvxfS/yc4w=="
},
"node_modules/d3-hierarchy": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-2.0.0.tgz",
"integrity": "sha512-SwIdqM3HxQX2214EG9GTjgmCc/mbSx4mQBn+DuEETubhOw6/U3fmnji4uCVrmzOydMHSO1nZle5gh6HB/wdOzw=="
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz",
"integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-interpolate": {
"version": "2.0.1",
@ -17473,9 +17533,12 @@
}
},
"node_modules/d3-path": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/d3-path/-/d3-path-2.0.0.tgz",
"integrity": "sha512-ZwZQxKhBnv9yHaiWd6ZU4x5BtCQ7pXszEV9CU6kRgwIQVQGLMv1oiL4M+MK/n79sYzsj+gcgpPQSctJUsLN7fA=="
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
"integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-path-arrows": {
"version": "0.4.0",
@ -17497,19 +17560,28 @@
"integrity": "sha512-SJ0BqYihzOjDnnlfyeHT0e30k0K1+5sR3d5fNueCNeuhZTnGw4M4o8mqJchSwgKMXCNFo+e2VTChiSJ0vYtXkg=="
},
"node_modules/d3-polygon": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-2.0.0.tgz",
"integrity": "sha512-MsexrCK38cTGermELs0cO1d79DcTsQRN7IWMJKczD/2kBjzNXxLUWP33qRF6VDpiLV/4EI4r6Gs0DAWQkE8pSQ=="
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz",
"integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-quadtree": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-2.0.0.tgz",
"integrity": "sha512-b0Ed2t1UUalJpc3qXzKi+cPGxeXRr4KU9YSlocN74aTzp6R/Ud43t79yLLqxHRWZfsvWXmbDWPpoENK1K539xw=="
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz",
"integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-random": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/d3-random/-/d3-random-2.2.2.tgz",
"integrity": "sha512-0D9P8TRj6qDAtHhRQn6EfdOtHMfsUWanl3yb/84C4DqpZ+VsgfI5iTVRNRbELCfNvRfpMr8OrqqUTQ6ANGCijw=="
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz",
"integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-sankey-circular": {
"version": "0.25.0",
@ -17539,12 +17611,15 @@
}
},
"node_modules/d3-scale-chromatic": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-2.0.0.tgz",
"integrity": "sha512-LLqy7dJSL8yDy7NRmf6xSlsFZ6zYvJ4BcWFE4zBrOPnQERv9zj24ohnXKRbyi9YHnYV+HN1oEO3iFK971/gkzA==",
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
"integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==",
"dependencies": {
"d3-color": "1 - 2",
"d3-interpolate": "1 - 2"
"d3-color": "1 - 3",
"d3-interpolate": "1 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-selection": {
@ -17607,23 +17682,181 @@
"integrity": "sha512-dArJ32hchFsrQ8uMiTBLq256MpnZjeuBtdHpaDlYuQyjU0CVzCJl/BVW+SkszaAeH95D/8gxqAhgx0ouAWAfRg=="
},
"node_modules/d3-zoom": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-2.0.0.tgz",
"integrity": "sha512-fFg7aoaEm9/jf+qfstak0IYpnesZLiMX6GZvXtUSdv8RH2o4E2qeelgdU09eKS6wGuiGMfcnMI0nTIqWzRHGpw==",
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz",
"integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
"dependencies": {
"d3-dispatch": "1 - 2",
"d3-drag": "2",
"d3-interpolate": "1 - 2",
"d3-selection": "2",
"d3-transition": "2"
"d3-dispatch": "1 - 3",
"d3-drag": "2 - 3",
"d3-interpolate": "1 - 3",
"d3-selection": "2 - 3",
"d3-transition": "2 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3/node_modules/d3-array": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
"integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
"dependencies": {
"internmap": "1 - 2"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3/node_modules/d3-brush": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz",
"integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==",
"dependencies": {
"d3-dispatch": "1 - 3",
"d3-drag": "2 - 3",
"d3-interpolate": "1 - 3",
"d3-selection": "3",
"d3-transition": "3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3/node_modules/d3-color": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
"engines": {
"node": ">=12"
}
},
"node_modules/d3/node_modules/d3-dispatch": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
"integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
"engines": {
"node": ">=12"
}
},
"node_modules/d3/node_modules/d3-drag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz",
"integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
"dependencies": {
"d3-dispatch": "1 - 3",
"d3-selection": "3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3/node_modules/d3-ease": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
"integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
"engines": {
"node": ">=12"
}
},
"node_modules/d3/node_modules/d3-format": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz",
"integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==",
"engines": {
"node": ">=12"
}
},
"node_modules/d3/node_modules/d3-interpolate": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
"dependencies": {
"d3-color": "1 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3/node_modules/d3-scale": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
"integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
"dependencies": {
"d3-array": "2.10.0 - 3",
"d3-format": "1 - 3",
"d3-interpolate": "1.2.0 - 3",
"d3-time": "2.1.1 - 3",
"d3-time-format": "2 - 4"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3/node_modules/d3-selection": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
"engines": {
"node": ">=12"
}
},
"node_modules/d3/node_modules/d3-shape": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-2.1.0.tgz",
"integrity": "sha512-PnjUqfM2PpskbSLTJvAzp2Wv4CZsnAgTfcVRTwW03QR3MkXF8Uo7B1y/lWkAsmbKwuecto++4NlsYcvYpXpTHA==",
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
"integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
"dependencies": {
"d3-path": "1 - 2"
"d3-path": "^3.1.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3/node_modules/d3-time": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
"integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
"dependencies": {
"d3-array": "2 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3/node_modules/d3-time-format": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
"integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
"dependencies": {
"d3-time": "1 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3/node_modules/d3-timer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
"integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
"engines": {
"node": ">=12"
}
},
"node_modules/d3/node_modules/d3-transition": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz",
"integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
"dependencies": {
"d3-color": "1 - 3",
"d3-dispatch": "1 - 3",
"d3-ease": "1 - 3",
"d3-interpolate": "1 - 3",
"d3-timer": "1 - 3"
},
"engines": {
"node": ">=12"
},
"peerDependencies": {
"d3-selection": "2 - 3"
}
},
"node_modules/dashdash": {
@ -18045,9 +18278,12 @@
}
},
"node_modules/delaunator": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/delaunator/-/delaunator-4.0.1.tgz",
"integrity": "sha512-WNPWi1IRKZfCt/qIDMfERkDp93+iZEmOxN2yy4Jg+Xhv8SLk2UTqqbe1sfiipn0and9QrE914/ihdx82Y/Giag=="
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz",
"integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==",
"dependencies": {
"robust-predicates": "^3.0.2"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
@ -21476,36 +21712,6 @@
"integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==",
"dev": true
},
"node_modules/handlebars": {
"version": "4.7.8",
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz",
"integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==",
"dev": true,
"dependencies": {
"minimist": "^1.2.5",
"neo-async": "^2.6.2",
"source-map": "^0.6.1",
"wordwrap": "^1.0.0"
},
"bin": {
"handlebars": "bin/handlebars"
},
"engines": {
"node": ">=0.4.7"
},
"optionalDependencies": {
"uglify-js": "^3.1.4"
}
},
"node_modules/handlebars/node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/har-schema": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
@ -28961,9 +29167,9 @@
}
},
"node_modules/jsonc-parser": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz",
"integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==",
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz",
"integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==",
"dev": true
},
"node_modules/jsonfile": {
@ -29764,15 +29970,15 @@
}
},
"node_modules/marked": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/marked/-/marked-2.1.3.tgz",
"integrity": "sha512-/Q+7MGzaETqifOMWYEA7HVMaZb4XbcRfaOzcSsHZEith83KGlvaSG33u0SKu89Mj5h+T8V2hM+8O45Qc5XTgwA==",
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz",
"integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==",
"dev": true,
"bin": {
"marked": "bin/marked"
"marked": "bin/marked.js"
},
"engines": {
"node": ">= 10"
"node": ">= 12"
}
},
"node_modules/martinez-polygon-clipping": {
@ -35940,6 +36146,11 @@
"rimraf": "bin.js"
}
},
"node_modules/robust-predicates": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz",
"integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg=="
},
"node_modules/rst-selector-parser": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz",
@ -36685,9 +36896,9 @@
"integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww=="
},
"node_modules/shiki": {
"version": "0.9.15",
"resolved": "https://registry.npmjs.org/shiki/-/shiki-0.9.15.tgz",
"integrity": "sha512-/Y0z9IzhJ8nD9nbceORCqu6NgT9X6I8Fk8c3SICHI5NbZRLdZYFaB233gwct9sU0vvSypyaL/qaKvzyQGJBZSw==",
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/shiki/-/shiki-0.10.1.tgz",
"integrity": "sha512-VsY7QJVzU51j5o1+DguUd+6vmCmZ5v/6gYu4vyYAhzjuNQU6P/vmSy4uQaOhvje031qQMiW0d2BwgMH52vqMng==",
"dev": true,
"dependencies": {
"jsonc-parser": "^3.0.0",
@ -38612,19 +38823,16 @@
}
},
"node_modules/typedoc": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.21.5.tgz",
"integrity": "sha512-uRDRmYheE5Iju9Zz0X50pTASTpBorIHFt02F5Y8Dt4eBt55h3mwk1CBSY2+EfwBxY16N4Xm7f8KXhnfFZ0AmBw==",
"version": "0.22.15",
"resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.22.15.tgz",
"integrity": "sha512-CMd1lrqQbFvbx6S9G6fL4HKp3GoIuhujJReWqlIvSb2T26vGai+8Os3Mde7Pn832pXYemd9BMuuYWhFpL5st0Q==",
"dev": true,
"dependencies": {
"glob": "^7.1.7",
"handlebars": "^4.7.7",
"glob": "^7.2.0",
"lunr": "^2.3.9",
"marked": "^2.1.1",
"minimatch": "^3.0.0",
"progress": "^2.0.3",
"shiki": "^0.9.3",
"typedoc-default-themes": "^0.12.10"
"marked": "^4.0.12",
"minimatch": "^5.0.1",
"shiki": "^0.10.1"
},
"bin": {
"typedoc": "bin/typedoc"
@ -38633,16 +38841,28 @@
"node": ">= 12.10.0"
},
"peerDependencies": {
"typescript": "4.0.x || 4.1.x || 4.2.x || 4.3.x"
"typescript": "4.0.x || 4.1.x || 4.2.x || 4.3.x || 4.4.x || 4.5.x || 4.6.x"
}
},
"node_modules/typedoc-default-themes": {
"version": "0.12.10",
"resolved": "https://registry.npmjs.org/typedoc-default-themes/-/typedoc-default-themes-0.12.10.tgz",
"integrity": "sha512-fIS001cAYHkyQPidWXmHuhs8usjP5XVJjWB8oZGqkTowZaz3v7g3KDZeeqE82FBrmkAnIBOY3jgy7lnPnqATbA==",
"node_modules/typedoc/node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"dev": true,
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/typedoc/node_modules/minimatch": {
"version": "5.1.6",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
"integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
"dev": true,
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">= 8"
"node": ">=10"
}
},
"node_modules/typescript": {
@ -39655,9 +39875,9 @@
}
},
"node_modules/webpack-dev-middleware": {
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.3.tgz",
"integrity": "sha512-hj5CYrY0bZLB+eTO+x/j67Pkrquiy7kWepMHmUMoPsmcUaeEnQJqFzHJOyxgWlq746/wUuA64p9ta34Kyb01pA==",
"version": "5.3.4",
"resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz",
"integrity": "sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q==",
"dev": true,
"dependencies": {
"colorette": "^2.0.10",
@ -39731,9 +39951,9 @@
}
},
"node_modules/webpack-dev-server": {
"version": "4.15.1",
"resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.1.tgz",
"integrity": "sha512-5hbAst3h3C3L8w6W4P96L5vaV0PxSmJhxZvWKYIdgxOQm8pNZ5dEOmmSLBVpP85ReeyRt6AS1QJNyo/oFFPeVA==",
"version": "4.15.2",
"resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.2.tgz",
"integrity": "sha512-0XavAZbNJ5sDrCbkpWL8mia0o5WPOd2YGtxrEiZkBK9FjLppIUK2TgxK6qGD2P3hUXTJNNPVibrerKcx5WkR1g==",
"dev": true,
"dependencies": {
"@types/bonjour": "^3.5.9",
@ -39764,7 +39984,7 @@
"serve-index": "^1.9.1",
"sockjs": "^0.3.24",
"spdy": "^4.0.2",
"webpack-dev-middleware": "^5.3.1",
"webpack-dev-middleware": "^5.3.4",
"ws": "^8.13.0"
},
"bin": {
@ -40241,12 +40461,6 @@
"node": ">=0.10.0"
}
},
"node_modules/wordwrap": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
"integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==",
"dev": true
},
"node_modules/wrap-ansi": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",

View File

@ -56,7 +56,7 @@
"copy-webpack-plugin": "11.0.0",
"crossroads": "0.12.2",
"css-element-queries": "1.1.1",
"d3": "6.1.1",
"d3": "7.8.5",
"datatables.net-colreorder-dt": "1.7.0",
"datatables.net-dt": "1.13.8",
"date-fns": "1.29.0",
@ -189,14 +189,14 @@
"sinon": "3.2.1",
"style-loader": "0.23.0",
"ts-loader": "9.2.4",
"typedoc": "0.21.5",
"typedoc": "0.22.15",
"typescript": "4.3.5",
"url-loader": "4.1.1",
"wait-on": "4.0.2",
"webpack": "5.88.2",
"webpack-bundle-analyzer": "4.9.1",
"webpack-cli": "5.1.4",
"webpack-dev-server": "4.15.1"
"webpack-dev-server": "4.15.2"
},
"scripts": {
"postinstall": "patch-package",

View File

@ -677,6 +677,7 @@ export function useMongoProxyEndpoint(api: string): boolean {
MongoProxyEndpoints.Local,
MongoProxyEndpoints.Mpac,
MongoProxyEndpoints.Prod,
MongoProxyEndpoints.Fairfax,
];
let canAccessMongoProxy: boolean = userContext.databaseAccount.properties.publicNetworkAccess === "Enabled";
if (

View File

@ -142,7 +142,7 @@ export const TableEntity: FunctionComponent<TableEntityProps> = ({
<Image
{...imageProps}
src={EditIcon}
alt="editEntity"
alt={`Edit ${entityProperty} entity`}
onClick={onEditEntity}
tabIndex={0}
onKeyPress={handleKeyPress}
@ -156,7 +156,7 @@ export const TableEntity: FunctionComponent<TableEntityProps> = ({
<Image
{...imageProps}
src={DeleteIcon}
alt="delete entity"
alt={`Delete ${entityProperty} entity`}
id="deleteEntity"
onClick={onDeleteEntity}
tabIndex={0}

View File

@ -68,10 +68,6 @@ export interface OpenPane extends DataExplorerAction {
paneKind: PaneKind | string;
}
export interface OpenSampleNotebook extends DataExplorerAction {
path: string;
}
/**
* The types of actions that the DataExplorer supports performing upon opening.
*/
@ -80,5 +76,4 @@ export enum ActionType {
OpenCollectionTab,
OpenPane,
TransmitCachedData,
OpenSampleNotebook,
}

View File

@ -29,8 +29,6 @@ exports[`SettingsComponent renders 1`] = `
"computedProperties": [Function],
"conflictResolutionPolicy": [Function],
"container": Explorer {
"_isInitializingNotebooks": false,
"_resetNotebookWorkspace": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function],
"isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function],
@ -47,10 +45,8 @@ exports[`SettingsComponent renders 1`] = `
"queriesClient": QueriesClient {
"container": [Circular],
},
"refreshNotebookList": [Function],
"resourceTree": ResourceTreeAdapter {
"container": [Circular],
"copyNotebook": [Function],
"parameters": [Function],
},
},
@ -107,8 +103,6 @@ exports[`SettingsComponent renders 1`] = `
"computedProperties": [Function],
"conflictResolutionPolicy": [Function],
"container": Explorer {
"_isInitializingNotebooks": false,
"_resetNotebookWorkspace": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function],
"isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function],
@ -125,10 +119,8 @@ exports[`SettingsComponent renders 1`] = `
"queriesClient": QueriesClient {
"container": [Circular],
},
"refreshNotebookList": [Function],
"resourceTree": ResourceTreeAdapter {
"container": [Circular],
"copyNotebook": [Function],
"parameters": [Function],
},
},
@ -224,8 +216,6 @@ exports[`SettingsComponent renders 1`] = `
"computedProperties": [Function],
"conflictResolutionPolicy": [Function],
"container": Explorer {
"_isInitializingNotebooks": false,
"_resetNotebookWorkspace": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function],
"isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function],
@ -242,10 +232,8 @@ exports[`SettingsComponent renders 1`] = `
"queriesClient": QueriesClient {
"container": [Circular],
},
"refreshNotebookList": [Function],
"resourceTree": ResourceTreeAdapter {
"container": [Circular],
"copyNotebook": [Function],
"parameters": [Function],
},
},
@ -271,8 +259,6 @@ exports[`SettingsComponent renders 1`] = `
}
explorer={
Explorer {
"_isInitializingNotebooks": false,
"_resetNotebookWorkspace": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function],
"isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function],
@ -289,10 +275,8 @@ exports[`SettingsComponent renders 1`] = `
"queriesClient": QueriesClient {
"container": [Circular],
},
"refreshNotebookList": [Function],
"resourceTree": ResourceTreeAdapter {
"container": [Circular],
"copyNotebook": [Function],
"parameters": [Function],
},
}

View File

@ -1,4 +1,3 @@
import { Link } from "@fluentui/react/lib/Link";
import { isPublicInternetAccessAllowed } from "Common/DatabaseAccountUtility";
import { sendMessage } from "Common/MessageHandler";
import { Platform, configContext } from "ConfigContext";
@ -16,7 +15,7 @@ import shallow from "zustand/shallow";
import { AuthType } from "../AuthType";
import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer";
import * as Constants from "../Common/Constants";
import { Areas, ConnectionStatusType, HttpStatusCodes, Notebook, PoolIdType } from "../Common/Constants";
import { Areas, ConnectionStatusType, HttpStatusCodes, PoolIdType } from "../Common/Constants";
import { getErrorMessage, getErrorStack, handleError } from "../Common/ErrorHandlingUtils";
import * as Logger from "../Common/Logger";
import { QueriesClient } from "../Common/QueriesClient";
@ -32,34 +31,23 @@ import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants"
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
import { isAccountNewerThanThresholdInMs, userContext } from "../UserContext";
import { getCollectionName, getUploadName } from "../Utils/APITypeUtils";
import { stringToBlob } from "../Utils/BlobUtils";
import { isCapabilityEnabled } from "../Utils/CapabilityUtils";
import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils";
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils";
import { update } from "../Utils/arm/generatedClients/cosmos/databaseAccounts";
import { listByDatabaseAccount } from "../Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces";
import { useSidePanel } from "../hooks/useSidePanel";
import { useTabs } from "../hooks/useTabs";
import "./ComponentRegisterer";
import { DialogProps, useDialog } from "./Controls/Dialog";
import { GalleryTab as GalleryTabKind } from "./Controls/NotebookGallery/GalleryViewerComponent";
import { useCommandBar } from "./Menus/CommandBar/CommandBarComponentAdapter";
import * as FileSystemUtil from "./Notebook/FileSystemUtil";
import { SnapshotRequest } from "./Notebook/NotebookComponent/types";
import { NotebookContentItem, NotebookContentItemType } from "./Notebook/NotebookContentItem";
import { NotebookContentItem } from "./Notebook/NotebookContentItem";
import type NotebookManager from "./Notebook/NotebookManager";
import { NotebookPaneContent } from "./Notebook/NotebookManager";
import { NotebookUtil } from "./Notebook/NotebookUtil";
import { useNotebook } from "./Notebook/useNotebook";
import { AddCollectionPanel } from "./Panes/AddCollectionPanel";
import { CassandraAddCollectionPane } from "./Panes/CassandraAddCollectionPane/CassandraAddCollectionPane";
import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane/ExecuteSprocParamsPane";
import { StringInputPane } from "./Panes/StringInputPane/StringInputPane";
import { UploadFilePane } from "./Panes/UploadFilePane/UploadFilePane";
import { UploadItemsPane } from "./Panes/UploadItemsPane/UploadItemsPane";
import { CassandraAPIDataClient, TableDataClient, TablesAPIDataClient } from "./Tables/TableDataClient";
import NotebookV2Tab, { NotebookTabOptions } from "./Tabs/NotebookV2Tab";
import TabsBase from "./Tabs/TabsBase";
import TerminalTab from "./Tabs/TerminalTab";
import Database from "./Tree/Database";
@ -87,7 +75,6 @@ export default class Explorer {
// Notebooks
public notebookManager?: NotebookManager;
private _isInitializingNotebooks: boolean;
private notebookToImport: {
name: string;
content: string;
@ -99,7 +86,6 @@ export default class Explorer {
const startKey: number = TelemetryProcessor.traceStart(Action.InitializeDataExplorer, {
dataExplorerArea: Constants.Areas.ResourceTree,
});
this._isInitializingNotebooks = false;
this.phoenixClient = new PhoenixClient(userContext?.databaseAccount?.id);
useNotebook.subscribe(
@ -205,12 +191,10 @@ export default class Explorer {
container: this,
resourceTree: this.resourceTree,
refreshCommandBarButtons: () => this.refreshCommandBarButtons(),
refreshNotebookList: () => this.refreshNotebookList(),
});
}
this.refreshCommandBarButtons();
this.refreshNotebookList();
}
public openEnableSynapseLinkDialog(): void {
@ -373,7 +357,6 @@ export default class Explorer {
userContext.authType === AuthType.ResourceToken
? this.refreshDatabaseForResourceToken()
: this.refreshAllDatabases();
this.refreshNotebookList();
};
// Facade
@ -381,19 +364,6 @@ export default class Explorer {
window.open(Constants.Urls.feedbackEmail, "_blank");
};
public async initNotebooks(databaseAccount: DataModels.DatabaseAccount): Promise<void> {
if (!databaseAccount) {
throw new Error("No database account specified");
}
if (this._isInitializingNotebooks) {
return;
}
this._isInitializingNotebooks = true;
this.refreshNotebookList();
this._isInitializingNotebooks = false;
}
public async allocateContainer(poolId: PoolIdType, mode?: string): Promise<void> {
const shouldUseNotebookStates = poolId === PoolIdType.DefaultPoolId ? true : false;
const notebookServerInfo = shouldUseNotebookStates
@ -472,8 +442,6 @@ export default class Explorer {
? useNotebook.getState().setIsAllocating(false)
: useQueryCopilot.getState().setIsAllocatingContainer(false);
this.refreshCommandBarButtons();
this.refreshNotebookList();
this._isInitializingNotebooks = false;
}
}
}
@ -510,104 +478,6 @@ export default class Explorer {
.then((memoryUsageInfo) => useNotebook.getState().setMemoryUsageInfo(memoryUsageInfo));
}
public resetNotebookWorkspace(): void {
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookClient) {
handleError(
"Attempt to reset notebook workspace, but notebook is not enabled",
"Explorer/resetNotebookWorkspace",
);
return;
}
const dialogContent = useNotebook.getState().isPhoenixNotebooks
? "Notebooks saved in the temporary workspace will be deleted. Do you want to proceed?"
: "This lets you keep your notebook files and the workspace will be restored to default. Proceed anyway?";
const resetConfirmationDialogProps: DialogProps = {
isModal: true,
title: "Reset Workspace",
subText: dialogContent,
primaryButtonText: "OK",
secondaryButtonText: "Cancel",
onPrimaryButtonClick: this._resetNotebookWorkspace,
onSecondaryButtonClick: () => useDialog.getState().closeDialog(),
};
useDialog.getState().openDialog(resetConfirmationDialogProps);
}
private async _containsDefaultNotebookWorkspace(databaseAccount: DataModels.DatabaseAccount): Promise<boolean> {
if (!databaseAccount) {
return false;
}
try {
const { value: workspaces } = await listByDatabaseAccount(
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
);
return workspaces && workspaces.length > 0 && workspaces.some((workspace) => workspace.name === "default");
} catch (error) {
Logger.logError(getErrorMessage(error), "Explorer/_containsDefaultNotebookWorkspace");
return false;
}
}
private _resetNotebookWorkspace = async () => {
useDialog.getState().closeDialog();
const clearInProgressMessage = logConsoleProgress("Resetting notebook workspace");
let connectionStatus: ContainerConnectionInfo;
try {
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
if (!notebookServerInfo || !notebookServerInfo.notebookServerEndpoint) {
const error = "No server endpoint detected";
Logger.logError(error, "NotebookContainerClient/resetWorkspace");
logConsoleError(error);
return;
}
TelemetryProcessor.traceStart(Action.PhoenixResetWorkspace, {
dataExplorerArea: Areas.Notebook,
});
if (useNotebook.getState().isPhoenixNotebooks) {
useTabs.getState().closeAllNotebookTabs(true);
connectionStatus = {
status: ConnectionStatusType.Connecting,
};
useNotebook.getState().setConnectionInfo(connectionStatus);
}
const connectionInfo = await this.notebookManager?.notebookClient.resetWorkspace();
if (connectionInfo?.status !== HttpStatusCodes.OK) {
throw new Error(`Reset Workspace: Received status code- ${connectionInfo?.status}`);
}
if (!connectionInfo?.data?.phoenixServiceUrl) {
throw new Error(`Reset Workspace: PhoenixServiceUrl is invalid!`);
}
if (useNotebook.getState().isPhoenixNotebooks) {
await this.setNotebookInfo(true, connectionInfo, connectionStatus);
useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed);
}
logConsoleInfo("Successfully reset notebook workspace");
TelemetryProcessor.traceSuccess(Action.PhoenixResetWorkspace, {
dataExplorerArea: Areas.Notebook,
});
} catch (error) {
logConsoleError(`Failed to reset notebook workspace: ${error}`);
TelemetryProcessor.traceFailure(Action.PhoenixResetWorkspace, {
dataExplorerArea: Areas.Notebook,
error: getErrorMessage(error),
errorStack: getErrorStack(error),
});
if (useNotebook.getState().isPhoenixNotebooks) {
connectionStatus = {
status: ConnectionStatusType.Failed,
};
useNotebook.getState().resetContainerConnection(connectionStatus);
useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed);
}
throw error;
} finally {
clearInProgressMessage();
}
};
private getDeltaDatabases(
updatedDatabaseList: DataModels.Database[],
databases: ViewModels.Database[],
@ -696,406 +566,6 @@ export default class Explorer {
}
}
public uploadFile(
name: string,
content: string,
parent: NotebookContentItem,
isGithubTree?: boolean,
): Promise<NotebookContentItem> {
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) {
const error = "Attempt to upload notebook, but notebook is not enabled";
handleError(error, "Explorer/uploadFile");
throw new Error(error);
}
const promise = this.notebookManager?.notebookContentClient.uploadFileAsync(name, content, parent, isGithubTree);
promise
.then(() => this.resourceTree.triggerRender())
.catch((reason) => useDialog.getState().showOkModalDialog("Unable to upload file", getErrorMessage(reason)));
return promise;
}
public async importAndOpen(path: string): Promise<boolean> {
const name = NotebookUtil.getName(path);
const item = NotebookUtil.createNotebookContentItem(name, path, "file");
const parent = this.resourceTree.myNotebooksContentRoot;
if (parent && parent.children && useNotebook.getState().isNotebookEnabled && this.notebookManager?.notebookClient) {
const existingItem = _.find(parent.children, (node) => node.name === name);
if (existingItem) {
return this.openNotebook(existingItem);
}
const content = await this.readFile(item);
const uploadedItem = await this.uploadFile(name, content, parent);
return this.openNotebook(uploadedItem);
}
return Promise.resolve(false);
}
public async importAndOpenContent(name: string, content: string): Promise<boolean> {
const parent = this.resourceTree.myNotebooksContentRoot;
if (parent && parent.children && useNotebook.getState().isNotebookEnabled && this.notebookManager?.notebookClient) {
if (this.notebookToImport && this.notebookToImport.name === name && this.notebookToImport.content === content) {
this.notebookToImport = undefined; // we don't want to try opening this notebook again
}
const existingItem = _.find(parent.children, (node) => node.name === name);
if (existingItem) {
return this.openNotebook(existingItem);
}
const uploadedItem = await this.uploadFile(name, content, parent);
return this.openNotebook(uploadedItem);
}
this.notebookToImport = { name, content }; // we'll try opening this notebook later on
return Promise.resolve(false);
}
public async publishNotebook(
name: string,
content: NotebookPaneContent,
notebookContentRef?: string,
onTakeSnapshot?: (request: SnapshotRequest) => void,
onClosePanel?: () => void,
): Promise<void> {
if (this.notebookManager) {
await this.notebookManager.openPublishNotebookPane(
name,
content,
notebookContentRef,
onTakeSnapshot,
onClosePanel,
);
}
}
public copyNotebook(name: string, content: string): void {
this.notebookManager?.openCopyNotebookPane(name, content);
}
/**
* Note: To keep it simple, this creates a disconnected NotebookContentItem that is not connected to the resource tree.
* Connecting it to a tree possibly requires the intermediate missing folders if the item is nested in a subfolder.
* Manually creating the missing folders between the root and its parent dir would break the UX: expanding a folder
* will not fetch its content if the children array exists (and has only one child which was manually created).
* Fetching the intermediate folders possibly involves a few chained async calls which isn't ideal.
*
* @param name
* @param path
*/
public createNotebookContentItemFile(name: string, path: string): NotebookContentItem {
return NotebookUtil.createNotebookContentItem(name, path, "file");
}
public async openNotebook(notebookContentItem: NotebookContentItem): Promise<boolean> {
if (!notebookContentItem || !notebookContentItem.path) {
throw new Error(`Invalid notebookContentItem: ${notebookContentItem}`);
}
if (notebookContentItem.type === NotebookContentItemType.Notebook && useNotebook.getState().isPhoenixNotebooks) {
await this.allocateContainer(PoolIdType.DefaultPoolId);
}
const notebookTabs = useTabs
.getState()
.getTabs(
ViewModels.CollectionTabKind.NotebookV2,
(tab) =>
(tab as NotebookV2Tab).notebookPath &&
FileSystemUtil.isPathEqual((tab as NotebookV2Tab).notebookPath(), notebookContentItem.path),
) as NotebookV2Tab[];
let notebookTab = notebookTabs && notebookTabs[0];
if (notebookTab) {
useTabs.getState().activateTab(notebookTab);
} else {
const options: NotebookTabOptions = {
account: userContext.databaseAccount,
tabKind: ViewModels.CollectionTabKind.NotebookV2,
node: undefined,
title: notebookContentItem.name,
tabPath: notebookContentItem.path,
collection: undefined,
masterKey: userContext.masterKey || "",
isTabsContentExpanded: ko.observable(true),
onLoadStartKey: undefined,
container: this,
notebookContentItem,
};
try {
const NotebookTabV2 = await import(/* webpackChunkName: "NotebookV2Tab" */ "./Tabs/NotebookV2Tab");
notebookTab = new NotebookTabV2.default(options);
useTabs.getState().activateNewTab(notebookTab);
} catch (reason) {
console.error("Import NotebookV2Tab failed!", reason);
return false;
}
}
return true;
}
public renameNotebook(notebookFile: NotebookContentItem, isGithubTree?: boolean): void {
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) {
const error = "Attempt to rename notebook, but notebook is not enabled";
handleError(error, "Explorer/renameNotebook");
throw new Error(error);
}
// Don't delete if tab is open to avoid accidental deletion
const openedNotebookTabs = useTabs
.getState()
.getTabs(ViewModels.CollectionTabKind.NotebookV2, (tab: NotebookV2Tab) => {
return tab.notebookPath && FileSystemUtil.isPathEqual(tab.notebookPath(), notebookFile.path);
});
if (openedNotebookTabs.length > 0) {
useDialog
.getState()
.showOkModalDialog("Unable to rename file", "This file is being edited. Please close the tab and try again.");
} else {
useSidePanel.getState().openSidePanel(
"Rename Notebook",
<StringInputPane
closePanel={() => {
useSidePanel.getState().closeSidePanel();
this.resourceTree.triggerRender();
}}
inputLabel="Enter new notebook name"
submitButtonLabel="Rename"
errorMessage="Could not rename notebook"
inProgressMessage="Renaming notebook to"
successMessage="Renamed notebook to"
paneTitle="Rename Notebook"
defaultInput={FileSystemUtil.stripExtension(notebookFile.name, "ipynb")}
onSubmit={(notebookFile: NotebookContentItem, input: string): Promise<NotebookContentItem> =>
this.notebookManager?.notebookContentClient.renameNotebook(notebookFile, input, isGithubTree)
}
notebookFile={notebookFile}
/>,
);
}
}
public onCreateDirectory(parent: NotebookContentItem, isGithubTree?: boolean): void {
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) {
const error = "Attempt to create notebook directory, but notebook is not enabled";
handleError(error, "Explorer/onCreateDirectory");
throw new Error(error);
}
useSidePanel.getState().openSidePanel(
"Create new directory",
<StringInputPane
closePanel={() => {
useSidePanel.getState().closeSidePanel();
this.resourceTree.triggerRender();
}}
errorMessage="Could not create directory "
inProgressMessage="Creating directory "
successMessage="Created directory "
inputLabel="Enter new directory name"
paneTitle="Create new directory"
submitButtonLabel="Create"
defaultInput=""
onSubmit={(notebookFile: NotebookContentItem, input: string): Promise<NotebookContentItem> =>
this.notebookManager?.notebookContentClient.createDirectory(notebookFile, input, isGithubTree)
}
notebookFile={parent}
/>,
);
}
public readFile(notebookFile: NotebookContentItem): Promise<string> {
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) {
const error = "Attempt to read file, but notebook is not enabled";
handleError(error, "Explorer/downloadFile");
throw new Error(error);
}
return this.notebookManager?.notebookContentClient.readFileContent(notebookFile.path);
}
public downloadFile(notebookFile: NotebookContentItem): Promise<void> {
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) {
const error = "Attempt to download file, but notebook is not enabled";
handleError(error, "Explorer/downloadFile");
throw new Error(error);
}
const clearMessage = NotificationConsoleUtils.logConsoleProgress(`Downloading ${notebookFile.path}`);
return this.notebookManager?.notebookContentClient.readFileContent(notebookFile.path).then(
(content: string) => {
const blob = stringToBlob(content, "text/plain");
if (navigator.msSaveBlob) {
// for IE and Edge
navigator.msSaveBlob(blob, notebookFile.name);
} else {
const downloadLink: HTMLAnchorElement = document.createElement("a");
const url = URL.createObjectURL(blob);
downloadLink.href = url;
downloadLink.target = "_self";
downloadLink.download = notebookFile.name;
// for some reason, FF displays the download prompt only when
// the link is added to the dom so we add and remove it
document.body.appendChild(downloadLink);
downloadLink.click();
downloadLink.remove();
}
clearMessage();
},
(error) => {
logConsoleError(`Could not download notebook ${getErrorMessage(error)}`);
clearMessage();
},
);
}
private refreshNotebookList = async (): Promise<void> => {
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) {
return;
}
await this.resourceTree.initialize();
await useNotebook.getState().initializeNotebooksTree(this.notebookManager);
this.notebookManager?.refreshPinnedRepos();
if (this.notebookToImport) {
this.importAndOpenContent(this.notebookToImport.name, this.notebookToImport.content);
}
};
public deleteNotebookFile(item: NotebookContentItem, isGithubTree?: boolean): Promise<void> {
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) {
const error = "Attempt to delete notebook file, but notebook is not enabled";
handleError(error, "Explorer/deleteNotebookFile");
throw new Error(error);
}
// Don't delete if tab is open to avoid accidental deletion
const openedNotebookTabs = useTabs
.getState()
.getTabs(ViewModels.CollectionTabKind.NotebookV2, (tab: NotebookV2Tab) => {
return tab.notebookPath && FileSystemUtil.isPathEqual(tab.notebookPath(), item.path);
});
if (openedNotebookTabs.length > 0) {
useDialog
.getState()
.showOkModalDialog("Unable to delete file", "This file is being edited. Please close the tab and try again.");
return Promise.reject();
}
if (item.type === NotebookContentItemType.Directory && item.children && item.children.length > 0) {
useDialog.getState().openDialog({
isModal: true,
title: "Unable to delete file",
subText: "Directory is not empty.",
primaryButtonText: "Close",
secondaryButtonText: undefined,
onPrimaryButtonClick: () => useDialog.getState().closeDialog(),
onSecondaryButtonClick: undefined,
});
return Promise.reject();
}
return this.notebookManager?.notebookContentClient.deleteContentItem(item, isGithubTree).then(
() => logConsoleInfo(`Successfully deleted: ${item.path}`),
(reason) => logConsoleError(`Failed to delete "${item.path}": ${JSON.stringify(reason)}`),
);
}
/**
* This creates a new notebook file, then opens the notebook
*/
public async onNewNotebookClicked(parent?: NotebookContentItem, isGithubTree?: boolean): Promise<void> {
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) {
const error = "Attempt to create new notebook, but notebook is not enabled";
handleError(error, "Explorer/onNewNotebookClicked");
throw new Error(error);
}
if (useNotebook.getState().isPhoenixNotebooks) {
if (isGithubTree) {
await this.allocateContainer(PoolIdType.DefaultPoolId);
parent = parent || this.resourceTree.myNotebooksContentRoot;
this.createNewNoteBook(parent, isGithubTree);
} else {
useDialog.getState().showOkCancelModalDialog(
Notebook.newNotebookModalTitle,
undefined,
"Create",
async () => {
await this.allocateContainer(PoolIdType.DefaultPoolId);
parent = parent || this.resourceTree.myNotebooksContentRoot;
this.createNewNoteBook(parent, isGithubTree);
},
"Cancel",
undefined,
this.getNewNoteWarningText(),
);
}
} else {
parent = parent || this.resourceTree.myNotebooksContentRoot;
this.createNewNoteBook(parent, isGithubTree);
}
}
private getNewNoteWarningText(): JSX.Element {
return (
<>
<p>{Notebook.newNotebookModalContent1}</p>
<br />
<p>
{Notebook.newNotebookModalContent2}
<Link href={Notebook.cosmosNotebookHomePageUrl} target="_blank">
{Notebook.learnMore}
</Link>
</p>
</>
);
}
private createNewNoteBook(parent?: NotebookContentItem, isGithubTree?: boolean): void {
const clearInProgressMessage = logConsoleProgress(`Creating new notebook in ${parent.path}`);
const startKey: number = TelemetryProcessor.traceStart(Action.CreateNewNotebook, {
dataExplorerArea: Constants.Areas.Notebook,
});
this.notebookManager?.notebookContentClient
.createNewNotebookFile(parent, isGithubTree)
.then((newFile: NotebookContentItem) => {
logConsoleInfo(`Successfully created: ${newFile.name}`);
TelemetryProcessor.traceSuccess(
Action.CreateNewNotebook,
{
dataExplorerArea: Constants.Areas.Notebook,
},
startKey,
);
return this.openNotebook(newFile);
})
.then(() => this.resourceTree.triggerRender())
.catch((error) => {
const errorMessage = `Failed to create a new notebook: ${getErrorMessage(error)}`;
logConsoleError(errorMessage);
TelemetryProcessor.traceFailure(
Action.CreateNewNotebook,
{
dataExplorerArea: Constants.Areas.Notebook,
error: errorMessage,
errorStack: getErrorStack(error),
},
startKey,
);
})
.finally(clearInProgressMessage);
}
// TODO: Delete this function when ResourceTreeAdapter is removed.
public async refreshContentItem(item: NotebookContentItem): Promise<void> {
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) {
@ -1252,32 +722,6 @@ export default class Explorer {
}
}
public async handleOpenFileAction(path: string): Promise<void> {
if (useNotebook.getState().isPhoenixNotebooks === undefined) {
await useNotebook.getState().getPhoenixStatus();
}
if (useNotebook.getState().isPhoenixNotebooks) {
await this.allocateContainer(PoolIdType.DefaultPoolId);
}
// We still use github urls like https://github.com/Azure-Samples/cosmos-notebooks/blob/master/CSharp_quickstarts/GettingStarted_CSharp.ipynb
// when launching a notebook quickstart from Portal. In future we should just use gallery id and use Juno to fetch instead of directly
// calling GitHub. For now convert this url to a raw url and download content.
const gitHubInfo = fromContentUri(path);
if (gitHubInfo) {
const rawUrl = toRawContentUri(gitHubInfo.owner, gitHubInfo.repo, gitHubInfo.branch, gitHubInfo.path);
const response = await fetch(rawUrl);
if (response.status === Constants.HttpStatusCodes.OK) {
this.notebookToImport = {
name: NotebookUtil.getName(path),
content: await response.text(),
};
this.importAndOpenContent(this.notebookToImport.name, this.notebookToImport.content);
}
}
}
public openUploadItemsPanePane(): void {
useSidePanel.getState().openSidePanel("Upload " + getUploadName(), <UploadItemsPane />);
}
@ -1287,54 +731,6 @@ export default class Explorer {
.openSidePanel("Input parameters", <ExecuteSprocParamsPane storedProcedure={storedProcedure} />);
}
public openUploadFilePanel(parent?: NotebookContentItem): void {
if (useNotebook.getState().isPhoenixNotebooks) {
useDialog.getState().showOkCancelModalDialog(
Notebook.newNotebookUploadModalTitle,
undefined,
"Upload",
async () => {
await this.allocateContainer(PoolIdType.DefaultPoolId);
parent = parent || this.resourceTree.myNotebooksContentRoot;
this.uploadFilePanel(parent);
},
"Cancel",
undefined,
this.getNewNoteWarningText(),
);
} else {
parent = parent || this.resourceTree.myNotebooksContentRoot;
this.uploadFilePanel(parent);
}
}
private uploadFilePanel(parent?: NotebookContentItem): void {
useSidePanel
.getState()
.openSidePanel(
"Upload file to notebook server",
<UploadFilePane uploadFile={(name: string, content: string) => this.uploadFile(name, content, parent)} />,
);
}
public getDownloadModalConent(fileName: string): JSX.Element {
if (useNotebook.getState().isPhoenixNotebooks) {
return (
<>
<p>{Notebook.galleryNotebookDownloadContent1}</p>
<br />
<p>
{Notebook.galleryNotebookDownloadContent2}
<Link href={Notebook.cosmosNotebookGitDocumentationUrl} target="_blank">
{Notebook.learnMore}
</Link>
</p>
</>
);
}
return <p> Download {fileName} from gallery as a copy to your notebooks to run and/or edit the notebook. </p>;
}
public async refreshExplorer(): Promise<void> {
if (userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo") {
userContext.authType === AuthType.ResourceToken
@ -1359,10 +755,6 @@ export default class Explorer {
dataExplorerArea: Constants.Areas.Notebook,
});
if (useNotebook.getState().isPhoenixNotebooks) {
await this.initNotebooks(userContext.databaseAccount);
}
await this.refreshSampleData();
}

View File

@ -349,7 +349,7 @@ export class NodePropertiesComponent extends React.Component<
onActivated={this.setIsDeleteConfirm.bind(this, true)}
aria-label="Delete this vertex"
>
<img src={DeleteIcon} alt="Delete" />
<img src={DeleteIcon} alt="Delete" role="button" />
</AccessibleElement>
);
} else {
@ -406,7 +406,7 @@ export class NodePropertiesComponent extends React.Component<
aria-label="Edit properties"
onActivated={expandClickHandler}
>
<img src={EditIcon} alt="Edit" />
<img src={EditIcon} alt="Edit" role="button" />
</AccessibleElement>
)}

View File

@ -184,12 +184,18 @@ export const NewVertexComponent: FunctionComponent<INewVertexComponentProps> = (
className="rightPaneTrashIcon rightPaneBtns"
tabIndex={0}
role="button"
aria-label={`Delete ${data.key}`}
onClick={(event: React.MouseEvent<HTMLDivElement>) => removeNewVertexProperty(event, index)}
onKeyPress={(event: React.KeyboardEvent<HTMLDivElement>) =>
removeNewVertexPropertyKeyPress(event, index)
}
>
<img className="refreshcol rightPaneTrashIconImg" src={DeleteIcon} alt="Remove property" />
<img
aria-label="hidden"
className="refreshcol rightPaneTrashIconImg"
src={DeleteIcon}
alt="Remove property"
/>
</div>
</div>
</div>

View File

@ -12,15 +12,13 @@ import * as Logger from "../../Common/Logger";
import { GitHubClient } from "../../GitHub/GitHubClient";
import { GitHubContentProvider } from "../../GitHub/GitHubContentProvider";
import { GitHubOAuthService } from "../../GitHub/GitHubOAuthService";
import { useSidePanel } from "../../hooks/useSidePanel";
import { JunoClient } from "../../Juno/JunoClient";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext";
import { getFullName } from "../../Utils/UserUtils";
import { useSidePanel } from "../../hooks/useSidePanel";
import { useDialog } from "../Controls/Dialog";
import Explorer from "../Explorer";
import { CopyNotebookPane } from "../Panes/CopyNotebookPane/CopyNotebookPane";
import { GitHubReposPanel } from "../Panes/GitHubReposPanel/GitHubReposPanel";
import { PublishNotebookPane } from "../Panes/PublishNotebookPane/PublishNotebookPane";
import { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter";
@ -40,7 +38,6 @@ export interface NotebookManagerOptions {
container: Explorer;
resourceTree: ResourceTreeAdapter;
refreshCommandBarButtons: () => void;
refreshNotebookList: () => void;
}
export default class NotebookManager {
@ -81,10 +78,6 @@ export default class NotebookManager {
contents.JupyterContentProvider,
);
this.notebookClient = new NotebookContainerClient(() =>
this.params.container.initNotebooks(userContext?.databaseAccount),
);
this.notebookContentClient = new NotebookContentClient(this.notebookContentProvider);
this.gitHubOAuthService.getTokenObservable().subscribe((token) => {
@ -106,11 +99,9 @@ export default class NotebookManager {
}
this.params.refreshCommandBarButtons();
this.params.refreshNotebookList();
});
this.junoClient.subscribeToPinnedRepos((pinnedRepos) => {
this.params.resourceTree.initializeGitHubRepos(pinnedRepos);
this.params.resourceTree.triggerRender();
useNotebook.getState().initializeGitHubRepos(pinnedRepos);
});
@ -149,22 +140,6 @@ export default class NotebookManager {
);
}
public openCopyNotebookPane(name: string, content: string): void {
const { container } = this.params;
useSidePanel
.getState()
.openSidePanel(
"Copy Notebook",
<CopyNotebookPane
container={container}
junoClient={this.junoClient}
gitHubOAuthService={this.gitHubOAuthService}
name={name}
content={content}
/>,
);
}
// Octokit's error handler uses any
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private onGitHubClientError = (error: any): void => {

View File

@ -195,17 +195,5 @@ export function handleOpenAction(
return true;
}
if (
action.actionType === ActionContracts.ActionType.OpenSampleNotebook ||
action.actionType === ActionContracts.ActionType[ActionContracts.ActionType.OpenSampleNotebook]
) {
openFile(action as ActionContracts.OpenSampleNotebook, explorer);
return true;
}
return false;
}
function openFile(action: ActionContracts.OpenSampleNotebook, explorer: Explorer) {
explorer.handleOpenFileAction(decodeURIComponent(action.path));
}

View File

@ -1,154 +0,0 @@
import { IDropdownOption } from "@fluentui/react";
import React, { FormEvent, FunctionComponent, useEffect, useState } from "react";
import { HttpStatusCodes, PoolIdType } from "../../../Common/Constants";
import { getErrorMessage, handleError } from "../../../Common/ErrorHandlingUtils";
import { GitHubOAuthService } from "../../../GitHub/GitHubOAuthService";
import { IPinnedRepo, JunoClient } from "../../../Juno/JunoClient";
import * as GitHubUtils from "../../../Utils/GitHubUtils";
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import { useSidePanel } from "../../../hooks/useSidePanel";
import Explorer from "../../Explorer";
import { NotebookContentItem, NotebookContentItemType } from "../../Notebook/NotebookContentItem";
import { useNotebook } from "../../Notebook/useNotebook";
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
import { CopyNotebookPaneComponent, CopyNotebookPaneProps } from "./CopyNotebookPaneComponent";
interface Location {
type: "MyNotebooks" | "GitHub";
// GitHub
owner?: string;
repo?: string;
branch?: string;
}
export interface CopyNotebookPanelProps {
name: string;
content: string;
container: Explorer;
junoClient: JunoClient;
gitHubOAuthService: GitHubOAuthService;
}
export const CopyNotebookPane: FunctionComponent<CopyNotebookPanelProps> = ({
name,
content,
container,
junoClient,
gitHubOAuthService,
}: CopyNotebookPanelProps) => {
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
const [isExecuting, setIsExecuting] = useState<boolean>();
const [formError, setFormError] = useState<string>("");
const [pinnedRepos, setPinnedRepos] = useState<IPinnedRepo[]>();
const [selectedLocation, setSelectedLocation] = useState<Location>();
useEffect(() => {
open();
}, []);
const open = async (): Promise<void> => {
if (gitHubOAuthService.isLoggedIn()) {
const response = await junoClient.getPinnedRepos(gitHubOAuthService.getTokenObservable()()?.scope);
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) {
handleError(`Received HTTP ${response.status} when fetching pinned repos`, "CopyNotebookPaneAdapter/submit");
}
if (response.data?.length > 0) {
setPinnedRepos(response.data);
}
}
};
const submit = async (): Promise<void> => {
let destination: string = selectedLocation?.type;
let clearMessage: () => void;
setIsExecuting(true);
try {
if (!selectedLocation) {
throw new Error(`No location selected`);
}
if (selectedLocation.type === "GitHub") {
destination = `${destination} - ${GitHubUtils.toRepoFullName(
selectedLocation.owner,
selectedLocation.repo,
)} - ${selectedLocation.branch}`;
} else if (selectedLocation.type === "MyNotebooks" && useNotebook.getState().isPhoenixNotebooks) {
destination = useNotebook.getState().notebookFolderName;
}
clearMessage = NotificationConsoleUtils.logConsoleProgress(`Copying ${name} to ${destination}`);
const notebookContentItem = await copyNotebook(selectedLocation);
if (!notebookContentItem) {
throw new Error(`Failed to upload ${name}`);
}
NotificationConsoleUtils.logConsoleInfo(`Successfully copied ${name} to ${destination}`);
closeSidePanel();
} catch (error) {
const errorMessage = getErrorMessage(error);
setFormError(`Failed to copy ${name} to ${destination}`);
handleError(errorMessage, "CopyNotebookPaneAdapter/submit", formError);
} finally {
clearMessage && clearMessage();
setIsExecuting(false);
}
};
const copyNotebook = async (location: Location): Promise<NotebookContentItem> => {
let parent: NotebookContentItem;
let isGithubTree: boolean;
switch (location.type) {
case "MyNotebooks":
parent = {
name: useNotebook.getState().notebookFolderName,
path: useNotebook.getState().notebookBasePath,
type: NotebookContentItemType.Directory,
};
isGithubTree = false;
if (useNotebook.getState().isPhoenixNotebooks) {
await container.allocateContainer(PoolIdType.DefaultPoolId);
}
break;
case "GitHub":
parent = {
name: selectedLocation.branch,
path: GitHubUtils.toContentUri(selectedLocation.owner, selectedLocation.repo, selectedLocation.branch, ""),
type: NotebookContentItemType.Directory,
};
isGithubTree = true;
break;
default:
throw new Error(`Unsupported location type ${location.type}`);
}
return container.uploadFile(name, content, parent, isGithubTree);
};
const onDropDownChange = (_: FormEvent<HTMLDivElement>, option?: IDropdownOption): void => {
setSelectedLocation(option?.data);
};
const props: RightPaneFormProps = {
formError,
isExecuting: isExecuting,
submitButtonText: "OK",
onSubmit: () => submit(),
};
const copyNotebookPaneProps: CopyNotebookPaneProps = {
name,
pinnedRepos,
onDropDownChange: onDropDownChange,
};
return (
<RightPaneForm {...props}>
<CopyNotebookPaneComponent {...copyNotebookPaneProps} />
</RightPaneForm>
);
};

View File

@ -1,120 +0,0 @@
import {
Dropdown,
IDropdownOption,
IDropdownProps,
IRenderFunction,
ISelectableOption,
Label,
SelectableOptionMenuItemType,
Stack,
Text,
} from "@fluentui/react";
import { GitHubReposTitle } from "Explorer/Tree/ResourceTree";
import React, { FormEvent, FunctionComponent } from "react";
import { IPinnedRepo } from "../../../Juno/JunoClient";
import * as GitHubUtils from "../../../Utils/GitHubUtils";
import { useNotebook } from "../../Notebook/useNotebook";
interface Location {
type: "MyNotebooks" | "GitHub";
// GitHub
owner?: string;
repo?: string;
branch?: string;
}
export interface CopyNotebookPaneProps {
name: string;
pinnedRepos: IPinnedRepo[];
onDropDownChange: (_: FormEvent<HTMLDivElement>, option?: IDropdownOption) => void;
}
export const CopyNotebookPaneComponent: FunctionComponent<CopyNotebookPaneProps> = ({
name,
pinnedRepos,
onDropDownChange,
}: CopyNotebookPaneProps) => {
const BranchNameWhiteSpace = " ";
const onRenderDropDownTitle: IRenderFunction<IDropdownOption[]> = (options: IDropdownOption[]): JSX.Element => {
return <span>{options.length && options[0].title}</span>;
};
const onRenderDropDownOption: IRenderFunction<ISelectableOption> = (option: ISelectableOption): JSX.Element => {
return <span style={{ whiteSpace: "pre-wrap" }}>{option.text}</span>;
};
const getDropDownOptions = (): IDropdownOption[] => {
const options: IDropdownOption[] = [];
options.push({
key: "MyNotebooks-Item",
text: useNotebook.getState().notebookFolderName,
title: useNotebook.getState().notebookFolderName,
data: {
type: "MyNotebooks",
} as Location,
});
if (pinnedRepos && pinnedRepos.length > 0) {
options.push({
key: "GitHub-Header-Divider",
text: undefined,
itemType: SelectableOptionMenuItemType.Divider,
});
options.push({
key: "GitHub-Header",
text: GitHubReposTitle,
itemType: SelectableOptionMenuItemType.Header,
});
pinnedRepos.forEach((pinnedRepo) => {
const repoFullName = GitHubUtils.toRepoFullName(pinnedRepo.owner, pinnedRepo.name);
options.push({
key: `GitHub-Repo-${repoFullName}`,
text: repoFullName,
disabled: true,
});
pinnedRepo.branches.forEach((branch) =>
options.push({
key: `GitHub-Repo-${repoFullName}-${branch.name}`,
text: `${BranchNameWhiteSpace}${branch.name}`,
title: `${repoFullName} - ${branch.name}`,
data: {
type: "GitHub",
owner: pinnedRepo.owner,
repo: pinnedRepo.name,
branch: branch.name,
} as Location,
}),
);
});
}
return options;
};
const dropDownProps: IDropdownProps = {
label: "Location",
ariaLabel: "Location",
placeholder: "Select an option",
onRenderTitle: onRenderDropDownTitle,
onRenderOption: onRenderDropDownOption,
options: getDropDownOptions(),
onChange: onDropDownChange,
};
return (
<div className="paneMainContent">
<Stack tokens={{ childrenGap: 10 }}>
<Stack.Item>
<Label htmlFor="notebookName">Name</Label>
<Text id="notebookName">{name}</Text>
</Stack.Item>
<Dropdown {...dropDownProps} />
</Stack>
</div>
);
};

View File

@ -17,8 +17,6 @@ exports[`GitHub Repos Panel should render Default properly 1`] = `
addRepoProps={
Object {
"container": Explorer {
"_isInitializingNotebooks": false,
"_resetNotebookWorkspace": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function],
"isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function],
@ -35,10 +33,8 @@ exports[`GitHub Repos Panel should render Default properly 1`] = `
"queriesClient": QueriesClient {
"container": [Circular],
},
"refreshNotebookList": [Function],
"resourceTree": ResourceTreeAdapter {
"container": [Circular],
"copyNotebook": [Function],
"parameters": [Function],
},
},

View File

@ -630,7 +630,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
Enable sample database
<InfoTooltip>
This is a sample database and collection with synthetic product data you can use to explore using
NoSQL queries and Copilot. This will appear as another database in the Data Explorer UI, and is
NoSQL queries and Query Advisor. This will appear as another database in the Data Explorer UI, and is
created by, and maintained by Microsoft at no cost to you.
</InfoTooltip>
</div>
@ -640,7 +640,7 @@ export const SettingsPane: FunctionComponent<{ explorer: Explorer }> = ({
label: { padding: 0 },
}}
className="padding"
ariaLabel="Enable sample db for Copilot"
ariaLabel="Enable sample db for Query Advisor"
checked={copilotSampleDBEnabled}
onChange={handleSampleDatabaseChange}
/>

View File

@ -7,8 +7,6 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
errorMessage="Could not create directory "
explorer={
Explorer {
"_isInitializingNotebooks": false,
"_resetNotebookWorkspace": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function],
"isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function],
@ -25,10 +23,8 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
"queriesClient": QueriesClient {
"container": [Circular],
},
"refreshNotebookList": [Function],
"resourceTree": ResourceTreeAdapter {
"container": [Circular],
"copyNotebook": [Function],
"parameters": [Function],
},
}

View File

@ -261,6 +261,7 @@ export const AddTableEntityPanel: FunctionComponent<AddTableEntityPanelProps> =
<TextField
multiline
rows={5}
ariaLabel={entityAttributeProperty}
value={entityAttributeValue}
onChange={(event, newInput?: string) => {
entityChange(newInput, selectedRow, "value");

View File

@ -385,7 +385,7 @@ export const QueryCopilotPromptbar: React.FC<QueryCopilotPromptProps> = ({
hasSmallHeadline={true}
headline="Write a prompt"
>
Write a prompt here and Copilot will generate the query for you. You can also choose from our{" "}
Write a prompt here and Query Advisor will generate the query for you. You can also choose from our{" "}
<Link
onClick={() => {
setShowSamplePrompts(true);

View File

@ -57,12 +57,12 @@ export const QueryCopilotTab: React.FC<QueryCopilotProps> = ({ explorer }: Query
const toggleCopilotButton = {
iconSrc: QueryCommandIcon,
iconAlt: "Copilot",
iconAlt: "Query Advisor",
onCommandClick: () => {
toggleCopilot(true);
},
commandButtonLabel: "Copilot",
ariaLabel: "Copilot",
commandButtonLabel: "Query Advisor",
ariaLabel: "Query Advisor",
hasPopup: false,
disabled: copilotActive,
};

View File

@ -22,8 +22,6 @@ exports[`Query copilot tab snapshot test should render with initial input 1`] =
databaseId="CopilotSampleDb"
explorer={
Explorer {
"_isInitializingNotebooks": false,
"_resetNotebookWorkspace": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function],
"isTabsContentExpanded": [Function],
"onRefreshDatabasesKeyPress": [Function],
@ -40,10 +38,8 @@ exports[`Query copilot tab snapshot test should render with initial input 1`] =
"queriesClient": QueriesClient {
"container": [Circular],
},
"refreshNotebookList": [Function],
"resourceTree": ResourceTreeAdapter {
"container": [Circular],
"copyNotebook": [Function],
"parameters": [Function],
},
}

View File

@ -25,11 +25,9 @@ import * as React from "react";
import ConnectIcon from "../../../images/Connect_color.svg";
import ContainersIcon from "../../../images/Containers.svg";
import LinkIcon from "../../../images/Link_blue.svg";
import NotebookColorIcon from "../../../images/Notebooks.svg";
import PowerShellIcon from "../../../images/PowerShell.svg";
import CopilotIcon from "../../../images/QueryCopilotNewLogo.svg";
import QuickStartIcon from "../../../images/Quickstart_Lightning.svg";
import NotebookIcon from "../../../images/notebook/Notebook-resource.svg";
import CollectionIcon from "../../../images/tree-collection.svg";
import * as Constants from "../../Common/Constants";
import { userContext } from "../../UserContext";
@ -151,9 +149,9 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
{useQueryCopilot.getState().copilotEnabled && (
<SplashScreenButton
imgSrc={CopilotIcon}
title={"Query faster with Copilot"}
title={"Query faster with Query Advisor"}
description={
"Copilot is your AI buddy that helps you write Azure Cosmos DB queries like a pro. Try it using our sample data set now!"
"Query Advisor is your AI buddy that helps you write Azure Cosmos DB queries like a pro. Try it using our sample data set now!"
}
onClick={() => {
const copilotVersion = userContext.features.copilotVersion;
@ -410,14 +408,6 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
},
};
heroes.push(launchQuickstartBtn);
} else if (useNotebook.getState().isPhoenixNotebooks) {
const newNotebookBtn = {
iconSrc: NotebookColorIcon,
title: "New notebook",
description: "Visualize your data stored in Azure Cosmos DB",
onClick: () => this.container.onNewNotebookClicked(),
};
heroes.push(newNotebookBtn);
}
heroes.push(this.getShellCard());
@ -493,28 +483,12 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
};
}
private decorateOpenNotebookActivity({ name, path }: MostRecentActivity.OpenNotebookItem) {
return {
info: path,
iconSrc: NotebookIcon,
title: name,
description: "Notebook",
onClick: () => {
const notebookItem = this.container.createNotebookContentItemFile(name, path);
notebookItem && this.container.openNotebook(notebookItem);
},
};
}
private createRecentItems(): SplashScreenItem[] {
return MostRecentActivity.mostRecentActivity.getItems(userContext.databaseAccount?.id).map((activity) => {
switch (activity.type) {
default: {
const unknownActivity: never = activity;
throw new Error(`Unknown activity: ${unknownActivity}`);
throw new Error(`Unknown activity: ${activity}`);
}
case MostRecentActivity.Type.OpenNotebook:
return this.decorateOpenNotebookActivity(activity);
case MostRecentActivity.Type.OpenCollection:
return this.decorateOpenCollectionActivity(activity);

View File

@ -80,7 +80,8 @@
placeholder:isPreferredApiMongoDB?'Type a query predicate (e.g., {´a´:´foo´}), or choose one from the drop down list, or leave empty to query all documents.':'Type a query predicate (e.g., WHERE c.id=´1´), or choose one from the drop down list, or leave empty to query all documents.'
},
css: { placeholderVisible: filterContent().length === 0 },
textInput: filterContent"
textInput: filterContent,
event: { keydown: onFilterKeyDown }"
/>
<datalist

View File

@ -1,7 +1,7 @@
import { ItemDefinition, PartitionKey, PartitionKeyDefinition, QueryIterator, Resource } from "@azure/cosmos";
import { Platform, configContext } from "ConfigContext";
import { querySampleDocuments, readSampleDocument } from "Explorer/QueryCopilot/QueryCopilotUtilities";
import { KeyboardAction } from "KeyboardShortcuts";
import { KeyboardAction, KeyboardActionGroup, KeyboardHandlerSetter, useKeyboardActionGroup } from "KeyboardShortcuts";
import { QueryConstants } from "Shared/Constants";
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
import * as ko from "knockout";
@ -86,9 +86,11 @@ export default class DocumentsTab extends TabsBase {
private _isQueryCopilotSampleContainer: boolean;
private queryAbortController: AbortController;
private cancelQueryTimeoutID: NodeJS.Timeout;
private setKeyboardActions: KeyboardHandlerSetter;
constructor(options: ViewModels.DocumentsTabOptions) {
super(options);
this.setKeyboardActions = useKeyboardActionGroup(KeyboardActionGroup.ACTIVE_TAB);
this.isPreferredApiMongoDB = userContext.apiType === "Mongo" || options.isPreferredApiMongoDB;
this.idHeader = this.isPreferredApiMongoDB ? "_id" : "id";
@ -463,7 +465,22 @@ export default class DocumentsTab extends TabsBase {
private initializeNewDocument = (): void => {
this.selectedDocumentId(null);
const defaultDocument: string = this.renderObjectForEditor({ id: "replace_with_new_document_id" }, null, 4);
const newDocument: any = {
id: "replace_with_new_document_id",
};
this.partitionKeyProperties.forEach((partitionKeyProperty) => {
let target = newDocument;
const keySegments = partitionKeyProperty.split(".");
const finalSegment = keySegments.pop();
// Initialize nested objects as needed
keySegments.forEach((segment) => {
target = target[segment] = target[segment] || {};
});
target[finalSegment] = "replace_with_new_partition_key_value";
});
const defaultDocument: string = this.renderObjectForEditor(newDocument, null, 4);
this.initialDocumentContent(defaultDocument);
this.selectedDocumentContent.setBaseline(defaultDocument);
this.editorState(ViewModels.DocumentExplorerState.newDocumentValid);
@ -650,9 +667,38 @@ export default class DocumentsTab extends TabsBase {
this.collection && this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Documents);
}
public onFilterKeyDown(model: unknown, e: KeyboardEvent): boolean {
if (e.key === "Enter") {
this.refreshDocumentsGrid(true);
// Suppress the default behavior of the key
return false;
} else if (e.key === "Escape") {
this.onHideFilterClick();
// Suppress the default behavior of the key
return false;
} else {
// Allow the default behavior of the key
return true;
}
}
public async onActivate(): Promise<void> {
super.onActivate();
this.setKeyboardActions({
[KeyboardAction.SEARCH]: () => {
this.onShowFilterClick();
return true;
},
[KeyboardAction.CLEAR_SEARCH]: () => {
this.filterContent("");
this.refreshDocumentsGrid(true);
return true;
},
});
if (!this._documentsIterator) {
try {
await this.autoPopulateContent();

View File

@ -1,3 +1,4 @@
import { useMongoProxyEndpoint } from "Common/MongoProxyClient";
import React, { Component } from "react";
import * as Constants from "../../../Common/Constants";
import { configContext } from "../../../ConfigContext";
@ -34,7 +35,7 @@ export interface IMongoShellTabAccessor {
}
export interface IMongoShellTabComponentStates {
url: URL;
url: string;
}
export interface IMongoShellTabComponentProps {
@ -54,8 +55,7 @@ export default class MongoShellTabComponent extends Component<
constructor(props: IMongoShellTabComponentProps) {
super(props);
this._logTraces = new Map();
this._useMongoProxyEndpoint = userContext.features.enableLegacyMongoShell;
// this._useMongoProxyEndpoint = useMongoProxyEndpoint("legacyMongoShell");
this._useMongoProxyEndpoint = useMongoProxyEndpoint("legacyMongoShell");
this.state = {
url: getMongoShellUrl(this._useMongoProxyEndpoint),
@ -221,7 +221,7 @@ export default class MongoShellTabComponent extends Component<
name="explorer"
className="iframe"
style={{ width: "100%", height: "100%", border: 0, padding: 0, margin: 0, overflow: "hidden" }}
src={this.state.url.toString()}
src={this.state.url}
id={this.props.tabsBaseInstance.tabId}
onLoad={(event) => this.setContentFocus(event)}
title="Mongo Shell"

View File

@ -3,7 +3,6 @@ import { updateUserContext, userContext } from "../../../UserContext";
import { getMongoShellUrl } from "./getMongoShellUrl";
const mongoBackendEndpoint = "https://localhost:1234";
const hostedExplorerURL = "https://cosmos.azure.com/";
describe("getMongoShellUrl", () => {
let queryString = "";
@ -13,7 +12,6 @@ describe("getMongoShellUrl", () => {
updateConfigContext({
BACKEND_ENDPOINT: mongoBackendEndpoint,
hostedExplorerURL: hostedExplorerURL,
platform: Platform.Hosted,
});

View File

@ -1,15 +1,11 @@
import { configContext } from "ConfigContext";
import { userContext } from "../../../UserContext";
export function getMongoShellUrl(useMongoProxyEndpoint?: boolean): URL {
export function getMongoShellUrl(useMongoProxyEndpoint?: boolean): string {
const { databaseAccount: account } = userContext;
const resourceId = account?.id;
const accountName = account?.name;
const mongoEndpoint = account?.properties?.mongoEndpoint || account?.properties?.documentEndpoint;
const queryString = `resourceId=${resourceId}&accountName=${accountName}&mongoEndpoint=${mongoEndpoint}`;
const path: string = useMongoProxyEndpoint
? `/mongoshell/index.html?${queryString}`
: `/mongoshell/indexv2.html?${queryString}`;
return new URL(path, configContext.hostedExplorerURL);
return useMongoProxyEndpoint ? `/mongoshell/index.html?${queryString}` : `/mongoshell/indexv2.html?${queryString}`;
}

View File

@ -1,31 +1,11 @@
import { stringifyNotebook, toJS } from "@nteract/commutable";
import * as ko from "knockout";
import * as Q from "q";
import { userContext } from "UserContext";
import ClearAllOutputsIcon from "../../../images/notebook/Notebook-clear-all-outputs.svg";
import CopyIcon from "../../../images/notebook/Notebook-copy.svg";
import CutIcon from "../../../images/notebook/Notebook-cut.svg";
import NewCellIcon from "../../../images/notebook/Notebook-insert-cell.svg";
import PasteIcon from "../../../images/notebook/Notebook-paste.svg";
import RestartIcon from "../../../images/notebook/Notebook-restart.svg";
import RunAllIcon from "../../../images/notebook/Notebook-run-all.svg";
import RunIcon from "../../../images/notebook/Notebook-run.svg";
import { default as InterruptKernelIcon, default as KillKernelIcon } from "../../../images/notebook/Notebook-stop.svg";
import SaveIcon from "../../../images/save-cosmos.svg";
import { useNotebookSnapshotStore } from "../../hooks/useNotebookSnapshotStore";
import { Action, ActionModifiers, Source } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import * as NotebookConfigurationUtils from "../../Utils/NotebookConfigurationUtils";
import { logConsoleInfo } from "../../Utils/NotificationConsoleUtils";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import { useDialog } from "../Controls/Dialog";
import * as CommandBarComponentButtonFactory from "../Menus/CommandBar/CommandBarComponentButtonFactory";
import { KernelSpecsDisplay } from "../Notebook/NotebookClientV2";
import * as CdbActions from "../Notebook/NotebookComponent/actions";
import { NotebookComponentAdapter } from "../Notebook/NotebookComponent/NotebookComponentAdapter";
import { CdbAppState, SnapshotRequest } from "../Notebook/NotebookComponent/types";
import { NotebookContentItem } from "../Notebook/NotebookContentItem";
import { NotebookUtil } from "../Notebook/NotebookUtil";
import { useNotebook } from "../Notebook/useNotebook";
import NotebookTabBase, { NotebookTabBaseOptions } from "./NotebookTabBase";
@ -90,275 +70,7 @@ export default class NotebookTabV2 extends NotebookTabBase {
}
protected getTabsButtons(): CommandButtonComponentProps[] {
const availableKernels = NotebookTabV2.clientManager.getAvailableKernelSpecs();
const isNotebookUntrusted = this.notebookComponentAdapter.isNotebookUntrusted();
const runBtnTooltip = isNotebookUntrusted ? NotebookUtil.UntrustedNotebookRunHint : undefined;
const saveLabel = "Save";
const copyToLabel = "Copy to ...";
const publishLabel = "Publish to gallery";
const kernelLabel = "No Kernel";
const runLabel = "Run";
const runActiveCellLabel = "Run Active Cell";
const runAllLabel = "Run All";
const interruptKernelLabel = "Interrupt Kernel";
const killKernelLabel = "Halt Kernel";
const restartKernelLabel = "Restart Kernel";
const clearLabel = "Clear outputs";
const newCellLabel = "New Cell";
const cellTypeLabel = "Cell Type";
const codeLabel = "Code";
const markdownLabel = "Markdown";
const rawLabel = "Raw";
const copyLabel = "Copy";
const cutLabel = "Cut";
const pasteLabel = "Paste";
const cellCodeType = "code";
const cellMarkdownType = "markdown";
const cellRawType = "raw";
const saveButtonChildren = [];
if (this.container.notebookManager?.gitHubOAuthService.isLoggedIn()) {
saveButtonChildren.push({
iconName: copyToLabel,
onCommandClick: () => this.copyNotebook(),
commandButtonLabel: copyToLabel,
hasPopup: false,
disabled: false,
ariaLabel: copyToLabel,
});
}
if (userContext.features.publicGallery) {
saveButtonChildren.push({
iconName: "PublishContent",
onCommandClick: async () => await this.publishToGallery(),
commandButtonLabel: publishLabel,
hasPopup: false,
disabled: false,
ariaLabel: publishLabel,
});
}
let buttons: CommandButtonComponentProps[] = [
{
iconSrc: SaveIcon,
iconAlt: saveLabel,
onCommandClick: () => this.notebookComponentAdapter.notebookSave(),
commandButtonLabel: saveLabel,
hasPopup: false,
disabled: false,
ariaLabel: saveLabel,
children: saveButtonChildren.length && [
{
iconName: "Save",
onCommandClick: () => this.notebookComponentAdapter.notebookSave(),
commandButtonLabel: saveLabel,
hasPopup: false,
disabled: false,
ariaLabel: saveLabel,
},
...saveButtonChildren,
],
},
{
iconSrc: null,
iconAlt: kernelLabel,
onCommandClick: () => {},
commandButtonLabel: null,
hasPopup: false,
disabled: availableKernels.length < 1,
isDropdown: true,
dropdownPlaceholder: kernelLabel,
dropdownSelectedKey: this.notebookComponentAdapter.getSelectedKernelName(), //this.currentKernelName,
dropdownWidth: 100,
children: availableKernels.map(
(kernel: KernelSpecsDisplay) =>
({
iconSrc: null,
iconAlt: kernel.displayName,
onCommandClick: () => this.notebookComponentAdapter.notebookChangeKernel(kernel.name),
commandButtonLabel: kernel.displayName,
dropdownItemKey: kernel.name,
hasPopup: false,
disabled: false,
ariaLabel: kernel.displayName,
}) as CommandButtonComponentProps,
),
ariaLabel: kernelLabel,
},
{
iconSrc: RunIcon,
iconAlt: runLabel,
onCommandClick: () => {
this.notebookComponentAdapter.notebookRunAndAdvance();
this.traceTelemetry(Action.ExecuteCell);
},
commandButtonLabel: runLabel,
tooltipText: runBtnTooltip,
ariaLabel: runLabel,
hasPopup: false,
disabled: isNotebookUntrusted,
children: [
{
iconSrc: RunIcon,
iconAlt: runActiveCellLabel,
onCommandClick: () => {
this.notebookComponentAdapter.notebookRunAndAdvance();
this.traceTelemetry(Action.ExecuteCell);
},
commandButtonLabel: runActiveCellLabel,
hasPopup: false,
disabled: false,
ariaLabel: runActiveCellLabel,
},
{
iconSrc: RunAllIcon,
iconAlt: runAllLabel,
onCommandClick: () => {
this.notebookComponentAdapter.notebookRunAll();
this.traceTelemetry(Action.ExecuteAllCells);
},
commandButtonLabel: runAllLabel,
hasPopup: false,
disabled: false,
ariaLabel: runAllLabel,
},
{
iconSrc: InterruptKernelIcon,
iconAlt: interruptKernelLabel,
onCommandClick: () => this.notebookComponentAdapter.notebookInterruptKernel(),
commandButtonLabel: interruptKernelLabel,
hasPopup: false,
disabled: false,
ariaLabel: interruptKernelLabel,
},
{
iconSrc: KillKernelIcon,
iconAlt: killKernelLabel,
onCommandClick: () => this.notebookComponentAdapter.notebookKillKernel(),
commandButtonLabel: killKernelLabel,
hasPopup: false,
disabled: false,
ariaLabel: killKernelLabel,
},
{
iconSrc: RestartIcon,
iconAlt: restartKernelLabel,
onCommandClick: () => this.notebookComponentAdapter.notebookRestartKernel(),
commandButtonLabel: restartKernelLabel,
hasPopup: false,
disabled: false,
ariaLabel: restartKernelLabel,
},
],
},
{
iconSrc: ClearAllOutputsIcon,
iconAlt: clearLabel,
onCommandClick: () => this.notebookComponentAdapter.notebookClearAllOutputs(),
commandButtonLabel: clearLabel,
hasPopup: false,
disabled: false,
ariaLabel: clearLabel,
},
{
iconSrc: NewCellIcon,
iconAlt: newCellLabel,
onCommandClick: () => this.notebookComponentAdapter.notebookInsertBelow(),
commandButtonLabel: newCellLabel,
ariaLabel: newCellLabel,
hasPopup: false,
disabled: false,
},
CommandBarComponentButtonFactory.createDivider(),
{
iconSrc: null,
iconAlt: null,
onCommandClick: () => {},
commandButtonLabel: null,
ariaLabel: cellTypeLabel,
hasPopup: false,
disabled: false,
isDropdown: true,
dropdownPlaceholder: cellTypeLabel,
dropdownSelectedKey: this.notebookComponentAdapter.getActiveCellTypeStr(),
dropdownWidth: 110,
children: [
{
iconSrc: null,
iconAlt: null,
onCommandClick: () => this.notebookComponentAdapter.notebookChangeCellType(cellCodeType),
commandButtonLabel: codeLabel,
ariaLabel: codeLabel,
dropdownItemKey: cellCodeType,
hasPopup: false,
disabled: false,
},
{
iconSrc: null,
iconAlt: null,
onCommandClick: () => this.notebookComponentAdapter.notebookChangeCellType(cellMarkdownType),
commandButtonLabel: markdownLabel,
ariaLabel: markdownLabel,
dropdownItemKey: cellMarkdownType,
hasPopup: false,
disabled: false,
},
{
iconSrc: null,
iconAlt: null,
onCommandClick: () => this.notebookComponentAdapter.notebookChangeCellType(cellRawType),
commandButtonLabel: rawLabel,
ariaLabel: rawLabel,
dropdownItemKey: cellRawType,
hasPopup: false,
disabled: false,
},
],
},
{
iconSrc: CopyIcon,
iconAlt: copyLabel,
onCommandClick: () => this.notebookComponentAdapter.notebokCopy(),
commandButtonLabel: copyLabel,
ariaLabel: copyLabel,
hasPopup: false,
disabled: false,
children: [
{
iconSrc: CopyIcon,
iconAlt: copyLabel,
onCommandClick: () => this.notebookComponentAdapter.notebokCopy(),
commandButtonLabel: copyLabel,
ariaLabel: copyLabel,
hasPopup: false,
disabled: false,
},
{
iconSrc: CutIcon,
iconAlt: cutLabel,
onCommandClick: () => this.notebookComponentAdapter.notebookCut(),
commandButtonLabel: cutLabel,
ariaLabel: cutLabel,
hasPopup: false,
disabled: false,
},
{
iconSrc: PasteIcon,
iconAlt: pasteLabel,
onCommandClick: () => this.notebookComponentAdapter.notebookPaste(),
commandButtonLabel: pasteLabel,
ariaLabel: pasteLabel,
hasPopup: false,
disabled: false,
},
],
},
// TODO: Uncomment when undo/redo is reimplemented in nteract
];
return buttons;
return [];
}
protected buildCommandBarOptions(): void {
@ -382,50 +94,4 @@ export default class NotebookTabV2 extends NotebookTabBase {
sparkClusterConnectionInfo,
);
}
private publishToGallery = async () => {
TelemetryProcessor.trace(Action.NotebooksGalleryClickPublishToGallery, ActionModifiers.Mark, {
source: Source.CommandBarMenu,
});
const notebookReduxStore = NotebookTabV2.clientManager.getStore();
const unsubscribe = notebookReduxStore.subscribe(() => {
const cdbState = (notebookReduxStore.getState() as CdbAppState).cdb;
useNotebookSnapshotStore.setState({
snapshot: cdbState.notebookSnapshot?.imageSrc,
error: cdbState.notebookSnapshotError,
});
});
const notebookContent = this.notebookComponentAdapter.getContent();
const notebookContentRef = this.notebookComponentAdapter.contentRef;
const onPanelClose = (): void => {
unsubscribe();
useNotebookSnapshotStore.setState({
snapshot: undefined,
error: undefined,
});
notebookReduxStore.dispatch(CdbActions.takeNotebookSnapshot(undefined));
};
await this.container.publishNotebook(
notebookContent.name,
notebookContent.content,
notebookContentRef,
(request: SnapshotRequest) => notebookReduxStore.dispatch(CdbActions.takeNotebookSnapshot(request)),
onPanelClose,
);
};
private copyNotebook = () => {
const notebookContent = this.notebookComponentAdapter.getContent();
let content: string;
if (typeof notebookContent.content === "string") {
content = notebookContent.content;
} else {
content = stringifyNotebook(toJS(notebookContent.content));
}
this.container.copyNotebook(notebookContent.name, content);
};
}

View File

@ -381,9 +381,13 @@ export const QueryResultSection: React.FC<QueryResultProps> = ({
<img className="paneErrorIcon" src={InfoColor} alt="Error" />
</span>
<span className="warningErrorDetailsLinkContainer">
We have detected you may be using a subquery. Non-correlated subqueries are not currently supported.
<a href="https://docs.microsoft.com/en-us/azure/cosmos-db/sql-query-subquery">
Please see Cosmos sub query documentation for further information
We detected you may be using a subquery. To learn more about subqueries effectively,{" "}
<a
href="https://learn.microsoft.com/azure/cosmos-db/nosql/query/subquery"
target="_blank"
rel="noreferrer"
>
visit the documentation
</a>
</span>
</div>

View File

@ -22,6 +22,7 @@ import "react-splitter-layout/lib/index.css";
import { format } from "react-string-format";
import QueryCommandIcon from "../../../../images/CopilotCommand.svg";
import LaunchCopilot from "../../../../images/CopilotTabIcon.svg";
import DownloadQueryIcon from "../../../../images/DownloadQuery.svg";
import CancelQueryIcon from "../../../../images/Entity_cancel.svg";
import ExecuteQueryIcon from "../../../../images/ExecuteQuery.svg";
import SaveQueryIcon from "../../../../images/save-cosmos.svg";
@ -225,6 +226,20 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
}
};
public onDownloadQueryClick = (): void => {
const text = this.getCurrentEditorQuery();
const queryFile = new File([text], `SavedQuery.txt`, { type: "text/plain" });
// It appears the most consistent to download a file from a blob is to create an anchor element and simulate clicking it
const blobUrl = URL.createObjectURL(queryFile);
const anchor = document.createElement("a");
anchor.href = blobUrl;
anchor.download = queryFile.name;
document.body.appendChild(anchor); // Must put the anchor in the document.
anchor.click();
document.body.removeChild(anchor); // Clean up the anchor.
};
public onSaveQueryClick = (): void => {
useSidePanel.getState().openSidePanel("Save Query", <SaveQueryPane explorer={this.props.collection.container} />);
};
@ -405,15 +420,28 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
});
}
if (this.saveQueryButton.visible && configContext.platform !== Platform.Fabric) {
const label = "Save Query";
if (this.saveQueryButton.visible) {
if (configContext.platform !== Platform.Fabric) {
const label = "Save Query";
buttons.push({
iconSrc: SaveQueryIcon,
iconAlt: label,
keyboardAction: KeyboardAction.SAVE_ITEM,
onCommandClick: this.onSaveQueryClick,
commandButtonLabel: label,
ariaLabel: label,
hasPopup: false,
disabled: !this.saveQueryButton.enabled,
});
}
buttons.push({
iconSrc: SaveQueryIcon,
iconAlt: label,
keyboardAction: KeyboardAction.SAVE_ITEM,
onCommandClick: this.onSaveQueryClick,
commandButtonLabel: label,
ariaLabel: label,
iconSrc: DownloadQueryIcon,
iconAlt: "Download Query",
keyboardAction: KeyboardAction.DOWNLOAD_ITEM,
onCommandClick: this.onDownloadQueryClick,
commandButtonLabel: "Download Query",
ariaLabel: "Download Query",
hasPopup: false,
disabled: !this.saveQueryButton.enabled,
});
@ -455,13 +483,13 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
if (this.props.copilotEnabled) {
const toggleCopilotButton: CommandButtonComponentProps = {
iconSrc: QueryCommandIcon,
iconAlt: "Copilot",
iconAlt: "Query Advisor",
keyboardAction: KeyboardAction.TOGGLE_COPILOT,
onCommandClick: () => {
this._toggleCopilot(!this.state.copilotActive);
},
commandButtonLabel: this.state.copilotActive ? "Disable Copilot" : "Enable Copilot",
ariaLabel: this.state.copilotActive ? "Disable Copilot" : "Enable Copilot",
commandButtonLabel: this.state.copilotActive ? "Disable Query Advisor" : "Enable Query Advisor",
ariaLabel: this.state.copilotActive ? "Disable Query Advisor" : "Enable Query Advisor",
hasPopup: false,
};
buttons.push(toggleCopilotButton);
@ -525,6 +553,8 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
}
}
this.saveQueryButton.enabled = newContent.length > 0;
useCommandBar.getState().setContextButtons(this.getTabsButtons());
}

View File

@ -1,3 +1,4 @@
import { KeyboardActionGroup, clearKeyboardActionGroup } from "KeyboardShortcuts";
import * as ko from "knockout";
import * as Constants from "../../Common/Constants";
import * as ThemeUtility from "../../Common/ThemeUtility";
@ -107,6 +108,7 @@ export default class TabsBase extends WaitsForTemplateViewModel {
}
public onActivate(): void {
clearKeyboardActionGroup(KeyboardActionGroup.ACTIVE_TAB);
this.updateSelectedNode();
this.collection?.selectedSubnodeKind(this.tabKind);
this.database?.selectedSubnodeKind(this.tabKind);

View File

@ -1,42 +1,23 @@
import { Callout, DirectionalHint, ICalloutProps, ILinkProps, Link, Stack, Text } from "@fluentui/react";
import { SampleDataTree } from "Explorer/Tree/SampleDataTree";
import { getItemName } from "Utils/APITypeUtils";
import { useQueryCopilot } from "hooks/useQueryCopilot";
import * as React from "react";
import shallow from "zustand/shallow";
import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg";
import GalleryIcon from "../../../images/GalleryIcon.svg";
import DeleteIcon from "../../../images/delete.svg";
import CopyIcon from "../../../images/notebook/Notebook-copy.svg";
import NewNotebookIcon from "../../../images/notebook/Notebook-new.svg";
import NotebookIcon from "../../../images/notebook/Notebook-resource.svg";
import FileIcon from "../../../images/notebook/file-cosmos.svg";
import PublishIcon from "../../../images/notebook/publish_content.svg";
import RefreshIcon from "../../../images/refresh-cosmos.svg";
import CollectionIcon from "../../../images/tree-collection.svg";
import { Areas, ConnectionStatusType, Notebook } from "../../Common/Constants";
import { isPublicInternetAccessAllowed } from "../../Common/DatabaseAccountUtility";
import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility";
import { Action, ActionModifiers, Source } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext";
import { isServerlessAccount } from "../../Utils/CapabilityUtils";
import * as GitHubUtils from "../../Utils/GitHubUtils";
import { useSidePanel } from "../../hooks/useSidePanel";
import { useTabs } from "../../hooks/useTabs";
import * as ResourceTreeContextMenuButtonFactory from "../ContextMenuButtonFactory";
import { AccordionComponent, AccordionItemComponent } from "../Controls/Accordion/AccordionComponent";
import { useDialog } from "../Controls/Dialog";
import { TreeComponent, TreeNode, TreeNodeMenuItem } from "../Controls/TreeComponent/TreeComponent";
import { TreeComponent, TreeNode } from "../Controls/TreeComponent/TreeComponent";
import Explorer from "../Explorer";
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
import { mostRecentActivity } from "../MostRecentActivity/MostRecentActivity";
import { NotebookContentItem, NotebookContentItemType } from "../Notebook/NotebookContentItem";
import { NotebookUtil } from "../Notebook/NotebookUtil";
import { useNotebook } from "../Notebook/useNotebook";
import { GitHubReposPanel } from "../Panes/GitHubReposPanel/GitHubReposPanel";
import TabsBase from "../Tabs/TabsBase";
import { useDatabases } from "../useDatabases";
import { useSelectedNode } from "../useSelectedNode";
@ -45,391 +26,21 @@ import StoredProcedure from "./StoredProcedure";
import Trigger from "./Trigger";
import UserDefinedFunction from "./UserDefinedFunction";
export const MyNotebooksTitle = "My Notebooks";
export const GitHubReposTitle = "GitHub repos";
interface ResourceTreeProps {
container: Explorer;
}
export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: ResourceTreeProps): JSX.Element => {
const databases = useDatabases((state) => state.databases);
const {
isNotebookEnabled,
myNotebooksContentRoot,
galleryContentRoot,
gitHubNotebooksContentRoot,
updateNotebookItem,
} = useNotebook(
const { isNotebookEnabled } = useNotebook(
(state) => ({
isNotebookEnabled: state.isNotebookEnabled,
myNotebooksContentRoot: state.myNotebooksContentRoot,
galleryContentRoot: state.galleryContentRoot,
gitHubNotebooksContentRoot: state.gitHubNotebooksContentRoot,
updateNotebookItem: state.updateNotebookItem,
}),
shallow,
);
const { activeTab, refreshActiveTab } = useTabs();
const { refreshActiveTab } = useTabs();
const showScriptNodes =
configContext.platform !== Platform.Fabric && (userContext.apiType === "SQL" || userContext.apiType === "Gremlin");
const pseudoDirPath = "PsuedoDir";
const buildGalleryCallout = (): JSX.Element => {
if (
LocalStorageUtility.hasItem(StorageKey.GalleryCalloutDismissed) &&
LocalStorageUtility.getEntryBoolean(StorageKey.GalleryCalloutDismissed)
) {
return undefined;
}
const calloutProps: ICalloutProps = {
calloutMaxWidth: 350,
ariaLabel: "New gallery",
role: "alertdialog",
gapSpace: 0,
target: ".galleryHeader",
directionalHint: DirectionalHint.leftTopEdge,
onDismiss: () => {
LocalStorageUtility.setEntryBoolean(StorageKey.GalleryCalloutDismissed, true);
},
setInitialFocus: true,
};
const openGalleryProps: ILinkProps = {
onClick: () => {
LocalStorageUtility.setEntryBoolean(StorageKey.GalleryCalloutDismissed, true);
container.openGallery();
},
};
return (
<Callout {...calloutProps}>
<Stack tokens={{ childrenGap: 10, padding: 20 }}>
<Text variant="xLarge" block>
New gallery
</Text>
<Text block>
Sample notebooks are now combined in gallery. View and try out samples provided by Microsoft and other
contributors.
</Text>
<Link {...openGalleryProps}>Open gallery</Link>
</Stack>
</Callout>
);
};
const buildNotebooksTree = (): TreeNode => {
const notebooksTree: TreeNode = {
label: undefined,
isExpanded: true,
children: [],
};
if (!useNotebook.getState().isPhoenixNotebooks) {
notebooksTree.children.push(buildNotebooksTemporarilyDownTree());
} else {
if (galleryContentRoot) {
notebooksTree.children.push(buildGalleryNotebooksTree());
}
if (
myNotebooksContentRoot &&
useNotebook.getState().isPhoenixNotebooks &&
useNotebook.getState().connectionInfo.status === ConnectionStatusType.Connected
) {
notebooksTree.children.push(buildMyNotebooksTree());
}
if (container.notebookManager?.gitHubOAuthService.isLoggedIn()) {
// collapse all other notebook nodes
notebooksTree.children.forEach((node) => (node.isExpanded = false));
notebooksTree.children.push(buildGitHubNotebooksTree(true));
}
}
return notebooksTree;
};
const buildNotebooksTemporarilyDownTree = (): TreeNode => {
return {
label: Notebook.temporarilyDownMsg,
className: "clickDisabled",
};
};
const buildGalleryNotebooksTree = (): TreeNode => {
return {
label: "Gallery",
iconSrc: GalleryIcon,
className: "notebookHeader galleryHeader",
onClick: () => container.openGallery(),
isSelected: () => activeTab?.tabKind === ViewModels.CollectionTabKind.Gallery,
};
};
const buildMyNotebooksTree = (): TreeNode => {
const myNotebooksTree: TreeNode = buildNotebookDirectoryNode(
myNotebooksContentRoot,
(item: NotebookContentItem) => {
container.openNotebook(item);
},
);
myNotebooksTree.isExpanded = true;
myNotebooksTree.isAlphaSorted = true;
// Remove "Delete" menu item from context menu
myNotebooksTree.contextMenu = myNotebooksTree.contextMenu.filter((menuItem) => menuItem.label !== "Delete");
return myNotebooksTree;
};
const buildGitHubNotebooksTree = (isConnected: boolean): TreeNode => {
const gitHubNotebooksTree: TreeNode = buildNotebookDirectoryNode(
gitHubNotebooksContentRoot,
(item: NotebookContentItem) => {
container.openNotebook(item);
},
true,
);
const manageGitContextMenu: TreeNodeMenuItem[] = [
{
label: "Manage GitHub settings",
onClick: () =>
useSidePanel
.getState()
.openSidePanel(
"Manage GitHub settings",
<GitHubReposPanel
explorer={container}
gitHubClientProp={container.notebookManager.gitHubClient}
junoClientProp={container.notebookManager.junoClient}
/>,
),
},
{
label: "Disconnect from GitHub",
onClick: () => {
TelemetryProcessor.trace(Action.NotebooksGitHubDisconnect, ActionModifiers.Mark, {
dataExplorerArea: Areas.Notebook,
});
container.notebookManager?.gitHubOAuthService.logout();
},
},
];
gitHubNotebooksTree.contextMenu = manageGitContextMenu;
gitHubNotebooksTree.isExpanded = true;
gitHubNotebooksTree.isAlphaSorted = true;
return gitHubNotebooksTree;
};
const buildChildNodes = (
item: NotebookContentItem,
onFileClick: (item: NotebookContentItem) => void,
isGithubTree?: boolean,
): TreeNode[] => {
if (!item || !item.children) {
return [];
} else {
return item.children.map((item) => {
const result =
item.type === NotebookContentItemType.Directory
? buildNotebookDirectoryNode(item, onFileClick, isGithubTree)
: buildNotebookFileNode(item, onFileClick, isGithubTree);
result.timestamp = item.timestamp;
return result;
});
}
};
const buildNotebookFileNode = (
item: NotebookContentItem,
onFileClick: (item: NotebookContentItem) => void,
isGithubTree?: boolean,
): TreeNode => {
return {
label: item.name,
iconSrc: NotebookUtil.isNotebookFile(item.path) ? NotebookIcon : FileIcon,
className: "notebookHeader",
onClick: () => onFileClick(item),
isSelected: () => {
return (
activeTab &&
activeTab.tabKind === ViewModels.CollectionTabKind.NotebookV2 &&
/* TODO Redesign Tab interface so that resource tree doesn't need to know about NotebookV2Tab.
NotebookV2Tab could be dynamically imported, but not worth it to just get this type right.
*/
(activeTab as any).notebookPath() === item.path
);
},
contextMenu: createFileContextMenu(container, item, isGithubTree),
data: item,
};
};
const createFileContextMenu = (
container: Explorer,
item: NotebookContentItem,
isGithubTree?: boolean,
): TreeNodeMenuItem[] => {
let items: TreeNodeMenuItem[] = [
{
label: "Rename",
iconSrc: NotebookIcon,
onClick: () => container.renameNotebook(item, isGithubTree),
},
{
label: "Delete",
iconSrc: DeleteIcon,
onClick: () => {
useDialog
.getState()
.showOkCancelModalDialog(
"Confirm delete",
`Are you sure you want to delete "${item.name}"`,
"Delete",
() => container.deleteNotebookFile(item, isGithubTree),
"Cancel",
undefined,
);
},
},
{
label: "Copy to ...",
iconSrc: CopyIcon,
onClick: () => copyNotebook(container, item),
},
{
label: "Download",
iconSrc: NotebookIcon,
onClick: () => container.downloadFile(item),
},
];
if (item.type === NotebookContentItemType.Notebook && userContext.features.publicGallery) {
items.push({
label: "Publish to gallery",
iconSrc: PublishIcon,
onClick: async () => {
TelemetryProcessor.trace(Action.NotebooksGalleryClickPublishToGallery, ActionModifiers.Mark, {
source: Source.ResourceTreeMenu,
});
const content = await container.readFile(item);
if (content) {
await container.publishNotebook(item.name, content);
}
},
});
}
// "Copy to ..." isn't needed if github locations are not available
if (!container.notebookManager?.gitHubOAuthService.isLoggedIn()) {
items = items.filter((item) => item.label !== "Copy to ...");
}
return items;
};
const copyNotebook = async (container: Explorer, item: NotebookContentItem) => {
const content = await container.readFile(item);
if (content) {
container.copyNotebook(item.name, content);
}
};
const createDirectoryContextMenu = (
container: Explorer,
item: NotebookContentItem,
isGithubTree?: boolean,
): TreeNodeMenuItem[] => {
let items: TreeNodeMenuItem[] = [
{
label: "Refresh",
iconSrc: RefreshIcon,
onClick: () => loadSubitems(item, isGithubTree),
},
{
label: "Delete",
iconSrc: DeleteIcon,
onClick: () => {
useDialog
.getState()
.showOkCancelModalDialog(
"Confirm delete",
`Are you sure you want to delete "${item.name}?"`,
"Delete",
() => container.deleteNotebookFile(item, isGithubTree),
"Cancel",
undefined,
);
},
},
{
label: "Rename",
iconSrc: NotebookIcon,
onClick: () => container.renameNotebook(item, isGithubTree),
},
{
label: "New Directory",
iconSrc: NewNotebookIcon,
onClick: () => container.onCreateDirectory(item, isGithubTree),
},
{
label: "Upload File",
iconSrc: NewNotebookIcon,
onClick: () => container.openUploadFilePanel(item),
},
];
//disallow renaming of temporary notebook workspace
if (item?.path === useNotebook.getState().notebookBasePath) {
items = items.filter((item) => item.label !== "Rename");
}
// For GitHub paths remove "Delete", "Rename", "New Directory", "Upload File"
if (GitHubUtils.fromContentUri(item.path)) {
items = items.filter(
(item) =>
item.label !== "Delete" &&
item.label !== "Rename" &&
item.label !== "New Directory" &&
item.label !== "Upload File",
);
}
return items;
};
const buildNotebookDirectoryNode = (
item: NotebookContentItem,
onFileClick: (item: NotebookContentItem) => void,
isGithubTree?: boolean,
): TreeNode => {
return {
label: item.name,
iconSrc: undefined,
className: "notebookHeader",
isAlphaSorted: true,
isLeavesParentsSeparate: true,
onClick: () => {
if (!item.children) {
loadSubitems(item, isGithubTree);
}
},
isSelected: () => {
return (
activeTab &&
activeTab.tabKind === ViewModels.CollectionTabKind.NotebookV2 &&
/* TODO Redesign Tab interface so that resource tree doesn't need to know about NotebookV2Tab.
NotebookV2Tab could be dynamically imported, but not worth it to just get this type right.
*/
(activeTab as any).notebookPath() === item.path
);
},
contextMenu: item.path !== pseudoDirPath ? createDirectoryContextMenu(container, item, isGithubTree) : undefined,
data: item,
children: buildChildNodes(item, onFileClick, isGithubTree),
};
};
const buildDataTree = (): TreeNode => {
const databaseTreeNodes: TreeNode[] = databases.map((database: ViewModels.Database) => {
@ -757,11 +368,6 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
return traverse(schema);
};
const loadSubitems = async (item: NotebookContentItem, isGithubTree?: boolean): Promise<void> => {
const updatedItem = await container.notebookManager?.notebookContentClient?.updateItemChildren(item);
updateNotebookItem(updatedItem, isGithubTree);
};
const dataRootNode = buildDataTree();
const isSampleDataEnabled =
useQueryCopilot().copilotEnabled &&
@ -775,46 +381,16 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
{!isNotebookEnabled && !isSampleDataEnabled && (
<TreeComponent className="dataResourceTree" rootNode={dataRootNode} />
)}
{isNotebookEnabled && !isSampleDataEnabled && (
<>
<AccordionComponent>
<AccordionItemComponent title={"DATA"} isExpanded={!gitHubNotebooksContentRoot}>
<TreeComponent className="dataResourceTree" rootNode={dataRootNode} />
</AccordionItemComponent>
</AccordionComponent>
{/* {buildGalleryCallout()} */}
</>
)}
{!isNotebookEnabled && isSampleDataEnabled && (
<>
<AccordionComponent>
<AccordionItemComponent title={"MY DATA"} isExpanded={!gitHubNotebooksContentRoot}>
<AccordionItemComponent title={"MY DATA"} isExpanded={true}>
<TreeComponent className="dataResourceTree" rootNode={dataRootNode} />
</AccordionItemComponent>
<AccordionItemComponent title={"SAMPLE DATA"} containerStyles={{ display: "table" }}>
<SampleDataTree sampleDataResourceTokenCollection={sampleDataResourceTokenCollection} />
</AccordionItemComponent>
</AccordionComponent>
{/* {buildGalleryCallout()} */}
</>
)}
{isNotebookEnabled && isSampleDataEnabled && (
<>
<AccordionComponent>
<AccordionItemComponent title={"MY DATA"} isExpanded={!gitHubNotebooksContentRoot}>
<TreeComponent className="dataResourceTree" rootNode={dataRootNode} />
</AccordionItemComponent>
<AccordionItemComponent title={"SAMPLE DATA"} containerStyles={{ display: "table" }}>
<SampleDataTree sampleDataResourceTokenCollection={sampleDataResourceTokenCollection} />
</AccordionItemComponent>
<AccordionItemComponent title={"NOTEBOOKS"}>
<TreeComponent className="notebookResourceTree" rootNode={buildNotebooksTree()} />
</AccordionItemComponent>
</AccordionComponent>
{/* {buildGalleryCallout()} */}
</>
)}
</>

View File

@ -1,42 +1,21 @@
import { Callout, DirectionalHint, ICalloutProps, ILinkProps, Link, Stack, Text } from "@fluentui/react";
import { getItemName } from "Utils/APITypeUtils";
import * as ko from "knockout";
import * as React from "react";
import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg";
import GalleryIcon from "../../../images/GalleryIcon.svg";
import DeleteIcon from "../../../images/delete.svg";
import CopyIcon from "../../../images/notebook/Notebook-copy.svg";
import NewNotebookIcon from "../../../images/notebook/Notebook-new.svg";
import NotebookIcon from "../../../images/notebook/Notebook-resource.svg";
import FileIcon from "../../../images/notebook/file-cosmos.svg";
import PublishIcon from "../../../images/notebook/publish_content.svg";
import RefreshIcon from "../../../images/refresh-cosmos.svg";
import CollectionIcon from "../../../images/tree-collection.svg";
import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
import { Areas } from "../../Common/Constants";
import { isPublicInternetAccessAllowed } from "../../Common/DatabaseAccountUtility";
import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import { IPinnedRepo } from "../../Juno/JunoClient";
import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility";
import { Action, ActionModifiers, Source } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext";
import { isServerlessAccount } from "../../Utils/CapabilityUtils";
import * as GitHubUtils from "../../Utils/GitHubUtils";
import { useSidePanel } from "../../hooks/useSidePanel";
import { useTabs } from "../../hooks/useTabs";
import * as ResourceTreeContextMenuButtonFactory from "../ContextMenuButtonFactory";
import { AccordionComponent, AccordionItemComponent } from "../Controls/Accordion/AccordionComponent";
import { useDialog } from "../Controls/Dialog";
import { TreeComponent, TreeNode, TreeNodeMenuItem } from "../Controls/TreeComponent/TreeComponent";
import { TreeComponent, TreeNode } from "../Controls/TreeComponent/TreeComponent";
import Explorer from "../Explorer";
import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter";
import { mostRecentActivity } from "../MostRecentActivity/MostRecentActivity";
import { NotebookContentItem, NotebookContentItemType } from "../Notebook/NotebookContentItem";
import { NotebookUtil } from "../Notebook/NotebookUtil";
import { useNotebook } from "../Notebook/useNotebook";
import { GitHubReposPanel } from "../Panes/GitHubReposPanel/GitHubReposPanel";
import TabsBase from "../Tabs/TabsBase";
import { useDatabases } from "../useDatabases";
import { useSelectedNode } from "../useSelectedNode";
@ -46,19 +25,8 @@ import Trigger from "./Trigger";
import UserDefinedFunction from "./UserDefinedFunction";
export class ResourceTreeAdapter implements ReactAdapter {
public static readonly MyNotebooksTitle = "My Notebooks";
public static readonly GitHubReposTitle = "GitHub repos";
private static readonly DataTitle = "DATA";
private static readonly NotebooksTitle = "NOTEBOOKS";
private static readonly PseudoDirPath = "PsuedoDir";
public parameters: ko.Observable<number>;
public galleryContentRoot: NotebookContentItem;
public myNotebooksContentRoot: NotebookContentItem;
public gitHubNotebooksContentRoot: NotebookContentItem;
public constructor(private container: Explorer) {
this.parameters = ko.observable(Date.now());
@ -76,111 +44,9 @@ export class ResourceTreeAdapter implements ReactAdapter {
this.triggerRender();
}
private traceMyNotebookTreeInfo() {
const myNotebooksTree = this.myNotebooksContentRoot;
if (myNotebooksTree.children) {
// Count 1st generation children (tree is lazy-loaded)
const nodeCounts = { files: 0, notebooks: 0, directories: 0 };
myNotebooksTree.children.forEach((treeNode) => {
switch ((treeNode as NotebookContentItem).type) {
case NotebookContentItemType.File:
nodeCounts.files++;
break;
case NotebookContentItemType.Directory:
nodeCounts.directories++;
break;
case NotebookContentItemType.Notebook:
nodeCounts.notebooks++;
break;
default:
break;
}
});
TelemetryProcessor.trace(Action.RefreshResourceTreeMyNotebooks, ActionModifiers.Mark, { ...nodeCounts });
}
}
public renderComponent(): JSX.Element {
const dataRootNode = this.buildDataTree();
const notebooksRootNode = this.buildNotebooksTrees();
if (useNotebook.getState().isNotebookEnabled) {
return (
<>
<AccordionComponent>
<AccordionItemComponent title={ResourceTreeAdapter.DataTitle} isExpanded={!this.gitHubNotebooksContentRoot}>
<TreeComponent className="dataResourceTree" rootNode={dataRootNode} />
</AccordionItemComponent>
<AccordionItemComponent title={ResourceTreeAdapter.NotebooksTitle}>
<TreeComponent className="notebookResourceTree" rootNode={notebooksRootNode} />
</AccordionItemComponent>
</AccordionComponent>
{/* {this.galleryContentRoot && this.buildGalleryCallout()} */}
</>
);
} else {
return <TreeComponent className="dataResourceTree" rootNode={dataRootNode} />;
}
}
public async initialize(): Promise<void[]> {
const refreshTasks: Promise<void>[] = [];
this.galleryContentRoot = {
name: "Gallery",
path: "Gallery",
type: NotebookContentItemType.File,
};
this.myNotebooksContentRoot = {
name: useNotebook.getState().notebookFolderName,
path: useNotebook.getState().notebookBasePath,
type: NotebookContentItemType.Directory,
};
// Only if notebook server is available we can refresh
if (useNotebook.getState().notebookServerInfo?.notebookServerEndpoint) {
refreshTasks.push(
this.container.refreshContentItem(this.myNotebooksContentRoot).then(() => {
this.triggerRender();
this.traceMyNotebookTreeInfo();
}),
);
}
this.gitHubNotebooksContentRoot = {
name: ResourceTreeAdapter.GitHubReposTitle,
path: ResourceTreeAdapter.PseudoDirPath,
type: NotebookContentItemType.Directory,
};
return Promise.all(refreshTasks);
}
public initializeGitHubRepos(pinnedRepos: IPinnedRepo[]): void {
if (this.gitHubNotebooksContentRoot) {
this.gitHubNotebooksContentRoot.children = [];
pinnedRepos?.forEach((pinnedRepo) => {
const repoFullName = GitHubUtils.toRepoFullName(pinnedRepo.owner, pinnedRepo.name);
const repoTreeItem: NotebookContentItem = {
name: repoFullName,
path: ResourceTreeAdapter.PseudoDirPath,
type: NotebookContentItemType.Directory,
children: [],
};
pinnedRepo.branches.forEach((branch) => {
repoTreeItem.children.push({
name: branch.name,
path: GitHubUtils.toContentUri(pinnedRepo.owner, pinnedRepo.name, branch.name, ""),
type: NotebookContentItemType.Directory,
});
});
this.gitHubNotebooksContentRoot.children.push(repoTreeItem);
});
this.triggerRender();
}
return <TreeComponent className="dataResourceTree" rootNode={dataRootNode} />;
}
private buildDataTree(): TreeNode {
@ -504,365 +370,6 @@ export class ResourceTreeAdapter implements ReactAdapter {
return traverse(schema);
}
private buildNotebooksTrees(): TreeNode {
let notebooksTree: TreeNode = {
label: undefined,
isExpanded: true,
children: [],
};
if (this.galleryContentRoot) {
notebooksTree.children.push(this.buildGalleryNotebooksTree());
}
if (this.myNotebooksContentRoot) {
notebooksTree.children.push(this.buildMyNotebooksTree());
}
if (this.gitHubNotebooksContentRoot) {
// collapse all other notebook nodes
notebooksTree.children.forEach((node) => (node.isExpanded = false));
notebooksTree.children.push(this.buildGitHubNotebooksTree());
}
return notebooksTree;
}
private buildGalleryCallout(): JSX.Element {
if (
LocalStorageUtility.hasItem(StorageKey.GalleryCalloutDismissed) &&
LocalStorageUtility.getEntryBoolean(StorageKey.GalleryCalloutDismissed)
) {
return undefined;
}
const calloutProps: ICalloutProps = {
calloutMaxWidth: 350,
ariaLabel: "New gallery",
role: "alertdialog",
gapSpace: 0,
target: ".galleryHeader",
directionalHint: DirectionalHint.leftTopEdge,
onDismiss: () => {
LocalStorageUtility.setEntryBoolean(StorageKey.GalleryCalloutDismissed, true);
this.triggerRender();
},
setInitialFocus: true,
};
const openGalleryProps: ILinkProps = {
onClick: () => {
LocalStorageUtility.setEntryBoolean(StorageKey.GalleryCalloutDismissed, true);
this.container.openGallery();
this.triggerRender();
},
};
return (
<Callout {...calloutProps}>
<Stack tokens={{ childrenGap: 10, padding: 20 }}>
<Text variant="xLarge" block>
New gallery
</Text>
<Text block>
Sample notebooks are now combined in gallery. View and try out samples provided by Microsoft and other
contributors.
</Text>
<Link {...openGalleryProps}>Open gallery</Link>
</Stack>
</Callout>
);
}
private buildGalleryNotebooksTree(): TreeNode {
return {
label: "Gallery",
iconSrc: GalleryIcon,
className: "notebookHeader galleryHeader",
onClick: () => this.container.openGallery(),
isSelected: () => {
const activeTab = useTabs.getState().activeTab;
return activeTab && activeTab.tabKind === ViewModels.CollectionTabKind.Gallery;
},
};
}
private buildMyNotebooksTree(): TreeNode {
const myNotebooksTree: TreeNode = this.buildNotebookDirectoryNode(
this.myNotebooksContentRoot,
(item: NotebookContentItem) => {
this.container.openNotebook(item).then((hasOpened) => {
if (hasOpened) {
mostRecentActivity.notebookWasItemOpened(userContext.databaseAccount?.id, item);
}
});
},
true,
true,
);
myNotebooksTree.isExpanded = true;
myNotebooksTree.isAlphaSorted = true;
// Remove "Delete" menu item from context menu
myNotebooksTree.contextMenu = myNotebooksTree.contextMenu.filter((menuItem) => menuItem.label !== "Delete");
return myNotebooksTree;
}
private buildGitHubNotebooksTree(): TreeNode {
const gitHubNotebooksTree: TreeNode = this.buildNotebookDirectoryNode(
this.gitHubNotebooksContentRoot,
(item: NotebookContentItem) => {
this.container.openNotebook(item).then((hasOpened) => {
if (hasOpened) {
mostRecentActivity.notebookWasItemOpened(userContext.databaseAccount?.id, item);
}
});
},
true,
true,
);
gitHubNotebooksTree.contextMenu = [
{
label: "Manage GitHub settings",
onClick: () =>
useSidePanel
.getState()
.openSidePanel(
"Manage GitHub settings",
<GitHubReposPanel
explorer={this.container}
gitHubClientProp={this.container.notebookManager.gitHubClient}
junoClientProp={this.container.notebookManager.junoClient}
/>,
),
},
{
label: "Disconnect from GitHub",
onClick: () => {
TelemetryProcessor.trace(Action.NotebooksGitHubDisconnect, ActionModifiers.Mark, {
dataExplorerArea: Areas.Notebook,
});
this.container.notebookManager?.gitHubOAuthService.logout();
},
},
];
gitHubNotebooksTree.isExpanded = true;
gitHubNotebooksTree.isAlphaSorted = true;
return gitHubNotebooksTree;
}
private buildChildNodes(
item: NotebookContentItem,
onFileClick: (item: NotebookContentItem) => void,
createDirectoryContextMenu: boolean,
createFileContextMenu: boolean,
): TreeNode[] {
if (!item || !item.children) {
return [];
} else {
return item.children.map((item) => {
const result =
item.type === NotebookContentItemType.Directory
? this.buildNotebookDirectoryNode(item, onFileClick, createDirectoryContextMenu, createFileContextMenu)
: this.buildNotebookFileNode(item, onFileClick, createFileContextMenu);
result.timestamp = item.timestamp;
return result;
});
}
}
private buildNotebookFileNode(
item: NotebookContentItem,
onFileClick: (item: NotebookContentItem) => void,
createFileContextMenu: boolean,
): TreeNode {
return {
label: item.name,
iconSrc: NotebookUtil.isNotebookFile(item.path) ? NotebookIcon : FileIcon,
className: "notebookHeader",
onClick: () => onFileClick(item),
isSelected: () => {
const activeTab = useTabs.getState().activeTab;
return (
activeTab &&
activeTab.tabKind === ViewModels.CollectionTabKind.NotebookV2 &&
/* TODO Redesign Tab interface so that resource tree doesn't need to know about NotebookV2Tab.
NotebookV2Tab could be dynamically imported, but not worth it to just get this type right.
*/
(activeTab as any).notebookPath() === item.path
);
},
contextMenu: createFileContextMenu && this.createFileContextMenu(item),
data: item,
};
}
private createFileContextMenu(item: NotebookContentItem): TreeNodeMenuItem[] {
let items: TreeNodeMenuItem[] = [
{
label: "Rename",
iconSrc: NotebookIcon,
onClick: () => this.container.renameNotebook(item),
},
{
label: "Delete",
iconSrc: DeleteIcon,
onClick: () => {
useDialog
.getState()
.showOkCancelModalDialog(
"Confirm delete",
`Are you sure you want to delete "${item.name}"`,
"Delete",
() => this.container.deleteNotebookFile(item).then(() => this.triggerRender()),
"Cancel",
undefined,
);
},
},
{
label: "Copy to ...",
iconSrc: CopyIcon,
onClick: () => this.copyNotebook(item),
},
{
label: "Download",
iconSrc: NotebookIcon,
onClick: () => this.container.downloadFile(item),
},
];
if (item.type === NotebookContentItemType.Notebook) {
items.push({
label: "Publish to gallery",
iconSrc: PublishIcon,
onClick: async () => {
TelemetryProcessor.trace(Action.NotebooksGalleryClickPublishToGallery, ActionModifiers.Mark, {
source: Source.ResourceTreeMenu,
});
const content = await this.container.readFile(item);
if (content) {
await this.container.publishNotebook(item.name, content);
}
},
});
}
// "Copy to ..." isn't needed if github locations are not available
if (!this.container.notebookManager?.gitHubOAuthService.isLoggedIn()) {
items = items.filter((item) => item.label !== "Copy to ...");
}
return items;
}
private copyNotebook = async (item: NotebookContentItem) => {
const content = await this.container.readFile(item);
if (content) {
this.container.copyNotebook(item.name, content);
}
};
private createDirectoryContextMenu(item: NotebookContentItem): TreeNodeMenuItem[] {
let items: TreeNodeMenuItem[] = [
{
label: "Refresh",
iconSrc: RefreshIcon,
onClick: () => this.container.refreshContentItem(item).then(() => this.triggerRender()),
},
{
label: "Delete",
iconSrc: DeleteIcon,
onClick: () => {
useDialog
.getState()
.showOkCancelModalDialog(
"Confirm delete",
`Are you sure you want to delete "${item.name}?"`,
"Delete",
() => this.container.deleteNotebookFile(item).then(() => this.triggerRender()),
"Cancel",
undefined,
);
},
},
{
label: "Rename",
iconSrc: NotebookIcon,
onClick: () => this.container.renameNotebook(item),
},
{
label: "New Directory",
iconSrc: NewNotebookIcon,
onClick: () => this.container.onCreateDirectory(item),
},
{
label: "Upload File",
iconSrc: NewNotebookIcon,
onClick: () => this.container.openUploadFilePanel(item),
},
];
//disallow renaming of temporary notebook workspace
if (item?.path === useNotebook.getState().notebookBasePath) {
items = items.filter((item) => item.label !== "Rename");
}
// For GitHub paths remove "Delete", "Rename", "New Directory", "Upload File"
if (GitHubUtils.fromContentUri(item.path)) {
items = items.filter(
(item) =>
item.label !== "Delete" &&
item.label !== "Rename" &&
item.label !== "New Directory" &&
item.label !== "Upload File",
);
}
return items;
}
private buildNotebookDirectoryNode(
item: NotebookContentItem,
onFileClick: (item: NotebookContentItem) => void,
createDirectoryContextMenu: boolean,
createFileContextMenu: boolean,
): TreeNode {
return {
label: item.name,
iconSrc: undefined,
className: "notebookHeader",
isAlphaSorted: true,
isLeavesParentsSeparate: true,
onClick: () => {
if (!item.children) {
this.container.refreshContentItem(item).then(() => this.triggerRender());
}
},
isSelected: () => {
const activeTab = useTabs.getState().activeTab;
return (
activeTab &&
activeTab.tabKind === ViewModels.CollectionTabKind.NotebookV2 &&
/* TODO Redesign Tab interface so that resource tree doesn't need to know about NotebookV2Tab.
NotebookV2Tab could be dynamically imported, but not worth it to just get this type right.
*/
(activeTab as any).notebookPath() === item.path
);
},
contextMenu:
createDirectoryContextMenu && item.path !== ResourceTreeAdapter.PseudoDirPath
? this.createDirectoryContextMenu(item)
: undefined,
data: item,
children: this.buildChildNodes(item, onFileClick, createDirectoryContextMenu, createFileContextMenu),
};
}
public triggerRender() {
window.requestAnimationFrame(() => this.parameters(Date.now()));
}

View File

@ -17,8 +17,17 @@ export type KeyboardHandlerMap = Partial<Record<KeyboardAction, KeyboardActionHa
* Each group can be updated separately, but, when updated, must be completely replaced.
*/
export enum KeyboardActionGroup {
/** Keyboard actions related to tab navigation. */
TABS = "TABS",
/** Keyboard actions managed by the global command bar. */
COMMAND_BAR = "COMMAND_BAR",
/**
* Keyboard actions specific to the active tab.
* This group is automatically cleared when the active tab changes.
*/
ACTIVE_TAB = "ACTIVE_TAB",
}
/**
@ -29,6 +38,7 @@ export enum KeyboardAction {
EXECUTE_ITEM = "EXECUTE_ITEM",
CANCEL_OR_DISCARD = "CANCEL_OR_DISCARD",
SAVE_ITEM = "SAVE_ITEM",
DOWNLOAD_ITEM = "DOWNLOAD_ITEM",
OPEN_QUERY = "OPEN_QUERY",
OPEN_QUERY_FROM_DISK = "OPEN_QUERY_FROM_DISK",
NEW_SPROC = "NEW_SPROC",
@ -42,6 +52,8 @@ export enum KeyboardAction {
SELECT_LEFT_TAB = "SELECT_LEFT_TAB",
SELECT_RIGHT_TAB = "SELECT_RIGHT_TAB",
CLOSE_TAB = "CLOSE_TAB",
SEARCH = "SEARCH",
CLEAR_SEARCH = "CLEAR_SEARCH",
}
/**
@ -54,9 +66,10 @@ const bindings: Record<KeyboardAction, string[]> = {
// See https://www.npmjs.com/package/tinykeys#commonly-used-keys-and-codes for more information on the expected values for keyboard shortcuts.
[KeyboardAction.NEW_QUERY]: ["$mod+J", "Alt+N Q"],
[KeyboardAction.EXECUTE_ITEM]: ["Shift+Enter"],
[KeyboardAction.EXECUTE_ITEM]: ["Shift+Enter", "F5"],
[KeyboardAction.CANCEL_OR_DISCARD]: ["Escape"],
[KeyboardAction.SAVE_ITEM]: ["$mod+S"],
[KeyboardAction.DOWNLOAD_ITEM]: ["$mod+Shift+S"],
[KeyboardAction.OPEN_QUERY]: ["$mod+O"],
[KeyboardAction.OPEN_QUERY_FROM_DISK]: ["$mod+Shift+O"],
[KeyboardAction.NEW_SPROC]: ["Alt+N P"],
@ -67,9 +80,11 @@ const bindings: Record<KeyboardAction, string[]> = {
[KeyboardAction.NEW_ITEM]: ["Alt+N I"],
[KeyboardAction.DELETE_ITEM]: ["Alt+D"],
[KeyboardAction.TOGGLE_COPILOT]: ["$mod+P"],
[KeyboardAction.SELECT_LEFT_TAB]: ["$mod+Alt+["],
[KeyboardAction.SELECT_RIGHT_TAB]: ["$mod+Alt+]"],
[KeyboardAction.SELECT_LEFT_TAB]: ["$mod+Alt+[", "$mod+Shift+F6"],
[KeyboardAction.SELECT_RIGHT_TAB]: ["$mod+Alt+]", "$mod+F6"],
[KeyboardAction.CLOSE_TAB]: ["$mod+Alt+W"],
[KeyboardAction.SEARCH]: ["$mod+Shift+F"],
[KeyboardAction.CLEAR_SEARCH]: ["$mod+Shift+C"],
};
interface KeyboardShortcutState {
@ -89,13 +104,24 @@ interface KeyboardShortcutState {
setHandlers: (group: KeyboardActionGroup, handlers: KeyboardHandlerMap) => void;
}
export type KeyboardHandlerSetter = (handlers: KeyboardHandlerMap) => void;
/**
* Defines the calling component as the manager of the keyboard actions for the given group.
* @param group The group of keyboard actions to manage.
* @returns A function that can be used to set the keyboard action handlers for the given group.
*/
export const useKeyboardActionGroup = (group: KeyboardActionGroup) => (handlers: KeyboardHandlerMap) =>
useKeyboardActionHandlers.getState().setHandlers(group, handlers);
export const useKeyboardActionGroup: (group: KeyboardActionGroup) => KeyboardHandlerSetter =
(group: KeyboardActionGroup) => (handlers: KeyboardHandlerMap) =>
useKeyboardActionHandlers.getState().setHandlers(group, handlers);
/**
* Clears the keyboard action handlers for the given group.
* @param group The group of keyboard actions to clear.
*/
export const clearKeyboardActionGroup = (group: KeyboardActionGroup) => {
useKeyboardActionHandlers.getState().setHandlers(group, {});
};
const useKeyboardActionHandlers: UseStore<KeyboardShortcutState> = create((set, get) => ({
allHandlers: {},

View File

@ -38,7 +38,6 @@ export type Features = {
readonly copilotChatFixedMonacoEditorHeight: boolean;
readonly enablePriorityBasedExecution: boolean;
readonly disableConnectionStringLogin: boolean;
readonly enableLegacyMongoShell: boolean;
// can be set via both flight and feature flag
autoscaleDefault: boolean;
@ -109,7 +108,6 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
copilotChatFixedMonacoEditorHeight: "true" === get("copilotchatfixedmonacoeditorheight"),
enablePriorityBasedExecution: "true" === get("enableprioritybasedexecution"),
disableConnectionStringLogin: "true" === get("disableconnectionstringlogin"),
enableLegacyMongoShell: "true" === get("enablelegacymongoshell"),
};
}

View File

@ -154,8 +154,16 @@ export const allowedNotebookServerUrls: ReadonlyArray<string> = [];
export function useNewPortalBackendEndpoint(backendApi: string): boolean {
// This maps backend APIs to the environments supported by the new backend.
const newBackendApiEnvironmentMap: { [key: string]: string[] } = {
[BackendApi.GenerateToken]: [PortalBackendEndpoints.Development],
[BackendApi.PortalSettings]: [PortalBackendEndpoints.Development, PortalBackendEndpoints.Mpac],
[BackendApi.GenerateToken]: [
PortalBackendEndpoints.Development,
PortalBackendEndpoints.Mpac,
PortalBackendEndpoints.Prod,
],
[BackendApi.PortalSettings]: [
PortalBackendEndpoints.Development,
PortalBackendEndpoints.Mpac,
PortalBackendEndpoints.Prod,
],
};
if (!newBackendApiEnvironmentMap[backendApi] || !configContext.PORTAL_BACKEND_ENDPOINT) {

View File

@ -245,7 +245,6 @@ export function downloadItem(
},
"Cancel",
undefined,
container.getDownloadModalConent(name),
);
}
export async function downloadNotebookItem(
@ -278,7 +277,6 @@ export async function downloadNotebookItem(
metadata.untrusted = true;
}
await container.importAndOpenContent(data.name, JSON.stringify(notebook));
logConsoleInfo(`Successfully downloaded ${data.name} to ${useNotebook.getState().notebookFolderName}`);
const increaseDownloadResponse = await junoClient.increaseNotebookDownloadCount(data.id);