From 70f5889e0efe4d0f2709e268da0cf203ea7a39ec Mon Sep 17 00:00:00 2001 From: Sarav Date: Mon, 22 Apr 2024 16:41:19 +0530 Subject: [PATCH 01/24] feat: query panel migration --- webview_panels/src/AppConstants.tsx | 2 + .../modules/queryPanel/QueryPanel.stories.tsx | 21 +++++++++++ .../src/modules/queryPanel/QueryPanel.tsx | 21 +++++++++++ .../clearResultsButton/ClearResultsButton.tsx | 7 ++++ .../queryPanel/components/help/HelpButton.tsx | 37 +++++++++++++++++++ 5 files changed, 88 insertions(+) create mode 100644 webview_panels/src/modules/queryPanel/QueryPanel.stories.tsx create mode 100644 webview_panels/src/modules/queryPanel/QueryPanel.tsx create mode 100644 webview_panels/src/modules/queryPanel/components/clearResultsButton/ClearResultsButton.tsx create mode 100644 webview_panels/src/modules/queryPanel/components/help/HelpButton.tsx diff --git a/webview_panels/src/AppConstants.tsx b/webview_panels/src/AppConstants.tsx index 2c4db1211..b79a1f99f 100644 --- a/webview_panels/src/AppConstants.tsx +++ b/webview_panels/src/AppConstants.tsx @@ -4,6 +4,7 @@ import Home from "./modules/home/Home"; import DocumentationProvider from "@modules/documentationEditor/DocumentationProvider"; import DataPilotPanel from "@modules/dataPilot"; import DbtDocsView from "@modules/dbtDocs/DbtDocsView"; +import QueryPanel from "@modules/queryPanel/QueryPanel"; // TODO: lazy loading breaks loading dynamic webviews when having css because of vite dynamic loading // research on how to fix that and then use lazy loading @@ -23,4 +24,5 @@ export const AvailableRoutes = { component: , }, "/dbt-docs": { component: }, + "/query-panel": { component: }, }; diff --git a/webview_panels/src/modules/queryPanel/QueryPanel.stories.tsx b/webview_panels/src/modules/queryPanel/QueryPanel.stories.tsx new file mode 100644 index 000000000..6f56c6c51 --- /dev/null +++ b/webview_panels/src/modules/queryPanel/QueryPanel.stories.tsx @@ -0,0 +1,21 @@ +import type { Meta } from "@storybook/react"; +import QueryPanel from "./QueryPanel"; + +const meta = { + title: "Query Panel", + parameters: { + layout: "padded", + }, + tags: ["autodocs"], + argTypes: { + backgroundColor: { control: "color" }, + }, +} satisfies Meta; + +export default meta; + +export const DefaultQueryPanelView = { + render: (): JSX.Element => { + return ; + }, +}; diff --git a/webview_panels/src/modules/queryPanel/QueryPanel.tsx b/webview_panels/src/modules/queryPanel/QueryPanel.tsx new file mode 100644 index 000000000..05a936f76 --- /dev/null +++ b/webview_panels/src/modules/queryPanel/QueryPanel.tsx @@ -0,0 +1,21 @@ +import FeedbackButton from "@modules/commonActionButtons/FeedbackButton"; +import { Stack } from "@uicore"; +import HelpButton from "./components/help/HelpButton"; +import ClearResultsButton from "./components/clearResultsButton/ClearResultsButton"; + +const QueryPanel = (): JSX.Element => { + return ( +
+ + Query panel + + + + + + +
+ ); +}; + +export default QueryPanel; diff --git a/webview_panels/src/modules/queryPanel/components/clearResultsButton/ClearResultsButton.tsx b/webview_panels/src/modules/queryPanel/components/clearResultsButton/ClearResultsButton.tsx new file mode 100644 index 000000000..a2a644db3 --- /dev/null +++ b/webview_panels/src/modules/queryPanel/components/clearResultsButton/ClearResultsButton.tsx @@ -0,0 +1,7 @@ +import { Button } from "@uicore"; + +const ClearResultsButton = (): JSX.Element => { + return ; +}; + +export default ClearResultsButton; diff --git a/webview_panels/src/modules/queryPanel/components/help/HelpButton.tsx b/webview_panels/src/modules/queryPanel/components/help/HelpButton.tsx new file mode 100644 index 000000000..9de0527a6 --- /dev/null +++ b/webview_panels/src/modules/queryPanel/components/help/HelpButton.tsx @@ -0,0 +1,37 @@ +import { HelpIcon } from "@assets/icons"; +import { Drawer } from "@uicore"; + +const HelpButton = (): JSX.Element => { + return ( + + Help + + } + title="Help" + > +
+

Previewing Query

+

+ Press Cmd+Enter (Mac) or Control+Enter (Windows/Linux) to run a query. + Highlight part of a query to preview only selection +

+

Default Query Limit

+

+ Query preview is limited to 500 rows by default, this can be + configured in Settings -> dbt Power User or via changing the input + to the left which is synchronized with the VS Code setting +

+

Dispatched SQL

+

+ This tab displays the compiled query sent to the database. You can + copy to run directly in your database. +

+
+
+ ); +}; + +export default HelpButton; From 89e4f064d8f7d413325ec7664ccad73b4b544481 Mon Sep 17 00:00:00 2001 From: Sarav Date: Tue, 23 Apr 2024 12:11:09 +0530 Subject: [PATCH 02/24] feat: add query panel default view --- webview_panels/package-lock.json | 866 +++++++++++++++++- webview_panels/package.json | 4 + .../src/modules/queryPanel/QueryPanel.tsx | 6 +- .../queryPanel/QueryPanelDefaultView.tsx | 18 + .../queryPanel/components/help/HelpButton.tsx | 20 +- .../components/help/HelpContent.tsx | 24 + .../perspective/PerspectiveViewer.stories.tsx | 30 + .../perspective/PerspectiveViewer.tsx | 38 + .../components/queryLimit/QueryLimit.tsx | 23 + .../components/tableScale/TableScale.tsx | 28 + 10 files changed, 1034 insertions(+), 23 deletions(-) create mode 100644 webview_panels/src/modules/queryPanel/QueryPanelDefaultView.tsx create mode 100644 webview_panels/src/modules/queryPanel/components/help/HelpContent.tsx create mode 100644 webview_panels/src/modules/queryPanel/components/perspective/PerspectiveViewer.stories.tsx create mode 100644 webview_panels/src/modules/queryPanel/components/perspective/PerspectiveViewer.tsx create mode 100644 webview_panels/src/modules/queryPanel/components/queryLimit/QueryLimit.tsx create mode 100644 webview_panels/src/modules/queryPanel/components/tableScale/TableScale.tsx diff --git a/webview_panels/package-lock.json b/webview_panels/package-lock.json index d933bc5a7..c2921b164 100644 --- a/webview_panels/package-lock.json +++ b/webview_panels/package-lock.json @@ -8,6 +8,10 @@ "name": "webview_panels", "version": "0.0.0", "dependencies": { + "@finos/perspective": "^2.10.0", + "@finos/perspective-viewer": "^2.10.0", + "@finos/perspective-viewer-d3fc": "^2.10.0", + "@finos/perspective-viewer-datagrid": "^2.10.0", "@hookform/resolvers": "^3.3.4", "@reduxjs/toolkit": "^2.0.1", "@vscode/codicons": "^0.0.35", @@ -2165,6 +2169,228 @@ "node": ">=0.1.90" } }, + "node_modules/@d3fc/d3fc-annotation": { + "version": "3.0.12", + "resolved": "https://registry.npmjs.org/@d3fc/d3fc-annotation/-/d3fc-annotation-3.0.12.tgz", + "integrity": "sha512-wwBDsER+/S9Z+aBZEQaSYk+Vcc0y+v5+H8Vvubam3gzMuBo0Nv9Ge7lvQhv6tDV8jTUsInYJUcNipGph/3z7pw==", + "dependencies": { + "@d3fc/d3fc-data-join": "^6.0.3", + "@d3fc/d3fc-rebind": "^6.0.1", + "@d3fc/d3fc-series": "^6.1.0", + "@d3fc/d3fc-shape": "^6.0.1" + }, + "peerDependencies": { + "d3-scale": "*", + "d3-selection": "*" + } + }, + "node_modules/@d3fc/d3fc-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@d3fc/d3fc-axis/-/d3fc-axis-3.0.6.tgz", + "integrity": "sha512-6D5HpTiSmht8x9TU7war5iVNW5QFlvzNDk92yJ6hUOKeD/otr2D3wrpvSgJrS7GehceC7Ik86etpoiCjR4wyrg==", + "dependencies": { + "@d3fc/d3fc-data-join": "^6.0.3", + "@d3fc/d3fc-rebind": "^6.0.1" + }, + "peerDependencies": { + "d3-scale": "*", + "d3-selection": "*", + "d3-shape": "*" + } + }, + "node_modules/@d3fc/d3fc-brush": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@d3fc/d3fc-brush/-/d3fc-brush-3.0.3.tgz", + "integrity": "sha512-fc1XBuNWl6DQVFSnBdauI/WNRvtNaeUWwDiLEIMY+v0VlrmGOgWEGCB5otVepGZiL56cjgRtHdwYBdKCvgGBSg==", + "dependencies": { + "@d3fc/d3fc-data-join": "^6.0.3", + "@d3fc/d3fc-rebind": "^6.0.1" + }, + "peerDependencies": { + "d3-brush": "*", + "d3-dispatch": "*", + "d3-scale": "*", + "d3-selection": "*" + } + }, + "node_modules/@d3fc/d3fc-chart": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/@d3fc/d3fc-chart/-/d3fc-chart-5.1.5.tgz", + "integrity": "sha512-aP/kzCRy87hFdOhPiOwDLRuzV8AmCr+KloWsrlM3pWYk5uxnoFuZLfYLtrJx4ooHxNHkF9hf/q7+prrFdJioxA==", + "dependencies": { + "@d3fc/d3fc-axis": "^3.0.6", + "@d3fc/d3fc-data-join": "^6.0.3", + "@d3fc/d3fc-element": "^6.2.0", + "@d3fc/d3fc-rebind": "^6.0.1", + "@d3fc/d3fc-series": "^6.1.0" + }, + "peerDependencies": { + "d3-scale": "*", + "d3-selection": "*" + } + }, + "node_modules/@d3fc/d3fc-data-join": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@d3fc/d3fc-data-join/-/d3fc-data-join-6.0.3.tgz", + "integrity": "sha512-fd1D2Cl4YGjzl3gBhcrvTl/VxaSncY0ZcokWsN8ahtmk9DZK4DnAgHGrdecnXVLkOx+ANDcqxqscYz6MWXLbcA==", + "peerDependencies": { + "d3-selection": "*" + } + }, + "node_modules/@d3fc/d3fc-discontinuous-scale": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@d3fc/d3fc-discontinuous-scale/-/d3fc-discontinuous-scale-4.1.0.tgz", + "integrity": "sha512-/8Qc9G9XuovuLg+zulrblU9TuKqDKvWneHtjxHy1oK1S3JBSpsjtjZ8I0mKazAjPiH8EqyLADli3LN9xG2P7mg==", + "dependencies": { + "@d3fc/d3fc-rebind": "^6.0.1" + }, + "peerDependencies": { + "d3-scale": "*", + "d3-time": "*" + } + }, + "node_modules/@d3fc/d3fc-element": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/@d3fc/d3fc-element/-/d3fc-element-6.2.0.tgz", + "integrity": "sha512-AvdZ3V4mVxF9dGYLiDCoqr3GhrFOUQEc1FcP20QEhQ3fJ3qYRwx7/uhL7G/L2xbe6k4delPgnLOvtoaDenhpZw==" + }, + "node_modules/@d3fc/d3fc-extent": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@d3fc/d3fc-extent/-/d3fc-extent-4.0.2.tgz", + "integrity": "sha512-m7w7Dof6KAIDtgzIsTcprWTEoiqExJGsGoQbb97bF+EwIkuEZWRUl1jkoeNL00efpX1o6zSdqSr6lojoR0aI/g==", + "peerDependencies": { + "d3-array": "*" + } + }, + "node_modules/@d3fc/d3fc-financial-feed": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@d3fc/d3fc-financial-feed/-/d3fc-financial-feed-7.1.0.tgz", + "integrity": "sha512-K8jktdRJQAiJepglErsuY2ZMKsm0YFWTeuhYnTFb8rWmyhwoPeem9QW+e6xBTiAvbElJm4yTrkal09KmO2cLlQ==", + "peerDependencies": { + "d3-fetch": "*" + } + }, + "node_modules/@d3fc/d3fc-group": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@d3fc/d3fc-group/-/d3fc-group-3.0.1.tgz", + "integrity": "sha512-GBUR6a4hkqfSo77iaFS4qPMS5tupH8hmJ8eniiD45GFmWQs69+Dlf8Uhx+GCCvQkE+px6rQyZWVCJKBq6gkz2Q==" + }, + "node_modules/@d3fc/d3fc-label-layout": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@d3fc/d3fc-label-layout/-/d3fc-label-layout-7.0.3.tgz", + "integrity": "sha512-EqeUXO5o0yLYfRPXT9zChx4HLZjsd7DTzRhfiIIJAjqEQeFW9ALYrt85ggqxKWaM5BGjtaOFcHFBrOkd9b2AMQ==", + "dependencies": { + "@d3fc/d3fc-data-join": "^6.0.3", + "@d3fc/d3fc-rebind": "^6.0.1" + }, + "peerDependencies": { + "d3-array": "*", + "d3-scale": "*", + "d3-selection": "*" + } + }, + "node_modules/@d3fc/d3fc-pointer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@d3fc/d3fc-pointer/-/d3fc-pointer-3.0.2.tgz", + "integrity": "sha512-GAODK7t+fwH2y/tQaQD0kUZMbrpY+Mu1duFixtthm7yvk1sKWQR9bic3Eh3YBp39lir0VzmHWQhjYNAFnRJERg==", + "dependencies": { + "@d3fc/d3fc-rebind": "^6.0.1" + }, + "peerDependencies": { + "d3-dispatch": "*", + "d3-selection": "*" + } + }, + "node_modules/@d3fc/d3fc-random-data": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@d3fc/d3fc-random-data/-/d3fc-random-data-4.0.1.tgz", + "integrity": "sha512-tKSoImB8V+EKnuwh4K+RBfYWmczgKF+rvH9b3SeGEocw3DlK1SvAr2ZVhYkyFVaqJU5YciWqQfaW5MjVT/33FA==", + "dependencies": { + "@d3fc/d3fc-rebind": "^6.0.1" + }, + "peerDependencies": { + "d3-random": "*", + "d3-time": "*" + } + }, + "node_modules/@d3fc/d3fc-rebind": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@d3fc/d3fc-rebind/-/d3fc-rebind-6.0.1.tgz", + "integrity": "sha512-+ryBZ53ALMffbADwnFAtTYQJcT7PE5BwpducGYS0X6Jux6ESnp+fP+cDQvBGbDBOVqaziGnfeLeJXjtMnZujmQ==" + }, + "node_modules/@d3fc/d3fc-sample": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@d3fc/d3fc-sample/-/d3fc-sample-5.0.1.tgz", + "integrity": "sha512-VJ4stSJDxcAhYhifiGE63MBVDPstx+yB3+JHuJt/qc8MjHSOC4NjrPpABNUS6qwfmOcQKbaRPN8urzJ+f5FCmg==", + "dependencies": { + "@d3fc/d3fc-rebind": "^6.0.1" + }, + "peerDependencies": { + "d3-array": "*" + } + }, + "node_modules/@d3fc/d3fc-series": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@d3fc/d3fc-series/-/d3fc-series-6.1.0.tgz", + "integrity": "sha512-8TMknE0IWRYowyuKZv361HCMZ9WXJrQoq1Md73fN0/oTgmI4nPnDtcA0f0qRQmEZPPr4iz9JPPzQG9kd9DW6Nw==", + "dependencies": { + "@d3fc/d3fc-data-join": "^6.0.3", + "@d3fc/d3fc-rebind": "^6.0.1", + "@d3fc/d3fc-shape": "^6.0.1", + "@d3fc/d3fc-webgl": "^3.2.0" + }, + "peerDependencies": { + "d3-array": "*", + "d3-scale": "*", + "d3-scale-chromatic": "*", + "d3-selection": "*", + "d3-shape": "*" + } + }, + "node_modules/@d3fc/d3fc-shape": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@d3fc/d3fc-shape/-/d3fc-shape-6.0.1.tgz", + "integrity": "sha512-/dD3S8BWrOjO2mSptUmwe38V7KG4Kw6liIE5NXZJjX/XidfZhuDu7WWuya3i90HeNYDZNcs6Z+4qM3FnvlZf8g==", + "peerDependencies": { + "d3-path": "*" + } + }, + "node_modules/@d3fc/d3fc-technical-indicator": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@d3fc/d3fc-technical-indicator/-/d3fc-technical-indicator-8.1.0.tgz", + "integrity": "sha512-ma5l4nLC0kReDjxeMuodwrQWzDzJHoBoYIJKfzAJhfcMhS/XqJBKXYfT5BO7ggjzAsW/oD9xh64tmozXx/pHoQ==", + "dependencies": { + "@d3fc/d3fc-rebind": "^6.0.1" + }, + "peerDependencies": { + "d3-array": "*" + } + }, + "node_modules/@d3fc/d3fc-webgl": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@d3fc/d3fc-webgl/-/d3fc-webgl-3.2.0.tgz", + "integrity": "sha512-ca0gSeitQYLIT5p3DoWpHS/cdkA/XmVWSHo1WiDMZt6Hwy86HSHwT3EGtTfXKtK1pZELyDvj9syhyRhi0zU5ow==", + "dependencies": { + "@d3fc/d3fc-rebind": "^6.0.1" + }, + "peerDependencies": { + "d3-scale": "*", + "d3-shape": "*" + } + }, + "node_modules/@d3fc/d3fc-zoom": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@d3fc/d3fc-zoom/-/d3fc-zoom-1.1.1.tgz", + "integrity": "sha512-JLYcrdq9W0ZA0vwXcwOhsEkjzfpS+/3Ec3gt1sTJ6KZmILyL+3HCem9y5MeGrns1rBi+9IyMMY2SwOOSJpMGcg==", + "dependencies": { + "@d3fc/d3fc-rebind": "^6.0.1" + }, + "peerDependencies": { + "d3-dispatch": "*", + "d3-selection": "*", + "d3-zoom": "*" + } + }, "node_modules/@discoveryjs/json-ext": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", @@ -2810,6 +3036,61 @@ "integrity": "sha512-cEee/Z+I12mZcFJshKcCqC8tuX5hG3s+d+9nZ3LabqKF1vKdF41B92pJVCBggjAGORAeOzyyDDKrZwIkLffeOQ==", "dev": true }, + "node_modules/@finos/perspective": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@finos/perspective/-/perspective-2.10.0.tgz", + "integrity": "sha512-8g6hsKhpS2gR7GYQWksNDm8kpymHZiSbJkC4HAp1dI95RArksr4U9CUAOnALgw8JI9B5CYI6b1SWQxbzCsTQ6A==", + "dependencies": { + "stoppable": "1.1.0", + "ws": "^6.1.2" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@finos/perspective-viewer": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@finos/perspective-viewer/-/perspective-viewer-2.10.0.tgz", + "integrity": "sha512-AWPduQl3/6kHWyDmBSZU7+IKvYnGz5GdMTc0gtW8jLB1p3tZcoiznyaK7BtfPEMDhOE0BNZP0hXuzN0bCtKXrg==", + "dependencies": { + "@finos/perspective": "^2.10.0" + } + }, + "node_modules/@finos/perspective-viewer-d3fc": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@finos/perspective-viewer-d3fc/-/perspective-viewer-d3fc-2.10.0.tgz", + "integrity": "sha512-zgShNcumLd0zVbLyzeEwDtD2XGjsECjkksdIo+B8I8z+uNBZU01oX7UdNQ+RYAhT9d6HQJ+tIGRPIgIc41+MDg==", + "dependencies": { + "@finos/perspective": "^2.10.0", + "@finos/perspective-viewer": "^2.10.0", + "chroma-js": "^1.3.4", + "d3": "^7.8.0", + "d3-array": "^3.2.1", + "d3-selection": "^3.0.0", + "d3-svg-legend": "^2.25.6", + "d3fc": "^15.2.4", + "gradient-parser": "1.0.2" + } + }, + "node_modules/@finos/perspective-viewer-datagrid": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@finos/perspective-viewer-datagrid/-/perspective-viewer-datagrid-2.10.0.tgz", + "integrity": "sha512-V/fea+/N+5Pqv9W5D7dHxhnSd3Mqu4Fs3AHOjqDmJAG4PKZWYfdnODYmQWYFnENEzAaR9vkvaywxWwyOkoTlYQ==", + "dependencies": { + "@finos/perspective": "^2.10.0", + "@finos/perspective-viewer": "^2.10.0", + "chroma-js": "^1.3.4", + "regular-table": "=0.6.4" + } + }, + "node_modules/@finos/perspective/node_modules/ws": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.2.tgz", + "integrity": "sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw==", + "dependencies": { + "async-limiter": "~1.0.0" + } + }, "node_modules/@floating-ui/core": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.0.tgz", @@ -6045,6 +6326,11 @@ "@types/node": "*" } }, + "node_modules/@types/d3-selection": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-1.0.10.tgz", + "integrity": "sha512-mHICSFHpIwgTycsvgINYCwItk039eofbGRzVNdeUUtv0S2BD1vXFFUKaeMJN3ARbVl+hlsVOIwdzhzub5tjr6Q==" + }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -7575,8 +7861,7 @@ "node_modules/async-limiter": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", - "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", - "dev": true + "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==" }, "node_modules/asynciterator.prototype": { "version": "1.0.0", @@ -8152,6 +8437,11 @@ "node": ">=10" } }, + "node_modules/chroma-js": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-1.4.1.tgz", + "integrity": "sha512-jTwQiT859RTFN/vIf7s+Vl/Z2LcMrvMv3WUFmd/4u76AdlFC0NTNgqEEFPcRiHmAswPsMiQEDZLM8vX8qXpZNQ==" + }, "node_modules/chrome-trace-event": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", @@ -8628,6 +8918,524 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, + "node_modules/d3": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "dependencies": { + "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": { + "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-axis": { + "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": "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-chord": { + "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 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-collection": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/d3-collection/-/d3-collection-1.0.7.tgz", + "integrity": "sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A==" + }, + "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-contour": { + "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": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "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-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-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "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": { + "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-fetch": { + "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 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "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 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "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-geo": { + "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 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "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": "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-path": { + "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-polygon": { + "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": "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": "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-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-scale-chromatic": { + "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 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "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-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-svg-legend": { + "version": "2.25.6", + "resolved": "https://registry.npmjs.org/d3-svg-legend/-/d3-svg-legend-2.25.6.tgz", + "integrity": "sha512-6dueSjQr3+g9SlQ1SOzc4V58cCjjBeyo4WEcY8PW80i9XD/s562W/4xk05bpky0vzQx+i2XmXj3CYT+9KIRlnw==", + "dependencies": { + "@types/d3-selection": "1.0.10", + "d3-array": "1.0.1", + "d3-dispatch": "1.0.1", + "d3-format": "1.0.2", + "d3-scale": "1.0.3", + "d3-selection": "1.0.2", + "d3-transition": "1.0.3" + } + }, + "node_modules/d3-svg-legend/node_modules/d3-array": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-1.0.1.tgz", + "integrity": "sha512-VPS5OH5Xb43tkFkxHEc4r5yWhlDwST47zh1q+qvgTj7xB9xDXn+UEcofhvNC7s8gD55y9Q/MCSPSBUVvnzo3Dw==" + }, + "node_modules/d3-svg-legend/node_modules/d3-color": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-1.4.1.tgz", + "integrity": "sha512-p2sTHSLCJI2QKunbGb7ocOh7DgTAn8IrLx21QRc/BSnodXM4sv6aLQlnfpvehFMLZEfBc6g9pH9SWQccFYfJ9Q==" + }, + "node_modules/d3-svg-legend/node_modules/d3-dispatch": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-1.0.1.tgz", + "integrity": "sha512-BRTp95mobTSKx8EtpOLbxXuYVtNNr0PmelkH9Uzg5cgcO5O1M0i3+2C0FeM2I95BwQoIlsuZXQTPIoIt5xOtmw==" + }, + "node_modules/d3-svg-legend/node_modules/d3-ease": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-1.0.7.tgz", + "integrity": "sha512-lx14ZPYkhNx0s/2HX5sLFUI3mbasHjSSpwO/KaaNACweVwxUruKyWVcb293wMv1RqTPZyZ8kSZ2NogUZNcLOFQ==" + }, + "node_modules/d3-svg-legend/node_modules/d3-format": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.0.2.tgz", + "integrity": "sha512-VHFdLLjGkeGrRL8T/rlIIDhI3vvVX/oOTM/GaDJfB1sIb4dU5ZgiEjg3EeidJdQ/70u60tM015TSWa1gqqLRhg==" + }, + "node_modules/d3-svg-legend/node_modules/d3-interpolate": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.4.0.tgz", + "integrity": "sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA==", + "dependencies": { + "d3-color": "1" + } + }, + "node_modules/d3-svg-legend/node_modules/d3-scale": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-1.0.3.tgz", + "integrity": "sha512-ah2Xqywu96gau2iET3T0ZTsu0/X0gfoB8vDTuZ1OaG5F0SgGJLXreBVBknSZf2HKnxjenRvFok3qY2FgY4RpFg==", + "dependencies": { + "d3-array": "1", + "d3-collection": "1", + "d3-color": "1", + "d3-format": "1", + "d3-interpolate": "1", + "d3-time": "1", + "d3-time-format": "2" + } + }, + "node_modules/d3-svg-legend/node_modules/d3-selection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-1.0.2.tgz", + "integrity": "sha512-nInNdsdhljkDqkU/83bdWwtiJ7xsX3l57YZMlqsAOMeQROeCv7osPqQgYnao0NmRZEGc11hNakY+EOkaIdsWpQ==" + }, + "node_modules/d3-svg-legend/node_modules/d3-time": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-1.1.0.tgz", + "integrity": "sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA==" + }, + "node_modules/d3-svg-legend/node_modules/d3-time-format": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.3.0.tgz", + "integrity": "sha512-guv6b2H37s2Uq/GefleCDtbe0XZAuy7Wa49VGkPVPMfLL9qObgBST3lEHJBMUp8S7NdLQAGIvr2KXk8Hc98iKQ==", + "dependencies": { + "d3-time": "1" + } + }, + "node_modules/d3-svg-legend/node_modules/d3-timer": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-1.0.10.tgz", + "integrity": "sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw==" + }, + "node_modules/d3-svg-legend/node_modules/d3-transition": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-1.0.3.tgz", + "integrity": "sha512-Facxcbma0nA2GVrx7B/Mgnn5ju6SwUMzGa9YcYmQjpqmaIq1Zbp5vVJLjtH6b08Lu0vcX7O6a4z+AlLmdCxrCQ==", + "dependencies": { + "d3-color": "1", + "d3-dispatch": "1", + "d3-ease": "1", + "d3-interpolate": "1", + "d3-selection": "1", + "d3-timer": "1" + } + }, + "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-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-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-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/d3-zoom": { + "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 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3fc": { + "version": "15.2.6", + "resolved": "https://registry.npmjs.org/d3fc/-/d3fc-15.2.6.tgz", + "integrity": "sha512-Au6wq4zTChMy8SyACbbPFZUFJ14ga6iCctQlOIIsA2wFo6Bf/85NTss53Y+sYjUsBTa05Cc2DwCHzCkEs0ctig==", + "dependencies": { + "@d3fc/d3fc-annotation": "^3.0.12", + "@d3fc/d3fc-axis": "^3.0.6", + "@d3fc/d3fc-brush": "^3.0.3", + "@d3fc/d3fc-chart": "^5.1.5", + "@d3fc/d3fc-data-join": "^6.0.3", + "@d3fc/d3fc-discontinuous-scale": "^4.1.0", + "@d3fc/d3fc-element": "^6.2.0", + "@d3fc/d3fc-extent": "^4.0.2", + "@d3fc/d3fc-financial-feed": "^7.1.0", + "@d3fc/d3fc-group": "^3.0.1", + "@d3fc/d3fc-label-layout": "^7.0.3", + "@d3fc/d3fc-pointer": "^3.0.2", + "@d3fc/d3fc-random-data": "^4.0.1", + "@d3fc/d3fc-rebind": "^6.0.1", + "@d3fc/d3fc-sample": "^5.0.1", + "@d3fc/d3fc-series": "^6.1.0", + "@d3fc/d3fc-shape": "^6.0.1", + "@d3fc/d3fc-technical-indicator": "^8.1.0", + "@d3fc/d3fc-webgl": "^3.2.0", + "@d3fc/d3fc-zoom": "^1.1.1" + } + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -8815,6 +9623,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/delaunator": { + "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", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -11388,6 +12204,14 @@ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "dev": true }, + "node_modules/gradient-parser": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/gradient-parser/-/gradient-parser-1.0.2.tgz", + "integrity": "sha512-gR6nY33xC9yJoH4wGLQtZQMXDi6RI3H37ERu7kQCVUzlXjNedpZM7xcA489Opwbq0BSGohtWGsWsntupmxelMg==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", @@ -11851,6 +12675,14 @@ "node": ">= 0.4" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "engines": { + "node": ">=12" + } + }, "node_modules/invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", @@ -16403,6 +17235,14 @@ "jsesc": "bin/jsesc" } }, + "node_modules/regular-table": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/regular-table/-/regular-table-0.6.4.tgz", + "integrity": "sha512-N53k0gMxK9X4pwc8/+UOqiigoLwANsUBTlfoPruLoU3248kDswgt2H1LpuK6RqKkTU9V9Gr1XOOOxK0rXAWsSQ==", + "engines": { + "node": ">=16" + } + }, "node_modules/remark-external-links": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/remark-external-links/-/remark-external-links-8.0.0.tgz", @@ -16682,6 +17522,11 @@ "node": "*" } }, + "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/rollup": { "version": "3.29.4", "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz", @@ -16721,6 +17566,11 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==" + }, "node_modules/safe-array-concat": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.0.tgz", @@ -16779,8 +17629,7 @@ "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/sass": { "version": "1.70.0", @@ -17154,6 +18003,15 @@ "node": ">= 0.4" } }, + "node_modules/stoppable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz", + "integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==", + "engines": { + "node": ">=4", + "npm": ">=6" + } + }, "node_modules/store2": { "version": "2.14.2", "resolved": "https://registry.npmjs.org/store2/-/store2-2.14.2.tgz", diff --git a/webview_panels/package.json b/webview_panels/package.json index dd7c4bba9..6902a14d0 100644 --- a/webview_panels/package.json +++ b/webview_panels/package.json @@ -18,6 +18,10 @@ "build-storybook": "storybook build" }, "dependencies": { + "@finos/perspective": "^2.10.0", + "@finos/perspective-viewer": "^2.10.0", + "@finos/perspective-viewer-d3fc": "^2.10.0", + "@finos/perspective-viewer-datagrid": "^2.10.0", "@hookform/resolvers": "^3.3.4", "@reduxjs/toolkit": "^2.0.1", "@vscode/codicons": "^0.0.35", diff --git a/webview_panels/src/modules/queryPanel/QueryPanel.tsx b/webview_panels/src/modules/queryPanel/QueryPanel.tsx index 05a936f76..d4303c80e 100644 --- a/webview_panels/src/modules/queryPanel/QueryPanel.tsx +++ b/webview_panels/src/modules/queryPanel/QueryPanel.tsx @@ -2,18 +2,22 @@ import FeedbackButton from "@modules/commonActionButtons/FeedbackButton"; import { Stack } from "@uicore"; import HelpButton from "./components/help/HelpButton"; import ClearResultsButton from "./components/clearResultsButton/ClearResultsButton"; +import QueryPanelDefaultView from "./QueryPanelDefaultView"; const QueryPanel = (): JSX.Element => { return (
- Query panel + +   + +
); }; diff --git a/webview_panels/src/modules/queryPanel/QueryPanelDefaultView.tsx b/webview_panels/src/modules/queryPanel/QueryPanelDefaultView.tsx new file mode 100644 index 000000000..397dc33cb --- /dev/null +++ b/webview_panels/src/modules/queryPanel/QueryPanelDefaultView.tsx @@ -0,0 +1,18 @@ +import { Stack } from "@uicore"; +import HelpContent from "./components/help/HelpContent"; +import QueryLimit from "./components/queryLimit/QueryLimit"; +import TableScale from "./components/tableScale/TableScale"; + +const QueryPanelDefaultView = (): JSX.Element => { + return ( + +
+ + +
+ +
+ ); +}; + +export default QueryPanelDefaultView; diff --git a/webview_panels/src/modules/queryPanel/components/help/HelpButton.tsx b/webview_panels/src/modules/queryPanel/components/help/HelpButton.tsx index 9de0527a6..d02b4ee70 100644 --- a/webview_panels/src/modules/queryPanel/components/help/HelpButton.tsx +++ b/webview_panels/src/modules/queryPanel/components/help/HelpButton.tsx @@ -1,5 +1,6 @@ import { HelpIcon } from "@assets/icons"; import { Drawer } from "@uicore"; +import HelpContent from "./HelpContent"; const HelpButton = (): JSX.Element => { return ( @@ -12,24 +13,7 @@ const HelpButton = (): JSX.Element => { } title="Help" > -
-

Previewing Query

-

- Press Cmd+Enter (Mac) or Control+Enter (Windows/Linux) to run a query. - Highlight part of a query to preview only selection -

-

Default Query Limit

-

- Query preview is limited to 500 rows by default, this can be - configured in Settings -> dbt Power User or via changing the input - to the left which is synchronized with the VS Code setting -

-

Dispatched SQL

-

- This tab displays the compiled query sent to the database. You can - copy to run directly in your database. -

-
+ ); }; diff --git a/webview_panels/src/modules/queryPanel/components/help/HelpContent.tsx b/webview_panels/src/modules/queryPanel/components/help/HelpContent.tsx new file mode 100644 index 000000000..4bebe9ca6 --- /dev/null +++ b/webview_panels/src/modules/queryPanel/components/help/HelpContent.tsx @@ -0,0 +1,24 @@ +const HelpContent = (): JSX.Element => { + return ( +
+

Previewing Query

+

+ Press Cmd+Enter (Mac) or Control+Enter (Windows/Linux) to run a query. + Highlight part of a query to preview only selection +

+

Default Query Limit

+

+ Query preview is limited to 500 rows by default, this can be configured + in Settings -> dbt Power User or via changing the input to the left + which is synchronized with the VS Code setting +

+

Dispatched SQL

+

+ This tab displays the compiled query sent to the database. You can copy + to run directly in your database. +

+
+ ); +}; + +export default HelpContent; diff --git a/webview_panels/src/modules/queryPanel/components/perspective/PerspectiveViewer.stories.tsx b/webview_panels/src/modules/queryPanel/components/perspective/PerspectiveViewer.stories.tsx new file mode 100644 index 000000000..49392ed02 --- /dev/null +++ b/webview_panels/src/modules/queryPanel/components/perspective/PerspectiveViewer.stories.tsx @@ -0,0 +1,30 @@ +import type { Meta } from "@storybook/react"; +import PerspectiveViewer from "./PerspectiveViewer"; + +const meta = { + title: "PerspectiveViewer", + parameters: { + layout: "padded", + }, + tags: ["autodocs"], + argTypes: { + backgroundColor: { control: "color" }, + }, +} satisfies Meta; + +export default meta; + +export const DefaultPerspectiveViewerView = { + render: (): JSX.Element => { + return ( + + ); + }, +}; diff --git a/webview_panels/src/modules/queryPanel/components/perspective/PerspectiveViewer.tsx b/webview_panels/src/modules/queryPanel/components/perspective/PerspectiveViewer.tsx new file mode 100644 index 000000000..a28caf5c1 --- /dev/null +++ b/webview_panels/src/modules/queryPanel/components/perspective/PerspectiveViewer.tsx @@ -0,0 +1,38 @@ +import perspective, { TableData } from "@finos/perspective"; +import "@finos/perspective-viewer"; +import "@finos/perspective-viewer-datagrid"; +import "@finos/perspective-viewer-d3fc"; +import { + HTMLPerspectiveViewerElement, + PerspectiveViewerConfig, +} from "@finos/perspective-viewer"; +import "@finos/perspective-viewer/dist/css/solarized.css"; +import { useEffect, useRef } from "react"; +import { panelLogger } from "@modules/logger"; + +interface Props { + data: TableData; +} +const PerspectiveViewer = ({ data }: Props): JSX.Element => { + const loadPerspectiveData = async () => { + const config: PerspectiveViewerConfig = { + theme: "Solarized", + }; + const table = await perspective.worker().table(data); + await perspectiveViewerRef.current?.load(table); + await perspectiveViewerRef.current?.restore(config); + }; + useEffect(() => { + loadPerspectiveData().catch((err) => panelLogger.error(err)); + }, []); + const perspectiveViewerRef = useRef(null); + + return ( + + ); +}; + +export default PerspectiveViewer; diff --git a/webview_panels/src/modules/queryPanel/components/queryLimit/QueryLimit.tsx b/webview_panels/src/modules/queryPanel/components/queryLimit/QueryLimit.tsx new file mode 100644 index 000000000..ff809dec3 --- /dev/null +++ b/webview_panels/src/modules/queryPanel/components/queryLimit/QueryLimit.tsx @@ -0,0 +1,23 @@ +import { FormGroup, Label, Input } from "@uicore"; +import { ChangeEvent, useState } from "react"; + +const QueryLimit = (): JSX.Element => { + const [limit, setLimit] = useState(500); + const handleChange = (e: ChangeEvent) => { + setLimit(parseInt(e.target.value)); + }; + return ( + + + + + ); +}; + +export default QueryLimit; diff --git a/webview_panels/src/modules/queryPanel/components/tableScale/TableScale.tsx b/webview_panels/src/modules/queryPanel/components/tableScale/TableScale.tsx new file mode 100644 index 000000000..741bbe4ff --- /dev/null +++ b/webview_panels/src/modules/queryPanel/components/tableScale/TableScale.tsx @@ -0,0 +1,28 @@ +import { FormGroup, Label } from "@uicore"; +import { ChangeEvent, useState } from "react"; + +const TableScale = (): JSX.Element => { + const [scale, setScale] = useState(1); + const handleChange = (e: ChangeEvent) => { + setScale(parseInt(e.target.value) / 10); + }; + return ( + + +
+ +
+
+ ); +}; + +export default TableScale; From 99eae3521efef756e2368aa5acc9c10d89465b32 Mon Sep 17 00:00:00 2001 From: Sarav Date: Tue, 23 Apr 2024 14:51:24 +0530 Subject: [PATCH 03/24] fix: query panel components --- webview_panels/src/AppConstants.tsx | 4 +- webview_panels/src/assets/icons/index.tsx | 9 ++- webview_panels/src/assets/icons/spinner.gif | Bin 0 -> 56100 bytes .../commonActionButtons/FeedbackButton.tsx | 2 +- .../modules/queryPanel/QueryPanel.stories.tsx | 4 +- .../src/modules/queryPanel/QueryPanel.tsx | 11 ++- .../modules/queryPanel/QueryPanelProvider.tsx | 43 +++++++++++ .../QueryPanelContents/QueryPanelContent.tsx | 35 +++++++++ .../QueryPanelContents/QueryPanelError.tsx | 23 ++++++ .../QueryPanelContents/QueryPanelLoader.tsx | 22 ++++++ .../QueryPanelContents/QueryPanelTitle.tsx | 27 +++++++ .../clearResultsButton/ClearResultsButton.tsx | 7 +- .../src/modules/queryPanel/constants.ts | 30 ++++++++ .../queryPanel/context/queryPanelSlice.ts | 69 ++++++++++++++++++ .../src/modules/queryPanel/context/types.ts | 14 ++++ .../src/modules/queryPanel/useListeners.ts | 60 +++++++++++++++ .../modules/queryPanel/useQueryPanelState.ts | 18 +++++ 17 files changed, 367 insertions(+), 11 deletions(-) create mode 100644 webview_panels/src/assets/icons/spinner.gif create mode 100644 webview_panels/src/modules/queryPanel/QueryPanelProvider.tsx create mode 100644 webview_panels/src/modules/queryPanel/components/QueryPanelContents/QueryPanelContent.tsx create mode 100644 webview_panels/src/modules/queryPanel/components/QueryPanelContents/QueryPanelError.tsx create mode 100644 webview_panels/src/modules/queryPanel/components/QueryPanelContents/QueryPanelLoader.tsx create mode 100644 webview_panels/src/modules/queryPanel/components/QueryPanelContents/QueryPanelTitle.tsx create mode 100644 webview_panels/src/modules/queryPanel/constants.ts create mode 100644 webview_panels/src/modules/queryPanel/context/queryPanelSlice.ts create mode 100644 webview_panels/src/modules/queryPanel/context/types.ts create mode 100644 webview_panels/src/modules/queryPanel/useListeners.ts create mode 100644 webview_panels/src/modules/queryPanel/useQueryPanelState.ts diff --git a/webview_panels/src/AppConstants.tsx b/webview_panels/src/AppConstants.tsx index b79a1f99f..c4b40e872 100644 --- a/webview_panels/src/AppConstants.tsx +++ b/webview_panels/src/AppConstants.tsx @@ -4,7 +4,7 @@ import Home from "./modules/home/Home"; import DocumentationProvider from "@modules/documentationEditor/DocumentationProvider"; import DataPilotPanel from "@modules/dataPilot"; import DbtDocsView from "@modules/dbtDocs/DbtDocsView"; -import QueryPanel from "@modules/queryPanel/QueryPanel"; +import QueryPanelProvider from "@modules/queryPanel/QueryPanelProvider"; // TODO: lazy loading breaks loading dynamic webviews when having css because of vite dynamic loading // research on how to fix that and then use lazy loading @@ -24,5 +24,5 @@ export const AvailableRoutes = { component: , }, "/dbt-docs": { component: }, - "/query-panel": { component: }, + "/query-panel": { component: }, }; diff --git a/webview_panels/src/assets/icons/index.tsx b/webview_panels/src/assets/icons/index.tsx index 368ca38fe..8d74727c0 100644 --- a/webview_panels/src/assets/icons/index.tsx +++ b/webview_panels/src/assets/icons/index.tsx @@ -18,6 +18,7 @@ export { default as EmptySquareIcon } from "./square.svg?react"; export { default as CheckedSquareIcon } from "./checked-square.svg?react"; export { default as TestsIcon } from "./tests.svg?react"; export { default as FolderIcon } from "./folder.svg?react"; +import LoadingSpinnerUrl from "./spinner.gif"; interface Props { icon: string; @@ -110,6 +111,10 @@ export const CloseIcon = (props: HTMLAttributes): JSX.Element => ( ); -export const CommentIcon = (props: HTMLAttributes): JSX.Element => ( - +export const CommentIcon = ( + props: HTMLAttributes, +): JSX.Element => ; + +export const LoadingSpinner = (): JSX.Element => ( + Altimate loader ); diff --git a/webview_panels/src/assets/icons/spinner.gif b/webview_panels/src/assets/icons/spinner.gif new file mode 100644 index 0000000000000000000000000000000000000000..8a700a6ccf6621b6c19258bb65512af80ce8398a GIT binary patch literal 56100 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.YM!BUZWxM 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 CKl_9L literal 0 HcmV?d00001 diff --git a/webview_panels/src/modules/commonActionButtons/FeedbackButton.tsx b/webview_panels/src/modules/commonActionButtons/FeedbackButton.tsx index 4f1d5ff01..2dc71dead 100644 --- a/webview_panels/src/modules/commonActionButtons/FeedbackButton.tsx +++ b/webview_panels/src/modules/commonActionButtons/FeedbackButton.tsx @@ -2,7 +2,7 @@ import { FeedbackIcon } from "@assets/icons"; import { vscode } from "@modules/vscode"; import { Button } from "@uicore"; -const FeedbackButton = ({ url }: { url: string }) => { +const FeedbackButton = ({ url }: { url: string }): JSX.Element => { const handleFeedbackClick = () => { vscode.postMessage({ command: "openURL", diff --git a/webview_panels/src/modules/queryPanel/QueryPanel.stories.tsx b/webview_panels/src/modules/queryPanel/QueryPanel.stories.tsx index 6f56c6c51..87db629da 100644 --- a/webview_panels/src/modules/queryPanel/QueryPanel.stories.tsx +++ b/webview_panels/src/modules/queryPanel/QueryPanel.stories.tsx @@ -1,5 +1,5 @@ import type { Meta } from "@storybook/react"; -import QueryPanel from "./QueryPanel"; +import QueryPanelProvider from "./QueryPanelProvider"; const meta = { title: "Query Panel", @@ -16,6 +16,6 @@ export default meta; export const DefaultQueryPanelView = { render: (): JSX.Element => { - return ; + return ; }, }; diff --git a/webview_panels/src/modules/queryPanel/QueryPanel.tsx b/webview_panels/src/modules/queryPanel/QueryPanel.tsx index d4303c80e..16e9ef5d1 100644 --- a/webview_panels/src/modules/queryPanel/QueryPanel.tsx +++ b/webview_panels/src/modules/queryPanel/QueryPanel.tsx @@ -2,14 +2,19 @@ import FeedbackButton from "@modules/commonActionButtons/FeedbackButton"; import { Stack } from "@uicore"; import HelpButton from "./components/help/HelpButton"; import ClearResultsButton from "./components/clearResultsButton/ClearResultsButton"; -import QueryPanelDefaultView from "./QueryPanelDefaultView"; +import useListeners from "./useListeners"; +import QueryPanelTitle from "./components/QueryPanelContents/QueryPanelTitle"; +import QueryPanelContent from "./components/QueryPanelContents/QueryPanelContent"; +import { useState } from "react"; const QueryPanel = (): JSX.Element => { + const [showCompiledCode, setShowCompiledCode] = useState(false); + useListeners(); return (
-   + @@ -17,7 +22,7 @@ const QueryPanel = (): JSX.Element => { - +
); }; diff --git a/webview_panels/src/modules/queryPanel/QueryPanelProvider.tsx b/webview_panels/src/modules/queryPanel/QueryPanelProvider.tsx new file mode 100644 index 000000000..de6196949 --- /dev/null +++ b/webview_panels/src/modules/queryPanel/QueryPanelProvider.tsx @@ -0,0 +1,43 @@ +import { createContext, useContext, useMemo, useReducer } from "react"; +import QueryPanel from "./QueryPanel"; +import { QueryPanelStateProps } from "./context/types"; +import { UnknownAction } from "@reduxjs/toolkit"; +import queryPanelSlice from "./context/queryPanelSlice"; + +interface ContextProps { + state: QueryPanelStateProps; + dispatch: React.Dispatch; +} + +export const QueryPanelContext = createContext({ + state: queryPanelSlice.getInitialState(), + dispatch: () => null, +}); + +const QueryPanelProvider = (): JSX.Element => { + const [state, dispatch] = useReducer( + queryPanelSlice.reducer, + queryPanelSlice.getInitialState(), + ); + + const values = useMemo( + () => ({ + state, + dispatch, + }), + [state, dispatch], + ); + + return ( + + + + ); +}; + +export default QueryPanelProvider; + +export const useQueryPanelDispatch = (): React.Dispatch => { + const { dispatch } = useContext(QueryPanelContext); + return dispatch; +}; diff --git a/webview_panels/src/modules/queryPanel/components/QueryPanelContents/QueryPanelContent.tsx b/webview_panels/src/modules/queryPanel/components/QueryPanelContents/QueryPanelContent.tsx new file mode 100644 index 000000000..baaf0103c --- /dev/null +++ b/webview_panels/src/modules/queryPanel/components/QueryPanelContents/QueryPanelContent.tsx @@ -0,0 +1,35 @@ +import QueryPanelDefaultView from "@modules/queryPanel/QueryPanelDefaultView"; +import useQueryPanelState from "@modules/queryPanel/useQueryPanelState"; +import QueryPanelLoader from "./QueryPanelLoader"; +import PerspectiveViewer from "../perspective/PerspectiveViewer"; +import QueryPanelError from "./QueryPanelError"; +import { CodeBlock } from "@lib"; + +const QueryPanelContent = ({ + showCompiledCode, +}: { + showCompiledCode: boolean; +}): JSX.Element => { + const { loading, hasError, queryResults, compiledCodeMarkup } = + useQueryPanelState(); + + if (showCompiledCode && compiledCodeMarkup) { + return ; + } + + if (loading) { + return ; + } + + if (queryResults) { + return ; + } + + if (hasError) { + return ; + } + + return ; +}; + +export default QueryPanelContent; diff --git a/webview_panels/src/modules/queryPanel/components/QueryPanelContents/QueryPanelError.tsx b/webview_panels/src/modules/queryPanel/components/QueryPanelContents/QueryPanelError.tsx new file mode 100644 index 000000000..b134244be --- /dev/null +++ b/webview_panels/src/modules/queryPanel/components/QueryPanelContents/QueryPanelError.tsx @@ -0,0 +1,23 @@ +import { CodeBlock } from "@lib"; +import useQueryPanelState from "@modules/queryPanel/useQueryPanelState"; + +const QueryPanelError = (): JSX.Element => { + const { queryResultsError } = useQueryPanelState(); + + return ( +
+

{queryResultsError?.errorTitle}

+

{queryResultsError?.errorMessage}

+
+
+ View Detailed Error 🚨 + +
+
+ ); +}; + +export default QueryPanelError; diff --git a/webview_panels/src/modules/queryPanel/components/QueryPanelContents/QueryPanelLoader.tsx b/webview_panels/src/modules/queryPanel/components/QueryPanelContents/QueryPanelLoader.tsx new file mode 100644 index 000000000..5a4423245 --- /dev/null +++ b/webview_panels/src/modules/queryPanel/components/QueryPanelContents/QueryPanelLoader.tsx @@ -0,0 +1,22 @@ +import { LoadingSpinner } from "@assets/icons"; +import { HINTS } from "@modules/queryPanel/constants"; +import useQueryPanelState from "@modules/queryPanel/useQueryPanelState"; +import { Button } from "@uicore"; + +const QueryPanelLoader = (): JSX.Element => { + const { hintIndex } = useQueryPanelState(); + const hint = HINTS[hintIndex]; + return ( +
+ + {hint ? ( +
+ {hint.message} Docs +
+ ) : null} + +
+ ); +}; + +export default QueryPanelLoader; diff --git a/webview_panels/src/modules/queryPanel/components/QueryPanelContents/QueryPanelTitle.tsx b/webview_panels/src/modules/queryPanel/components/QueryPanelContents/QueryPanelTitle.tsx new file mode 100644 index 000000000..98d8df1a2 --- /dev/null +++ b/webview_panels/src/modules/queryPanel/components/QueryPanelContents/QueryPanelTitle.tsx @@ -0,0 +1,27 @@ +import useQueryPanelState from "@modules/queryPanel/useQueryPanelState"; + +const QueryPanelTitle = ({ + setShowCompiledCode, +}: { + setShowCompiledCode: (show: boolean) => void; +}): JSX.Element => { + const { loading, hasData, hasError, queryExecutionInfo, compiledCodeMarkup } = + useQueryPanelState(); + + if (loading || hasData || hasError) { + return ( +
+ setShowCompiledCode(false)}> + Preview {queryExecutionInfo} + + {compiledCodeMarkup ? ( + setShowCompiledCode(true)}>SQL + ) : null} +
+ ); + } + + return
Welcome 🚀
; +}; + +export default QueryPanelTitle; diff --git a/webview_panels/src/modules/queryPanel/components/clearResultsButton/ClearResultsButton.tsx b/webview_panels/src/modules/queryPanel/components/clearResultsButton/ClearResultsButton.tsx index a2a644db3..b942ededd 100644 --- a/webview_panels/src/modules/queryPanel/components/clearResultsButton/ClearResultsButton.tsx +++ b/webview_panels/src/modules/queryPanel/components/clearResultsButton/ClearResultsButton.tsx @@ -1,6 +1,11 @@ +import useQueryPanelState from "@modules/queryPanel/useQueryPanelState"; import { Button } from "@uicore"; -const ClearResultsButton = (): JSX.Element => { +const ClearResultsButton = (): JSX.Element | null => { + const { queryResults } = useQueryPanelState(); + if (!queryResults) { + return null; + } return ; }; diff --git a/webview_panels/src/modules/queryPanel/constants.ts b/webview_panels/src/modules/queryPanel/constants.ts new file mode 100644 index 000000000..a140ef32f --- /dev/null +++ b/webview_panels/src/modules/queryPanel/constants.ts @@ -0,0 +1,30 @@ +export const HINTS = [ + { + message: "Generate models from SQL or source", + link: "https://docs.myaltimate.com/develop/genmodelSQL/", + }, + { + message: "Generate documentation for dbt models", + link: "https://docs.myaltimate.com/document/generatedoc/", + }, + { + message: "Defer building upstream models", + link: "https://docs.myaltimate.com/test/defertoprod/", + }, + { + message: "Get a query explanation & ask questions", + link: "https://docs.myaltimate.com/develop/explanation/", + }, + { + message: "Update dbt Model in natural language", + link: "https://docs.myaltimate.com/develop/updatemodel/", + }, + { + message: "Explore real-time column lineage", + link: "https://docs.myaltimate.com/test/lineage/", + }, + { + message: "Write & edit dbt tests in UI", + link: "https://docs.myaltimate.com/test/writetests/", + }, +]; diff --git a/webview_panels/src/modules/queryPanel/context/queryPanelSlice.ts b/webview_panels/src/modules/queryPanel/context/queryPanelSlice.ts new file mode 100644 index 000000000..ab3754f84 --- /dev/null +++ b/webview_panels/src/modules/queryPanel/context/queryPanelSlice.ts @@ -0,0 +1,69 @@ +import { createSlice, PayloadAction } from "@reduxjs/toolkit"; +import { QueryPanelStateProps } from "./types"; + +export const initialState = { + loading: false, + queryResults: undefined, + queryExecutionInfo: undefined, + queryResultsError: undefined, + compiledCodeMarkup: undefined, + hintIndex: -1, +} as QueryPanelStateProps; + +const queryPanelSlice = createSlice({ + name: "queryPanelState", + initialState, + reducers: { + resetData: () => { + return initialState; + }, + setHintIndex: ( + state, + action: PayloadAction, + ) => { + state.hintIndex = action.payload; + }, + setCompiledCodeMarkup: ( + state, + action: PayloadAction, + ) => { + state.compiledCodeMarkup = action.payload; + }, + setQueryResultsError: ( + state, + action: PayloadAction, + ) => { + state.queryResultsError = action.payload; + }, + setQueryExecutionInfo: ( + state, + action: PayloadAction, + ) => { + state.queryExecutionInfo = action.payload; + }, + setQueryResults: ( + state, + action: PayloadAction, + ) => { + state.queryResults = action.payload; + }, + setLoading: ( + state, + action: PayloadAction, + ) => { + state.loading = action.payload; + }, + }, +}); + +export const { + resetData, + setLoading, + setHintIndex, + setCompiledCodeMarkup, + setQueryResultsError, + setQueryExecutionInfo, + setQueryResults, +} = queryPanelSlice.actions; + +export default queryPanelSlice; diff --git a/webview_panels/src/modules/queryPanel/context/types.ts b/webview_panels/src/modules/queryPanel/context/types.ts new file mode 100644 index 000000000..6b8e0122c --- /dev/null +++ b/webview_panels/src/modules/queryPanel/context/types.ts @@ -0,0 +1,14 @@ +import { TableData } from "@finos/perspective"; + +export interface QueryPanelStateProps { + loading: boolean; + queryResults?: TableData; + queryExecutionInfo?: string; + queryResultsError?: { + errorTitle: string; + errorMessage: string; + errorDataMarkup: string; + }; + compiledCodeMarkup?: string; + hintIndex: number; +} diff --git a/webview_panels/src/modules/queryPanel/useListeners.ts b/webview_panels/src/modules/queryPanel/useListeners.ts new file mode 100644 index 000000000..48e498e2d --- /dev/null +++ b/webview_panels/src/modules/queryPanel/useListeners.ts @@ -0,0 +1,60 @@ +import { IncomingMessageProps } from "@modules/app/types"; +import { useCallback, useEffect } from "react"; +import { useQueryPanelDispatch } from "./QueryPanelProvider"; +import { resetData, setLoading } from "./context/queryPanelSlice"; +import useQueryPanelState from "./useQueryPanelState"; +import { panelLogger } from "@modules/logger"; + +const useListeners = (): void => { + const dispatch = useQueryPanelDispatch(); + const { loading } = useQueryPanelState(); + + const handleLoading = () => { + if (loading) { + return; + } + dispatch(resetData()); + // this.focusPreviewPane(); + dispatch(setLoading(true)); + // this.timeExecution(); + // const now = Date.now(); + // this.showHint = false; + // if (this.lastHintTimestamp + HINT_VISIBILITY_DELAY < now) { + // this.showHint = true; + // this.lastHintTimestamp = now; + // HINTS.sort(() => Math.random() - 0.5); + // executeCommand("setContext", { + // key: "lastHintTimestamp", + // value: now, + // }); + // this.updateHintText(); + // this.hintInterval = setInterval(() => { + // this.updateHintText(); + // }, 3500); + // } + }; + + const onMesssage = useCallback( + (event: MessageEvent) => { + const { command, args } = event.data; + panelLogger.info("query panel onMesssage", command, args); + switch (command) { + case "renderLoading": + handleLoading(); + break; + default: + break; + } + }, + [], + ); + useEffect(() => { + window.addEventListener("message", onMesssage); + + return () => { + window.removeEventListener("message", onMesssage); + }; + }); +}; + +export default useListeners; diff --git a/webview_panels/src/modules/queryPanel/useQueryPanelState.ts b/webview_panels/src/modules/queryPanel/useQueryPanelState.ts new file mode 100644 index 000000000..ba8f1a912 --- /dev/null +++ b/webview_panels/src/modules/queryPanel/useQueryPanelState.ts @@ -0,0 +1,18 @@ +import { useContext } from "react"; +import { QueryPanelContext } from "./QueryPanelProvider"; +import { QueryPanelStateProps } from "./context/types"; + +const useQueryPanelState = (): QueryPanelStateProps & { + hasData: boolean; + hasError: boolean; + hasCode: boolean; +} => { + const { state } = useContext(QueryPanelContext); + + const hasData = Boolean(state.queryResults); + const hasError = Boolean(state.queryResultsError?.errorTitle); + const hasCode = Boolean(state.compiledCodeMarkup); + return { ...state, hasData, hasError, hasCode }; +}; + +export default useQueryPanelState; From 8e7ebcc95d653db5f36b40fc8ad51eb3d39f95a7 Mon Sep 17 00:00:00 2001 From: Sarav Date: Tue, 23 Apr 2024 16:12:42 +0530 Subject: [PATCH 04/24] fix: front end changes --- .../altimateWebviewProvider.ts | 6 + .../modules/queryPanel/QueryPanel.stories.tsx | 61 +++++++- .../src/modules/queryPanel/QueryPanel.tsx | 4 +- .../QueryPanelContents/QueryPanelTitle.tsx | 15 +- .../clearResultsButton/ClearResultsButton.tsx | 12 +- .../src/modules/queryPanel/constants.ts | 2 + .../queryPanel/context/queryPanelSlice.ts | 8 + .../src/modules/queryPanel/context/types.ts | 3 +- .../src/modules/queryPanel/useListeners.ts | 60 -------- .../queryPanel/useQueryPanelListeners.ts | 140 ++++++++++++++++++ .../modules/queryPanel/useQueryPanelState.ts | 9 +- 11 files changed, 251 insertions(+), 69 deletions(-) delete mode 100644 webview_panels/src/modules/queryPanel/useListeners.ts create mode 100644 webview_panels/src/modules/queryPanel/useQueryPanelListeners.ts diff --git a/src/webview_provider/altimateWebviewProvider.ts b/src/webview_provider/altimateWebviewProvider.ts index ec4f31f86..65a24da34 100644 --- a/src/webview_provider/altimateWebviewProvider.ts +++ b/src/webview_provider/altimateWebviewProvider.ts @@ -296,6 +296,12 @@ export class AltimateWebviewProvider implements WebviewViewProvider { params.properties as { [key: string]: string }, ); break; + case "setContext": + this.dbtProjectContainer.setToGlobalState( + params.key as string, + params.value, + ); + break; case "updateConfig": if (!this.isUpdateConfigProps(params)) { return; diff --git a/webview_panels/src/modules/queryPanel/QueryPanel.stories.tsx b/webview_panels/src/modules/queryPanel/QueryPanel.stories.tsx index 87db629da..61034048d 100644 --- a/webview_panels/src/modules/queryPanel/QueryPanel.stories.tsx +++ b/webview_panels/src/modules/queryPanel/QueryPanel.stories.tsx @@ -1,5 +1,6 @@ import type { Meta } from "@storybook/react"; import QueryPanelProvider from "./QueryPanelProvider"; +import { Button, Stack } from "@uicore"; const meta = { title: "Query Panel", @@ -14,8 +15,66 @@ const meta = { export default meta; +const ActionButton = ({ + data, + title, +}: { + data: { command: string } & Record; + title: string; +}) => { + const handleAction = () => { + window.postMessage({ + command: data.command, + args: data, + }); + }; + return ; +}; + export const DefaultQueryPanelView = { render: (): JSX.Element => { - return ; + return ( +
+ + + + + + + +
+ ); + }, + parameters: { + vscode: { + func: (request: Record): unknown => { + if (request.command === "getQueryPanelContext") { + return { lastHintTimestamp: 0 }; + } + }, + timer: 500, + }, }, }; diff --git a/webview_panels/src/modules/queryPanel/QueryPanel.tsx b/webview_panels/src/modules/queryPanel/QueryPanel.tsx index 16e9ef5d1..b70792db4 100644 --- a/webview_panels/src/modules/queryPanel/QueryPanel.tsx +++ b/webview_panels/src/modules/queryPanel/QueryPanel.tsx @@ -2,14 +2,14 @@ import FeedbackButton from "@modules/commonActionButtons/FeedbackButton"; import { Stack } from "@uicore"; import HelpButton from "./components/help/HelpButton"; import ClearResultsButton from "./components/clearResultsButton/ClearResultsButton"; -import useListeners from "./useListeners"; +import useQueryPanelListeners from "./useQueryPanelListeners"; import QueryPanelTitle from "./components/QueryPanelContents/QueryPanelTitle"; import QueryPanelContent from "./components/QueryPanelContents/QueryPanelContent"; import { useState } from "react"; const QueryPanel = (): JSX.Element => { const [showCompiledCode, setShowCompiledCode] = useState(false); - useListeners(); + useQueryPanelListeners(); return (
diff --git a/webview_panels/src/modules/queryPanel/components/QueryPanelContents/QueryPanelTitle.tsx b/webview_panels/src/modules/queryPanel/components/QueryPanelContents/QueryPanelTitle.tsx index 98d8df1a2..a83bb0c28 100644 --- a/webview_panels/src/modules/queryPanel/components/QueryPanelContents/QueryPanelTitle.tsx +++ b/webview_panels/src/modules/queryPanel/components/QueryPanelContents/QueryPanelTitle.tsx @@ -5,14 +5,23 @@ const QueryPanelTitle = ({ }: { setShowCompiledCode: (show: boolean) => void; }): JSX.Element => { - const { loading, hasData, hasError, queryExecutionInfo, compiledCodeMarkup } = - useQueryPanelState(); + const { + loading, + hasData, + hasError, + queryExecutionInfo, + compiledCodeMarkup, + queryResultsRowCount, + } = useQueryPanelState(); if (loading || hasData || hasError) { return (
setShowCompiledCode(false)}> - Preview {queryExecutionInfo} + Preview{" "} + + {queryResultsRowCount} rows in {queryExecutionInfo?.elapsedTime}s + {compiledCodeMarkup ? ( setShowCompiledCode(true)}>SQL diff --git a/webview_panels/src/modules/queryPanel/components/clearResultsButton/ClearResultsButton.tsx b/webview_panels/src/modules/queryPanel/components/clearResultsButton/ClearResultsButton.tsx index b942ededd..ed548e72c 100644 --- a/webview_panels/src/modules/queryPanel/components/clearResultsButton/ClearResultsButton.tsx +++ b/webview_panels/src/modules/queryPanel/components/clearResultsButton/ClearResultsButton.tsx @@ -1,12 +1,22 @@ +import { useQueryPanelDispatch } from "@modules/queryPanel/QueryPanelProvider"; +import { resetData } from "@modules/queryPanel/context/queryPanelSlice"; import useQueryPanelState from "@modules/queryPanel/useQueryPanelState"; import { Button } from "@uicore"; const ClearResultsButton = (): JSX.Element | null => { const { queryResults } = useQueryPanelState(); + const dispatch = useQueryPanelDispatch(); + const handleClear = () => { + dispatch(resetData()); + }; if (!queryResults) { return null; } - return ; + return ( + + ); }; export default ClearResultsButton; diff --git a/webview_panels/src/modules/queryPanel/constants.ts b/webview_panels/src/modules/queryPanel/constants.ts index a140ef32f..7fb0d59d5 100644 --- a/webview_panels/src/modules/queryPanel/constants.ts +++ b/webview_panels/src/modules/queryPanel/constants.ts @@ -1,3 +1,5 @@ +export const HINT_VISIBILITY_DELAY = 1 * 60 * 60 * 1000; // 1 hr + export const HINTS = [ { message: "Generate models from SQL or source", diff --git a/webview_panels/src/modules/queryPanel/context/queryPanelSlice.ts b/webview_panels/src/modules/queryPanel/context/queryPanelSlice.ts index ab3754f84..8b01d89a5 100644 --- a/webview_panels/src/modules/queryPanel/context/queryPanelSlice.ts +++ b/webview_panels/src/modules/queryPanel/context/queryPanelSlice.ts @@ -8,6 +8,7 @@ export const initialState = { queryResultsError: undefined, compiledCodeMarkup: undefined, hintIndex: -1, + lastHintTimestamp: 0, } as QueryPanelStateProps; const queryPanelSlice = createSlice({ @@ -29,6 +30,12 @@ const queryPanelSlice = createSlice({ ) => { state.compiledCodeMarkup = action.payload; }, + setLastHintTimestamp: ( + state, + action: PayloadAction, + ) => { + state.lastHintTimestamp = action.payload; + }, setQueryResultsError: ( state, action: PayloadAction, @@ -64,6 +71,7 @@ export const { setQueryResultsError, setQueryExecutionInfo, setQueryResults, + setLastHintTimestamp, } = queryPanelSlice.actions; export default queryPanelSlice; diff --git a/webview_panels/src/modules/queryPanel/context/types.ts b/webview_panels/src/modules/queryPanel/context/types.ts index 6b8e0122c..54df371a5 100644 --- a/webview_panels/src/modules/queryPanel/context/types.ts +++ b/webview_panels/src/modules/queryPanel/context/types.ts @@ -3,7 +3,7 @@ import { TableData } from "@finos/perspective"; export interface QueryPanelStateProps { loading: boolean; queryResults?: TableData; - queryExecutionInfo?: string; + queryExecutionInfo?: { elapsedTime: number }; queryResultsError?: { errorTitle: string; errorMessage: string; @@ -11,4 +11,5 @@ export interface QueryPanelStateProps { }; compiledCodeMarkup?: string; hintIndex: number; + lastHintTimestamp: number; } diff --git a/webview_panels/src/modules/queryPanel/useListeners.ts b/webview_panels/src/modules/queryPanel/useListeners.ts deleted file mode 100644 index 48e498e2d..000000000 --- a/webview_panels/src/modules/queryPanel/useListeners.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { IncomingMessageProps } from "@modules/app/types"; -import { useCallback, useEffect } from "react"; -import { useQueryPanelDispatch } from "./QueryPanelProvider"; -import { resetData, setLoading } from "./context/queryPanelSlice"; -import useQueryPanelState from "./useQueryPanelState"; -import { panelLogger } from "@modules/logger"; - -const useListeners = (): void => { - const dispatch = useQueryPanelDispatch(); - const { loading } = useQueryPanelState(); - - const handleLoading = () => { - if (loading) { - return; - } - dispatch(resetData()); - // this.focusPreviewPane(); - dispatch(setLoading(true)); - // this.timeExecution(); - // const now = Date.now(); - // this.showHint = false; - // if (this.lastHintTimestamp + HINT_VISIBILITY_DELAY < now) { - // this.showHint = true; - // this.lastHintTimestamp = now; - // HINTS.sort(() => Math.random() - 0.5); - // executeCommand("setContext", { - // key: "lastHintTimestamp", - // value: now, - // }); - // this.updateHintText(); - // this.hintInterval = setInterval(() => { - // this.updateHintText(); - // }, 3500); - // } - }; - - const onMesssage = useCallback( - (event: MessageEvent) => { - const { command, args } = event.data; - panelLogger.info("query panel onMesssage", command, args); - switch (command) { - case "renderLoading": - handleLoading(); - break; - default: - break; - } - }, - [], - ); - useEffect(() => { - window.addEventListener("message", onMesssage); - - return () => { - window.removeEventListener("message", onMesssage); - }; - }); -}; - -export default useListeners; diff --git a/webview_panels/src/modules/queryPanel/useQueryPanelListeners.ts b/webview_panels/src/modules/queryPanel/useQueryPanelListeners.ts new file mode 100644 index 000000000..d9acf1537 --- /dev/null +++ b/webview_panels/src/modules/queryPanel/useQueryPanelListeners.ts @@ -0,0 +1,140 @@ +import { IncomingMessageProps } from "@modules/app/types"; +import { useCallback, useEffect, useRef } from "react"; +import { useQueryPanelDispatch } from "./QueryPanelProvider"; +import { + resetData, + setCompiledCodeMarkup, + setHintIndex, + setLastHintTimestamp, + setLoading, + setQueryExecutionInfo, + setQueryResults, + setQueryResultsError, +} from "./context/queryPanelSlice"; +import useQueryPanelState from "./useQueryPanelState"; +import { panelLogger } from "@modules/logger"; +import { + executeRequestInAsync, + executeRequestInSync, +} from "@modules/app/requestExecutor"; +import { HINTS, HINT_VISIBILITY_DELAY } from "./constants"; +import { QueryPanelStateProps } from "./context/types"; + +const useQueryPanelListeners = (): void => { + const dispatch = useQueryPanelDispatch(); + const { loading, lastHintTimestamp, hintIndex } = useQueryPanelState(); + const hintInterval = useRef(); + const queryExecutionTimer = useRef(); + const queryStart = useRef(Date.now()); + + const handleHintMessage = () => { + const now = Date.now(); + dispatch(setHintIndex(-1)); + if (lastHintTimestamp + HINT_VISIBILITY_DELAY < now) { + dispatch(setLastHintTimestamp(now)); + HINTS.sort(() => Math.random() - 0.5); + executeRequestInAsync("setContext", { + key: "lastHintTimestamp", + value: now, + }); + dispatch(setHintIndex((hintIndex + 1) % HINTS.length)); + + hintInterval.current = setInterval(() => { + dispatch(setHintIndex((hintIndex + 1) % HINTS.length)); + }, 3500); + } + }; + + const clearData = () => { + dispatch(resetData()); + queryStart.current = Date.now(); + }; + + const endQueryExecutionTimer = () => + clearTimeout(queryExecutionTimer.current); + + const handleLoading = () => { + if (loading) { + return; + } + clearData(); + dispatch(setLoading(true)); + queryExecutionTimer.current = setInterval(() => { + const now = Date.now(); + const elapsedTime = Math.round((now - queryStart.current) / 100) / 10; + const time = isNaN(elapsedTime) ? 0 : elapsedTime; + dispatch(setQueryExecutionInfo({ elapsedTime: time })); + }, 100); + handleHintMessage(); + }; + + const clearHintInterval = () => { + clearInterval(hintInterval.current); + hintInterval.current = undefined; + }; + + const handleError = (args: Record) => { + dispatch( + setQueryResultsError(args as QueryPanelStateProps["queryResultsError"]), + ); + dispatch(setCompiledCodeMarkup(args.compiled_sql as string)); + clearHintInterval(); + endQueryExecutionTimer(); + }; + + const handleQueryResults = (args: Record) => { + dispatch(setLoading(false)); + dispatch(setQueryResults(args as QueryPanelStateProps["queryResults"])); + dispatch(setCompiledCodeMarkup(args.compiled_sql as string)); + clearHintInterval(); + endQueryExecutionTimer(); + }; + + const handleResetState = () => { + clearData(); + clearHintInterval(); + endQueryExecutionTimer(); + }; + + const onMesssage = useCallback( + (event: MessageEvent) => { + const { command, args } = event.data; + panelLogger.info("query panel onMesssage", command, args); + switch (command) { + case "renderError": + handleError(args); + break; + case "resetState": + handleResetState(); + break; + case "renderQuery": + handleQueryResults(args); + break; + case "renderLoading": + handleLoading(); + break; + default: + break; + } + }, + [handleLoading], + ); + + const getContext = async () => { + const result = (await executeRequestInSync("getQueryPanelContext", {})) as { + lastHintTimestamp: number; + }; + dispatch(setLastHintTimestamp(result.lastHintTimestamp)); + }; + + useEffect(() => { + window.addEventListener("message", onMesssage); + void getContext(); + + return () => { + window.removeEventListener("message", onMesssage); + }; + }, []); +}; + +export default useQueryPanelListeners; diff --git a/webview_panels/src/modules/queryPanel/useQueryPanelState.ts b/webview_panels/src/modules/queryPanel/useQueryPanelState.ts index ba8f1a912..5f081bc00 100644 --- a/webview_panels/src/modules/queryPanel/useQueryPanelState.ts +++ b/webview_panels/src/modules/queryPanel/useQueryPanelState.ts @@ -6,13 +6,20 @@ const useQueryPanelState = (): QueryPanelStateProps & { hasData: boolean; hasError: boolean; hasCode: boolean; + queryResultsRowCount: number; } => { const { state } = useContext(QueryPanelContext); const hasData = Boolean(state.queryResults); const hasError = Boolean(state.queryResultsError?.errorTitle); const hasCode = Boolean(state.compiledCodeMarkup); - return { ...state, hasData, hasError, hasCode }; + return { + ...state, + hasData, + hasError, + hasCode, + queryResultsRowCount: (state.queryResults as [] | undefined)?.length ?? 0, + }; }; export default useQueryPanelState; From 8bcc5befb9963730619796dcaacddfa219939498 Mon Sep 17 00:00:00 2001 From: Sarav Date: Fri, 26 Apr 2024 14:25:21 +0530 Subject: [PATCH 05/24] fix: perspective in vscode --- package.json | 5 ++ src/comment_provider/conversationProvider.ts | 4 +- src/services/usersService.ts | 4 +- .../altimateWebviewProvider.ts | 2 +- src/webview_provider/queryResultPanel.ts | 77 ++++++++++++++---- webview_panels/.storybook/preview-head.html | 10 +++ .../src/modules/queryPanel/QueryPanel.tsx | 7 +- .../perspective/PerspectiveViewer.stories.tsx | 15 ++-- .../perspective/PerspectiveViewer.tsx | 81 ++++++++++++++++--- .../perspective/perspective.module.scss | 6 ++ .../components/perspective/perspective.scss | 25 ++++++ .../modules/queryPanel/querypanel.module.scss | 11 +++ .../queryPanel/useQueryPanelListeners.ts | 6 +- 13 files changed, 214 insertions(+), 39 deletions(-) create mode 100644 webview_panels/.storybook/preview-head.html create mode 100644 webview_panels/src/modules/queryPanel/components/perspective/perspective.module.scss create mode 100644 webview_panels/src/modules/queryPanel/components/perspective/perspective.scss create mode 100644 webview_panels/src/modules/queryPanel/querypanel.module.scss diff --git a/package.json b/package.json index 9339bfa0b..922992c3d 100644 --- a/package.json +++ b/package.json @@ -86,6 +86,11 @@ "description": "Enable the new query panel in dbt.", "default": true }, + "dbt.enableQueryPanelV2": { + "type": "boolean", + "description": "Enable the new query panel in dbt.", + "default": false + }, "dbt.lineage.showSelectEdges": { "type": "boolean", "description": "Show select edges in lineage panel.", diff --git a/src/comment_provider/conversationProvider.ts b/src/comment_provider/conversationProvider.ts index 62ea3d542..27a7e705c 100644 --- a/src/comment_provider/conversationProvider.ts +++ b/src/comment_provider/conversationProvider.ts @@ -108,7 +108,7 @@ export class ConversationProvider implements Disposable { d.command === "manifestCacheChanged" || d.command === "refetchConversations" ) { - this.loadThreads(); + // this.loadThreads(); } }), ); @@ -141,7 +141,7 @@ export class ConversationProvider implements Disposable { pollingInterval, ); this.timer = setTimeout(() => { - this.loadThreads(); + // this.loadThreads(); }, pollingInterval * 1000); } diff --git a/src/services/usersService.ts b/src/services/usersService.ts index 7d8d09c38..1188f39ad 100644 --- a/src/services/usersService.ts +++ b/src/services/usersService.ts @@ -41,8 +41,8 @@ export class UsersService implements Disposable { event: DBTInstallationVerificationEvent, ) { if (event.installed) { - this.loadCurrentUser(); - this.loadUsersInTenant(); + // this.loadCurrentUser(); + // this.loadUsersInTenant(); } } diff --git a/src/webview_provider/altimateWebviewProvider.ts b/src/webview_provider/altimateWebviewProvider.ts index 65a24da34..8780eac9c 100644 --- a/src/webview_provider/altimateWebviewProvider.ts +++ b/src/webview_provider/altimateWebviewProvider.ts @@ -466,7 +466,7 @@ export class AltimateWebviewProvider implements WebviewViewProvider { and only allow scripts that have a specific nonce. Added unsafe-inline for css due to csp issue: https://github.com/JedWatson/react-select/issues/4631 --> - + VSCode DBT Power user extension diff --git a/src/webview_provider/queryResultPanel.ts b/src/webview_provider/queryResultPanel.ts index ade9a7f47..dadf3939f 100644 --- a/src/webview_provider/queryResultPanel.ts +++ b/src/webview_provider/queryResultPanel.ts @@ -27,6 +27,10 @@ import { QueryExecution, } from "../dbt_client/dbtIntegration"; import { SharedStateService } from "../services/sharedStateService"; +import { AltimateWebviewProvider } from "./altimateWebviewProvider"; +import { DBTTerminal } from "../dbt_client/dbtTerminal"; +import { QueryManifestService } from "../services/queryManifestService"; +import { UsersService } from "../services/usersService"; interface JsonObj { [key: string]: string | number | undefined; @@ -95,27 +99,60 @@ interface RecOpenUrl { } @provideSingleton(QueryResultPanel) -export class QueryResultPanel implements WebviewViewProvider { +export class QueryResultPanel extends AltimateWebviewProvider { public static readonly viewType = "dbtPowerUser.PreviewResults"; + protected viewPath = "/query-panel"; + protected panelDescription = "Query results panel"; - private _disposables: Disposable[] = []; - private _panel: WebviewView | undefined; + protected _panel: WebviewView | undefined; private queryExecution?: QueryExecution; public constructor( - private dbtProjectContainer: DBTProjectContainer, - private telemetry: TelemetryService, + protected dbtProjectContainer: DBTProjectContainer, + protected telemetry: TelemetryService, private altimate: AltimateRequest, private eventEmitterService: SharedStateService, + protected dbtTerminal: DBTTerminal, + protected queryManifestService: QueryManifestService, + protected usersService: UsersService, ) { + super( + dbtProjectContainer, + altimate, + telemetry, + eventEmitterService, + dbtTerminal, + queryManifestService, + usersService, + ); + this._disposables.push( + workspace.onDidChangeConfiguration( + (e) => { + if (!e.affectsConfiguration("dbt.enableQueryPanelV2")) { + return; + } + if (this._panel) { + this.renderWebviewView(this._panel.webview); + } + }, + this, + this._disposables, + ), + ); window.onDidChangeActiveColorTheme( (e) => { if (this._panel) { - this._panel.webview.html = getHtml( - this._panel.webview, - this.dbtProjectContainer.extensionUri, - ); - this.transmitConfig(); + const enableQueryPanelV2 = workspace + .getConfiguration("dbt") + .get("enableQueryPanelV2", false); + + if (!enableQueryPanelV2) { + this._panel.webview.html = getHtml( + this._panel.webview, + this.dbtProjectContainer.extensionUri, + ); + this.transmitConfig(); + } } }, null, @@ -129,8 +166,8 @@ export class QueryResultPanel implements WebviewViewProvider { _token: CancellationToken, ) { this._panel = panel; - this.setupWebviewOptions(context); - this.renderWebviewView(context); + this.bindWebviewOptions(context); + this.renderWebviewView(panel.webview); this.setupWebviewHooks(context); this.transmitConfig(); await this._panel?.webview.postMessage({ @@ -144,7 +181,7 @@ export class QueryResultPanel implements WebviewViewProvider { } /** Sets options, note that retainContextWhen hidden is set on registration */ - private setupWebviewOptions(context: WebviewViewResolveContext) { + private bindWebviewOptions(context: WebviewViewResolveContext) { this._panel!.title = "Query Results"; this._panel!.description = "Preview dbt SQL Results"; this._panel!.webview.options = { enableScripts: true }; @@ -231,8 +268,18 @@ export class QueryResultPanel implements WebviewViewProvider { } /** Renders webview content */ - private renderWebviewView(context: WebviewViewResolveContext) { - const webview = this._panel!.webview!; + protected renderWebviewView(webview: Webview) { + const enableQueryPanelV2 = workspace + .getConfiguration("dbt") + .get("enableQueryPanelV2", false); + + if (enableQueryPanelV2) { + this._panel!.webview.html = super.getHtml( + webview, + this.dbtProjectContainer.extensionUri, + ); + return; + } this._panel!.webview.html = getHtml( webview, this.dbtProjectContainer.extensionUri, diff --git a/webview_panels/.storybook/preview-head.html b/webview_panels/.storybook/preview-head.html new file mode 100644 index 000000000..58282f7ae --- /dev/null +++ b/webview_panels/.storybook/preview-head.html @@ -0,0 +1,10 @@ + + + diff --git a/webview_panels/src/modules/queryPanel/QueryPanel.tsx b/webview_panels/src/modules/queryPanel/QueryPanel.tsx index b70792db4..6715a4f71 100644 --- a/webview_panels/src/modules/queryPanel/QueryPanel.tsx +++ b/webview_panels/src/modules/queryPanel/QueryPanel.tsx @@ -6,12 +6,13 @@ import useQueryPanelListeners from "./useQueryPanelListeners"; import QueryPanelTitle from "./components/QueryPanelContents/QueryPanelTitle"; import QueryPanelContent from "./components/QueryPanelContents/QueryPanelContent"; import { useState } from "react"; +import classes from "./querypanel.module.scss"; const QueryPanel = (): JSX.Element => { const [showCompiledCode, setShowCompiledCode] = useState(false); useQueryPanelListeners(); return ( -
+
@@ -22,7 +23,9 @@ const QueryPanel = (): JSX.Element => { - +
+ +
); }; diff --git a/webview_panels/src/modules/queryPanel/components/perspective/PerspectiveViewer.stories.tsx b/webview_panels/src/modules/queryPanel/components/perspective/PerspectiveViewer.stories.tsx index 49392ed02..06d9db616 100644 --- a/webview_panels/src/modules/queryPanel/components/perspective/PerspectiveViewer.stories.tsx +++ b/webview_panels/src/modules/queryPanel/components/perspective/PerspectiveViewer.stories.tsx @@ -1,5 +1,6 @@ import type { Meta } from "@storybook/react"; import PerspectiveViewer from "./PerspectiveViewer"; +import { TableData } from "@finos/perspective"; const meta = { title: "PerspectiveViewer", @@ -18,12 +19,14 @@ export const DefaultPerspectiveViewerView = { render: (): JSX.Element => { return ( ); }, diff --git a/webview_panels/src/modules/queryPanel/components/perspective/PerspectiveViewer.tsx b/webview_panels/src/modules/queryPanel/components/perspective/PerspectiveViewer.tsx index a28caf5c1..f15308662 100644 --- a/webview_panels/src/modules/queryPanel/components/perspective/PerspectiveViewer.tsx +++ b/webview_panels/src/modules/queryPanel/components/perspective/PerspectiveViewer.tsx @@ -6,30 +6,93 @@ import { HTMLPerspectiveViewerElement, PerspectiveViewerConfig, } from "@finos/perspective-viewer"; -import "@finos/perspective-viewer/dist/css/solarized.css"; -import { useEffect, useRef } from "react"; +import "@finos/perspective-viewer/dist/css/pro.css"; +import { useEffect, useRef, useState } from "react"; import { panelLogger } from "@modules/logger"; +import useAppContext from "@modules/app/useAppContext"; +import { Themes } from "@modules/app/types"; +import classes from "./perspective.module.scss"; +import perspectiveStyles from "./perspective.scss?inline"; interface Props { data: TableData; } const PerspectiveViewer = ({ data }: Props): JSX.Element => { + const { + state: { theme }, + } = useAppContext(); + const [tableRendered, setTableRendered] = useState(false); + const perspectiveViewerRef = useRef(null); + + const config: PerspectiveViewerConfig = { + theme: theme === Themes.Dark ? "Pro Dark" : "Pro Light", + title: "query result", + }; + const loadPerspectiveData = async () => { - const config: PerspectiveViewerConfig = { - theme: "Solarized", + if (!perspectiveViewerRef.current) { + return; + } + + const styles = { + types: { + integer: { + format: { + useGrouping: false, + }, + }, + float: { + format: { + maximumFractionDigits: 20, + minimumFractionDigits: 0, + useGrouping: false, + }, + }, + }, }; - const table = await perspective.worker().table(data); - await perspectiveViewerRef.current?.load(table); - await perspectiveViewerRef.current?.restore(config); + // @ts-expect-error valid parameter + const table = await perspective.worker(styles).table(data); + + await perspectiveViewerRef.current.load(table); + await perspectiveViewerRef.current.resetThemes(["Pro Light"]); + await perspectiveViewerRef.current.restore(config); + + const shadowRoot = perspectiveViewerRef.current?.querySelector( + "perspective-viewer-datagrid", + )?.shadowRoot; + if (!shadowRoot) { + return; + } + const style = document.createElement("style"); + style.textContent = perspectiveStyles; + shadowRoot.appendChild(style); + shadowRoot.querySelector("regular-table")?.setAttribute("theme", theme); + setTableRendered(true); }; + useEffect(() => { loadPerspectiveData().catch((err) => panelLogger.error(err)); }, []); - const perspectiveViewerRef = useRef(null); + + useEffect(() => { + if (!tableRendered || !config.theme || !perspectiveViewerRef.current) { + return; + } + + perspectiveViewerRef.current + ?.querySelector("perspective-viewer-datagrid") + ?.shadowRoot?.querySelector("regular-table") + ?.setAttribute("theme", theme); + perspectiveViewerRef.current + .restore(config) + .catch((err) => + panelLogger.error("error while restoring perspective", err), + ); + }, [theme, tableRendered]); return ( ); diff --git a/webview_panels/src/modules/queryPanel/components/perspective/perspective.module.scss b/webview_panels/src/modules/queryPanel/components/perspective/perspective.module.scss new file mode 100644 index 000000000..a4d117525 --- /dev/null +++ b/webview_panels/src/modules/queryPanel/components/perspective/perspective.module.scss @@ -0,0 +1,6 @@ +.altimatePerspectiveViewer { + height: 100%; + font-size: 1rem !important; + font-family: var(--bs-body-font-family) !important; + width: auto; +} \ No newline at end of file diff --git a/webview_panels/src/modules/queryPanel/components/perspective/perspective.scss b/webview_panels/src/modules/queryPanel/components/perspective/perspective.scss new file mode 100644 index 000000000..300d9e572 --- /dev/null +++ b/webview_panels/src/modules/queryPanel/components/perspective/perspective.scss @@ -0,0 +1,25 @@ +regular-table{ +tbody { + tr:nth-child(even) { + background-color: rgb(239, 239, 239); + } + tr:hover { + background-color: rgb(187, 187, 187); + cursor: pointer; + } + td { + color: var(--text-color--paragraph) !important; + } + } +} + +regular-table[theme="dark"] { + tbody { + tr:nth-child(even) { + background-color: var(--background--03); + } + tr:hover { + background-color: var(--background--01); + } + } +} diff --git a/webview_panels/src/modules/queryPanel/querypanel.module.scss b/webview_panels/src/modules/queryPanel/querypanel.module.scss new file mode 100644 index 000000000..f3a5afeea --- /dev/null +++ b/webview_panels/src/modules/queryPanel/querypanel.module.scss @@ -0,0 +1,11 @@ +.queryPanel { + position: absolute; + width: 100vw; + height: 100vh; + box-sizing: border-box; + top: 0; + left: 0; + padding: 0.5rem; + display: flex; + flex-direction: column; +} \ No newline at end of file diff --git a/webview_panels/src/modules/queryPanel/useQueryPanelListeners.ts b/webview_panels/src/modules/queryPanel/useQueryPanelListeners.ts index d9acf1537..387c26a7d 100644 --- a/webview_panels/src/modules/queryPanel/useQueryPanelListeners.ts +++ b/webview_panels/src/modules/queryPanel/useQueryPanelListeners.ts @@ -84,7 +84,9 @@ const useQueryPanelListeners = (): void => { const handleQueryResults = (args: Record) => { dispatch(setLoading(false)); - dispatch(setQueryResults(args as QueryPanelStateProps["queryResults"])); + dispatch( + setQueryResults(args.rows as QueryPanelStateProps["queryResults"]), + ); dispatch(setCompiledCodeMarkup(args.compiled_sql as string)); clearHintInterval(); endQueryExecutionTimer(); @@ -98,7 +100,7 @@ const useQueryPanelListeners = (): void => { const onMesssage = useCallback( (event: MessageEvent) => { - const { command, args } = event.data; + const { command, ...args } = event.data; panelLogger.info("query panel onMesssage", command, args); switch (command) { case "renderError": From 0df91039ae4b121ae82a910d7e5a8fbbb3691ccc Mon Sep 17 00:00:00 2001 From: Sarav Date: Fri, 26 Apr 2024 19:52:40 +0530 Subject: [PATCH 06/24] fix: ui styling --- src/webview_provider/queryResultPanel.ts | 164 ++++++++++-------- webview_panels/.storybook/preview-head.html | 2 +- .../modules/queryPanel/QueryPanel.stories.tsx | 9 +- .../src/modules/queryPanel/QueryPanel.tsx | 5 +- .../queryPanel/QueryPanelDefaultView.tsx | 2 +- .../QueryPanelContents/QueryPanelLoader.tsx | 16 +- .../QueryPanelContents/QueryPanelTitle.tsx | 41 +++-- .../perspective/perspective.module.scss | 3 + .../components/perspective/perspective.scss | 8 +- .../modules/queryPanel/querypanel.module.scss | 18 ++ 10 files changed, 173 insertions(+), 95 deletions(-) diff --git a/src/webview_provider/queryResultPanel.ts b/src/webview_provider/queryResultPanel.ts index dadf3939f..e85e9cb98 100644 --- a/src/webview_provider/queryResultPanel.ts +++ b/src/webview_provider/queryResultPanel.ts @@ -27,7 +27,10 @@ import { QueryExecution, } from "../dbt_client/dbtIntegration"; import { SharedStateService } from "../services/sharedStateService"; -import { AltimateWebviewProvider } from "./altimateWebviewProvider"; +import { + AltimateWebviewProvider, + HandleCommandProps, +} from "./altimateWebviewProvider"; import { DBTTerminal } from "../dbt_client/dbtTerminal"; import { QueryManifestService } from "../services/queryManifestService"; import { UsersService } from "../services/usersService"; @@ -74,6 +77,7 @@ enum InboundCommand { GetSummary = "getSummary", CancelQuery = "cancelQuery", SetContext = "setContext", + GetQueryPanelContext = "getQueryPanelContext", } interface RecInfo { @@ -165,10 +169,15 @@ export class QueryResultPanel extends AltimateWebviewProvider { context: WebviewViewResolveContext, _token: CancellationToken, ) { + const enableQueryPanelV2 = workspace + .getConfiguration("dbt") + .get("enableQueryPanelV2", false); this._panel = panel; this.bindWebviewOptions(context); this.renderWebviewView(panel.webview); - this.setupWebviewHooks(context); + // if (!enableQueryPanelV2) { + this.setupWebviewHooks(); + // } this.transmitConfig(); await this._panel?.webview.postMessage({ command: OutboundCommand.GetContext, @@ -180,6 +189,10 @@ export class QueryResultPanel extends AltimateWebviewProvider { }); } + async handleCommand(message: HandleCommandProps): Promise { + this.onMessage(message); + } + /** Sets options, note that retainContextWhen hidden is set on registration */ private bindWebviewOptions(context: WebviewViewResolveContext) { this._panel!.title = "Query Results"; @@ -187,75 +200,88 @@ export class QueryResultPanel extends AltimateWebviewProvider { this._panel!.webview.options = { enableScripts: true }; } + private async onMessage(message: any) { + switch (message.command) { + case InboundCommand.GetQueryPanelContext: + this.handleSyncRequestFromWebview( + message.syncRequestId, + () => { + return { + lastHintTimestamp: + this.dbtProjectContainer.getFromGlobalState( + "lastHintTimestamp", + ) || 0, + }; + }, + message.command, + ); + break; + case InboundCommand.CancelQuery: + if (this.queryExecution) { + this.queryExecution.cancel(); + } + break; + case InboundCommand.Error: + const error = message as RecError; + window.showErrorMessage(error.text); + break; + case InboundCommand.Info: + const info = message as RecInfo; + window.withProgress( + { + title: info.text, + location: ProgressLocation.Notification, + cancellable: false, + }, + async () => { + await new Promise((timer) => setTimeout(timer, 3000)); + }, + ); + break; + case InboundCommand.UpdateConfig: + const configMessage = message as RecConfig; + if (configMessage.limit !== undefined) { + workspace + .getConfiguration("dbt") + .update("queryLimit", configMessage.limit); + } + if (configMessage.scale) { + workspace + .getConfiguration("dbt") + .update("queryScale", configMessage.scale); + } + if ("enableNewQueryPanel" in configMessage) { + workspace + .getConfiguration("dbt") + .update("enableNewQueryPanel", configMessage.enableNewQueryPanel); + } + break; + case InboundCommand.OpenUrl: + const config = message as RecOpenUrl; + env.openExternal(Uri.parse(config.url)); + break; + case InboundCommand.GetSummary: + const summary = message as RecSummary; + this.eventEmitterService.fire({ + command: "dbtPowerUser.summarizeQuery", + payload: { + query: summary.compiledSql, + }, + }); + break; + case InboundCommand.SetContext: + this.dbtProjectContainer.setToGlobalState(message.key, message.value); + break; + default: + super.handleCommand(message); + } + } + /** Primary interface for WebviewView inbound communication */ - private setupWebviewHooks(context: WebviewViewResolveContext) { + private setupWebviewHooks() { this._panel!.webview.onDidReceiveMessage( - async (message) => { - switch (message.command) { - case InboundCommand.CancelQuery: - if (this.queryExecution) { - this.queryExecution.cancel(); - } - case InboundCommand.Error: - const error = message as RecError; - window.showErrorMessage(error.text); - break; - case InboundCommand.Info: - const info = message as RecInfo; - window.withProgress( - { - title: info.text, - location: ProgressLocation.Notification, - cancellable: false, - }, - async () => { - await new Promise((timer) => setTimeout(timer, 3000)); - }, - ); - break; - case InboundCommand.UpdateConfig: - const configMessage = message as RecConfig; - if (configMessage.limit !== undefined) { - workspace - .getConfiguration("dbt") - .update("queryLimit", configMessage.limit); - } - if (configMessage.scale) { - workspace - .getConfiguration("dbt") - .update("queryScale", configMessage.scale); - } - if ("enableNewQueryPanel" in configMessage) { - workspace - .getConfiguration("dbt") - .update( - "enableNewQueryPanel", - configMessage.enableNewQueryPanel, - ); - } - break; - case InboundCommand.OpenUrl: - const config = message as RecOpenUrl; - env.openExternal(Uri.parse(config.url)); - break; - case InboundCommand.GetSummary: - const summary = message as RecSummary; - this.eventEmitterService.fire({ - command: "dbtPowerUser.summarizeQuery", - payload: { - query: summary.compiledSql, - }, - }); - break; - case InboundCommand.SetContext: - this.dbtProjectContainer.setToGlobalState( - message.key, - message.value, - ); - break; - } - }, - null, + this.onMessage, + this, this._disposables, ); const sendQueryPanelViewEvent = () => { diff --git a/webview_panels/.storybook/preview-head.html b/webview_panels/.storybook/preview-head.html index 58282f7ae..bc5b08b0f 100644 --- a/webview_panels/.storybook/preview-head.html +++ b/webview_panels/.storybook/preview-head.html @@ -1,6 +1,6 @@