diff --git a/package.json b/package.json index 81e13222d..996360d8a 100644 --- a/package.json +++ b/package.json @@ -102,10 +102,10 @@ "description": "Enable AltimateAI teammates feature.", "default": false }, - "dbt.enableQueryBookmarks": { + "dbt.disableQueryHistory": { "type": "boolean", - "description": "Enable Query history and bookmarks feature", - "default": true + "description": "Disable Query history and bookmarks", + "default": false }, "dbt.enableNotebooks": { "type": "boolean", @@ -701,8 +701,7 @@ "menus": { "file/newFile": [ { - "command": "dbtPowerUser.createSqlFile", - "when": "dbt.enableQueryBookmarks == true" + "command": "dbtPowerUser.createSqlFile" }, { "command": "dbtPowerUser.createDatapilotNotebook", diff --git a/src/telemetry/events.ts b/src/telemetry/events.ts index 8477d72eb..241449ed1 100644 --- a/src/telemetry/events.ts +++ b/src/telemetry/events.ts @@ -56,4 +56,6 @@ export enum TelemetryEvents { "Notebook/Launch" = "Notebook/Launch", "Notebook/LaunchError" = "Notebook/LaunchError", "Notebook/StoreDataInKernelError" = "Notebook/StoreDataInKernelError", + "QueryHistory/Disabled" = "QueryHistory/Disabled", + "QueryHistory/Cleared" = "QueryHistory/Cleared", } diff --git a/src/utils.ts b/src/utils.ts index 324d3d78b..b6a0633e7 100755 --- a/src/utils.ts +++ b/src/utils.ts @@ -341,3 +341,21 @@ export function getFormattedDateTime(): string { return `${date}-${time}`; } + +export const getStringSizeInMb = (str: string): number => { + let sizeInBytes = 0; + for (let i = 0; i < str.length; i++) { + const charCode = str.charCodeAt(i); + if (charCode <= 0x7f) { + sizeInBytes += 1; + } else if (charCode <= 0x7ff) { + sizeInBytes += 2; + } else if (charCode <= 0xffff) { + sizeInBytes += 3; + } else { + sizeInBytes += 4; + } + } + const sizeInMB = sizeInBytes / (1024 * 1024); + return sizeInMB; +}; diff --git a/src/webview_provider/queryResultPanel.ts b/src/webview_provider/queryResultPanel.ts index c46ddcef5..44c055f76 100644 --- a/src/webview_provider/queryResultPanel.ts +++ b/src/webview_provider/queryResultPanel.ts @@ -14,12 +14,12 @@ import { workspace, } from "vscode"; -import { readFileSync } from "fs"; import { PythonException } from "python-bridge"; import { DBTProjectContainer } from "../manifest/dbtProjectContainer"; import { extendErrorWithSupportLinks, getFormattedDateTime, + getStringSizeInMb, provideSingleton, } from "../utils"; import { TelemetryService } from "../telemetry"; @@ -38,6 +38,7 @@ import { import { DBTTerminal } from "../dbt_client/dbtTerminal"; import { QueryManifestService } from "../services/queryManifestService"; import { UsersService } from "../services/usersService"; +import { TelemetryEvents } from "../telemetry/events"; interface JsonObj { [key: string]: string | number | undefined; @@ -94,6 +95,7 @@ enum InboundCommand { RunAdhocQuery = "runAdhocQuery", ViewResultSet = "viewResultSet", OpenCodeInEditor = "openCodeInEditor", + ClearQueryHistory = "clearQueryHistory", } interface RecInfo { @@ -124,7 +126,7 @@ interface QueryHistory { duration: number; adapter: string; projectName: string; - data: JsonObj[]; + data?: JsonObj[]; columnNames: string[]; columnTypes: string[]; modelName: string; @@ -165,13 +167,11 @@ export class QueryResultPanel extends AltimateWebviewProvider { this._disposables.push( workspace.onDidChangeConfiguration( (e) => { - if (e.affectsConfiguration("dbt.enableQueryBookmarks")) { - this.updateEnableBookmarksInContext(); + if (e.affectsConfiguration("dbt.disableQueryHistory")) { if (this._panel) { this.renderWebviewView(this._panel.webview); } } - if (e.affectsConfiguration("dbt.enableNotebooks")) { this.updateEnableNotebooksInContext(); const event = workspace @@ -187,7 +187,6 @@ export class QueryResultPanel extends AltimateWebviewProvider { ), ); - this.updateEnableBookmarksInContext(); this.updateEnableNotebooksInContext(); this._disposables.push( commands.registerCommand( @@ -205,17 +204,6 @@ export class QueryResultPanel extends AltimateWebviewProvider { }); } - private updateEnableBookmarksInContext() { - // Setting this here to access it in package.json for enabling new file command - commands.executeCommand( - "setContext", - "dbt.enableQueryBookmarks", - workspace - .getConfiguration("dbt") - .get("enableQueryBookmarks", false), - ); - } - private updateEnableNotebooksInContext() { // Setting this here to access it in package.json for enabling new file command commands.executeCommand( @@ -378,6 +366,20 @@ export class QueryResultPanel extends AltimateWebviewProvider { this._panel!.webview.onDidReceiveMessage( async (message) => { switch (message.command) { + // Incase of error in rendering perspective viewer query results, user can click button + // to disable query history and retry + case InboundCommand.ClearQueryHistory: + this.telemetry.sendTelemetryError( + TelemetryEvents["QueryHistory/Cleared"], + message.error, + ); + this._queryHistory = []; + this.sendResponseToWebview({ + command: "response", + data: {}, + syncRequestId: message.syncRequestId, + }); + break; case InboundCommand.OpenCodeInEditor: this.handleOpenCodeInEditor(message); break; @@ -430,22 +432,24 @@ export class QueryResultPanel extends AltimateWebviewProvider { } break; case InboundCommand.GetQueryPanelContext: - const perspectiveTheme = workspace - .getConfiguration("dbt") - .get("perspectiveTheme", "Vintage"); - const queryBookmarksEnabled = workspace - .getConfiguration("dbt") - .get("enableQueryBookmarks", false); + { + const perspectiveTheme = workspace + .getConfiguration("dbt") + .get("perspectiveTheme", "Vintage"); + const queryHistoryDisabled = workspace + .getConfiguration("dbt") + .get("disableQueryHistory", false); - const limit = workspace - .getConfiguration("dbt") - .get("queryLimit"); - await this._panel!.webview.postMessage({ - command: OutboundCommand.GetContext, - limit, - perspectiveTheme, - queryBookmarksEnabled, - }); + const limit = workspace + .getConfiguration("dbt") + .get("queryLimit"); + await this._panel!.webview.postMessage({ + command: OutboundCommand.GetContext, + limit, + perspectiveTheme, + queryHistoryDisabled, + }); + } break; case InboundCommand.CancelQuery: if (this.queryExecution) { @@ -665,8 +669,8 @@ export class QueryResultPanel extends AltimateWebviewProvider { duration: number, modelName: string, ) { - // Do not update query history if bookmarks are disabled - if (!workspace.getConfiguration("dbt").get("enableQueryBookmarks", false)) { + // Do not update query history if disabled + if (workspace.getConfiguration("dbt").get("disableQueryHistory", false)) { return; } const project = projectName @@ -679,6 +683,17 @@ export class QueryResultPanel extends AltimateWebviewProvider { ); return; } + const queryHistoryCurrentSize = getStringSizeInMb( + JSON.stringify(this._queryHistory), + ); + // if current history size > 3MB, remove the oldest entry + if (queryHistoryCurrentSize > 3) { + this._queryHistory.pop(); + this.dbtTerminal.info( + "updateQueryHistory", + "Query history size exceeded 3MB, cleared oldest entry", + ); + } this._queryHistory.unshift({ rawSql: query, compiledSql: result.compiled_sql, @@ -692,13 +707,6 @@ export class QueryResultPanel extends AltimateWebviewProvider { modelName, }); this._queryHistory = this._queryHistory.splice(0, 10); - this._bottomPanel?.webview.postMessage({ - command: "queryHistory", - args: { - body: this._queryHistory, - status: true, - }, - }); } /** Runs a query transmitting appropriate notifications to webview */ diff --git a/webview_panels/.eslintrc.cjs b/webview_panels/.eslintrc.cjs index 4d58743f6..687444176 100644 --- a/webview_panels/.eslintrc.cjs +++ b/webview_panels/.eslintrc.cjs @@ -15,7 +15,7 @@ module.exports = { "plugin:you-dont-need-lodash-underscore/compatible", "plugin:storybook/recommended", ], - ignorePatterns: ["dist", ".eslintrc.cjs"], + ignorePatterns: ["dist", ".eslintrc.cjs", "src/lib/altimate/*"], parser: "@typescript-eslint/parser", settings: { react: { @@ -107,12 +107,6 @@ module.exports = { }, ], }, - }, - { - "files": ["src/lib/altimate/altimate-components.d.ts"], - "rules": { - "@typescript-eslint/naming-convention": "off" - } - } + } ], }; diff --git a/webview_panels/src/assets/icons/error.svg b/webview_panels/src/assets/icons/error.svg new file mode 100644 index 000000000..f67a2c761 --- /dev/null +++ b/webview_panels/src/assets/icons/error.svg @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/webview_panels/src/assets/icons/index.tsx b/webview_panels/src/assets/icons/index.tsx index c8d8a7ae6..99f5a42d6 100644 --- a/webview_panels/src/assets/icons/index.tsx +++ b/webview_panels/src/assets/icons/index.tsx @@ -26,6 +26,7 @@ export { default as NoHistoryIcon } from "./no-history.svg?react"; export { default as NoNotebooksIcon } from "./notebook.svg?react"; export { default as ThinkingIcon } from "./thinking.svg?react"; export { default as CoachAIIcon } from "./coachAi.svg?react"; +export { default as ErrorIcon } from "./error.svg?react"; import LoadingSpinnerUrl from "./spinner.gif"; import "./styles.css"; diff --git a/webview_panels/src/modules/queryPanel/QueryPanel.stories.tsx b/webview_panels/src/modules/queryPanel/QueryPanel.stories.tsx index caf45c635..36379acdc 100644 --- a/webview_panels/src/modules/queryPanel/QueryPanel.stories.tsx +++ b/webview_panels/src/modules/queryPanel/QueryPanel.stories.tsx @@ -89,7 +89,7 @@ export const DefaultQueryPanelView = { if (request.command === "getQueryPanelContext") { window.postMessage({ command: "getContext", - queryBookmarksEnabled: true, + queryHistoryDisabled: false, }); window.postMessage({ diff --git a/webview_panels/src/modules/queryPanel/components/QueryPanelContents/QueryPanelContent.tsx b/webview_panels/src/modules/queryPanel/components/QueryPanelContents/QueryPanelContent.tsx index 1e7d639d0..e2a5b2e3c 100644 --- a/webview_panels/src/modules/queryPanel/components/QueryPanelContents/QueryPanelContent.tsx +++ b/webview_panels/src/modules/queryPanel/components/QueryPanelContents/QueryPanelContent.tsx @@ -8,6 +8,7 @@ import PreTag from "@modules/markdown/PreTag"; import { QueryPanelTitleTabState } from "./types"; import QueryPanelHistory from "../queryPanelQueryHistory/QueryPanelHistory"; import QueryPanelBookmarks from "../queryPanelBookmarks/QueryPanelBookmarks"; +import PerspectiveErrorBoundary from "../perspective/PerspectiveErrorBoundary"; const QueryPanelContent = ({ tabState, @@ -41,11 +42,13 @@ const QueryPanelContent = ({ if (queryResults) { return ( + + ); } diff --git a/webview_panels/src/modules/queryPanel/components/QueryPanelContents/QueryPanelTitle.tsx b/webview_panels/src/modules/queryPanel/components/QueryPanelContents/QueryPanelTitle.tsx index 3f5c842f9..ed56c6921 100644 --- a/webview_panels/src/modules/queryPanel/components/QueryPanelContents/QueryPanelTitle.tsx +++ b/webview_panels/src/modules/queryPanel/components/QueryPanelContents/QueryPanelTitle.tsx @@ -19,7 +19,7 @@ const QueryPanelTitle = ({ queryExecutionInfo, compiledCodeMarkup, queryResultsRowCount, - queryBookmarksEnabled, + queryHistoryDisabled, viewType, } = useQueryPanelState(); @@ -29,7 +29,7 @@ const QueryPanelTitle = ({ const commonTabs = useMemo( () => - queryBookmarksEnabled && viewType === QueryPanelViewType.DEFAULT ? ( + !queryHistoryDisabled && viewType === QueryPanelViewType.DEFAULT ? ( <> @@ -57,7 +57,7 @@ const QueryPanelTitle = ({ ) : null, - [queryBookmarksEnabled, tabState, viewType], + [queryHistoryDisabled, tabState, viewType], ); if (loading || hasData || hasError) { diff --git a/webview_panels/src/modules/queryPanel/components/perspective/PerspectiveErrorBoundary.tsx b/webview_panels/src/modules/queryPanel/components/perspective/PerspectiveErrorBoundary.tsx new file mode 100644 index 000000000..8af57de0d --- /dev/null +++ b/webview_panels/src/modules/queryPanel/components/perspective/PerspectiveErrorBoundary.tsx @@ -0,0 +1,65 @@ +import { ErrorIcon } from "@assets/icons"; +import { executeRequestInSync } from "@modules/app/requestExecutor"; +import { setLoading } from "@modules/queryPanel/context/queryPanelSlice"; +import { useQueryPanelDispatch } from "@modules/queryPanel/QueryPanelProvider"; +import { Stack, Button } from "@uicore"; +import { ReactNode, useState } from "react"; +import { ErrorBoundary } from "react-error-boundary"; + +const PerspectiveErrorBoundary = ({ + children, +}: { + children: ReactNode; +}): JSX.Element => { + const [error, setError] = useState(); + const dispatch = useQueryPanelDispatch(); + + const handleClick = async () => { + dispatch(setLoading(true)); + try { + await executeRequestInSync("clearQueryHistory", { error }); + // Artificial delay for UX + await new Promise((resolve) => setTimeout(resolve, 1000)); + } finally { + dispatch(setLoading(false)); + } + }; + + const handleOnError = (errorObject: Error) => { + setError({ + message: errorObject.message, + stack: errorObject.stack, + name: errorObject.name, + }); + }; + + const fallbackRenderer = () => { + return ( + + +

+ Something went wrong while rendering the query results. +

+

To clear query history and re render the results

+ +

+ Please contact us{" "} + if the issue persists. +

+
+ ); + }; + + return ( + + {children} + + ); +}; + +export default PerspectiveErrorBoundary; 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 e91715531..f49a43e7e 100644 --- a/webview_panels/src/modules/queryPanel/components/perspective/PerspectiveViewer.stories.tsx +++ b/webview_panels/src/modules/queryPanel/components/perspective/PerspectiveViewer.stories.tsx @@ -1,6 +1,7 @@ import type { Meta } from "@storybook/react"; import PerspectiveViewer from "./PerspectiveViewer"; import { TableData } from "@finos/perspective"; +import PerspectiveErrorBoundary from "./PerspectiveErrorBoundary"; const meta = { title: "PerspectiveViewer", @@ -18,6 +19,8 @@ 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 index 07c024420..e22d4a5fb 100644 --- a/webview_panels/src/modules/queryPanel/components/perspective/PerspectiveViewer.tsx +++ b/webview_panels/src/modules/queryPanel/components/perspective/PerspectiveViewer.tsx @@ -24,6 +24,7 @@ import useQueryPanelState from "@modules/queryPanel/useQueryPanelState"; import { useQueryPanelDispatch } from "@modules/queryPanel/QueryPanelProvider"; import { setPerspectiveTheme } from "@modules/queryPanel/context/queryPanelSlice"; import { Drawer, DrawerRef } from "@uicore"; +import { useErrorBoundary } from "react-error-boundary"; interface Props { data: TableData; @@ -40,6 +41,9 @@ const PerspectiveViewer = ({ const { state: { theme }, } = useAppContext(); + + const { showBoundary } = useErrorBoundary(); + const { perspectiveTheme } = useQueryPanelState(); const dispatch = useQueryPanelDispatch(); const [tableRendered, setTableRendered] = useState(false); @@ -150,7 +154,7 @@ const PerspectiveViewer = ({ return; } - const styles = { + const dataFormats = { types: { integer: { format: { @@ -172,46 +176,57 @@ const PerspectiveViewer = ({ schema[columnNames[i]] = mapType(columnTypes[i]); } - // @ts-expect-error valid parameter - const worker = perspective.worker(styles); - const table = await worker.table(schema); - await table.replace(data); + try { + // @ts-expect-error valid parameter + const worker = perspective.worker(dataFormats); + const table = await worker.table(schema); + await table.replace(data); - await perspectiveViewerRef.current.load(table); - await perspectiveViewerRef.current.resetThemes([ - "Vintage", - "Pro Light", - "Pro Dark", - "Vaporwave", - "Solarized", - "Solarized Dark", - "Monokai", - ]); - await perspectiveViewerRef.current.restore(config); - const datagridShadowRoot = perspectiveViewerRef.current?.shadowRoot; - if (datagridShadowRoot) { - const exportButton = datagridShadowRoot.getElementById("export"); - if (!exportButton) { - return; + await perspectiveViewerRef.current.load(table); + await perspectiveViewerRef.current.resetThemes([ + "Vintage", + "Pro Light", + "Pro Dark", + "Vaporwave", + "Solarized", + "Solarized Dark", + "Monokai", + ]); + await perspectiveViewerRef.current.restore(config); + const datagridShadowRoot = perspectiveViewerRef.current?.shadowRoot; + if (datagridShadowRoot) { + const exportButton = datagridShadowRoot.getElementById("export"); + if (!exportButton) { + return; + } + exportButton.removeEventListener("click", downloadAsCSV); + exportButton.addEventListener("click", downloadAsCSV); + } + updateCustomStyles(perspectiveTheme); + perspectiveViewerRef.current.addEventListener( + "perspective-config-update", + (event) => { + const ev = event as CustomEvent; + panelLogger.log("perspective-config-update", ev.detail); + if (ev.detail.theme) { + updateCustomStyles(ev.detail.theme); + executeRequestInAsync("updateConfig", { + perspectiveTheme: ev.detail.theme, + }); + dispatch(setPerspectiveTheme(ev.detail.theme)); + } + }, + ); + } catch (err) { + panelLogger.error("error while loading perspective data", err); + // catching this error: Uncaught (in promise) RangeError: WebAssembly.instantiate(): Out of memory: Cannot allocate Wasm memory for new instance + const isWasmError = (err as Error)?.message?.includes( + "WebAssembly.instantiate", + ); + if (isWasmError) { + showBoundary(err); } - exportButton.removeEventListener("click", downloadAsCSV); - exportButton.addEventListener("click", downloadAsCSV); } - updateCustomStyles(perspectiveTheme); - perspectiveViewerRef.current.addEventListener( - "perspective-config-update", - (event) => { - const ev = event as CustomEvent; - panelLogger.log("perspective-config-update", ev.detail); - if (ev.detail.theme) { - updateCustomStyles(ev.detail.theme); - executeRequestInAsync("updateConfig", { - perspectiveTheme: ev.detail.theme, - }); - dispatch(setPerspectiveTheme(ev.detail.theme)); - } - }, - ); setTableRendered(true); }; diff --git a/webview_panels/src/modules/queryPanel/components/queryPanelQueryHistory/QueryPanelHistory.tsx b/webview_panels/src/modules/queryPanel/components/queryPanelQueryHistory/QueryPanelHistory.tsx index 0ae1a31b1..2cbd0fed4 100644 --- a/webview_panels/src/modules/queryPanel/components/queryPanelQueryHistory/QueryPanelHistory.tsx +++ b/webview_panels/src/modules/queryPanel/components/queryPanelQueryHistory/QueryPanelHistory.tsx @@ -24,6 +24,10 @@ const QueryPanelHistory = (): JSX.Element => { const { queryHistory, queryBookmarksTagsFromDB } = useQueryPanelState(); const { refetchBookmarkTags } = useQueryPanelCommonActions(); + useEffect(() => { + void executeRequestInAsync("getQueryHistory", {}); + }, []); + useEffect(() => { if (queryBookmarksTagsFromDB) { return; @@ -123,11 +127,13 @@ const QueryPanelHistory = (): JSX.Element => {
- - - + {activeHistory.data ? ( + + + + ) : null} {activeHistory.adapter} diff --git a/webview_panels/src/modules/queryPanel/context/queryPanelSlice.ts b/webview_panels/src/modules/queryPanel/context/queryPanelSlice.ts index 5201555f1..531323536 100644 --- a/webview_panels/src/modules/queryPanel/context/queryPanelSlice.ts +++ b/webview_panels/src/modules/queryPanel/context/queryPanelSlice.ts @@ -18,7 +18,7 @@ export const initialState = { perspectiveTheme: "Vintage", queryHistory: [], queryBookmarks: {}, - queryBookmarksEnabled: false, + queryHistoryDisabled: false, tabState: QueryPanelTitleTabState.Preview, queryBookmarksTagsFromDB: undefined, } as QueryPanelStateProps; @@ -61,11 +61,11 @@ const queryPanelSlice = createSlice({ ) => { state.tabState = action.payload; }, - setQueryBookmarksEnabled: ( + setQueryHistoryDisabled: ( state, - action: PayloadAction, + action: PayloadAction, ) => { - state.queryBookmarksEnabled = action.payload; + state.queryHistoryDisabled = action.payload; }, setQueryHistory: ( state, @@ -138,7 +138,7 @@ export const { setPerspectiveTheme, setQueryHistory, setQueryBookmarks, - setQueryBookmarksEnabled, + setQueryHistoryDisabled, setTabState, setQueryBookmarksTagsFromDB, } = queryPanelSlice.actions; diff --git a/webview_panels/src/modules/queryPanel/context/types.ts b/webview_panels/src/modules/queryPanel/context/types.ts index 00db16edb..ffa3db68a 100644 --- a/webview_panels/src/modules/queryPanel/context/types.ts +++ b/webview_panels/src/modules/queryPanel/context/types.ts @@ -10,6 +10,7 @@ export interface QueryHistory { adapter: string; projectName: string; modelName: string; + data?: TableData; } export interface QueryBookmarkResponse { @@ -64,7 +65,7 @@ export interface QueryPanelStateProps { private?: QueryBookmarkResponse; public?: QueryBookmarkResponse; }; - queryBookmarksEnabled: boolean; + queryHistoryDisabled: boolean; queryBookmarksTagsFromDB?: { id: number; tag: string }[]; tabState: QueryPanelTitleTabState; } diff --git a/webview_panels/src/modules/queryPanel/useQueryPanelListeners.ts b/webview_panels/src/modules/queryPanel/useQueryPanelListeners.ts index d67a56d45..41fa233d6 100644 --- a/webview_panels/src/modules/queryPanel/useQueryPanelListeners.ts +++ b/webview_panels/src/modules/queryPanel/useQueryPanelListeners.ts @@ -8,7 +8,7 @@ import { setLimit, setLoading, setPerspectiveTheme, - setQueryBookmarksEnabled, + setQueryHistoryDisabled, setQueryExecutionInfo, setQueryHistory, setQueryResults, @@ -160,7 +160,7 @@ const useQueryPanelListeners = (): { loading: boolean } => { dispatch(setPerspectiveTheme(args.perspectiveTheme as string)); dispatch( // @ts-expect-error valid type - setQueryBookmarksEnabled(args.queryBookmarksEnabled as boolean) + setQueryHistoryDisabled(args.queryHistoryDisabled as boolean) ); break; case "collectQueryResultsDebugInfo": @@ -175,8 +175,6 @@ const useQueryPanelListeners = (): { loading: boolean } => { useEffect(() => { void executeRequestInSync("getQueryPanelContext", {}); - - void executeRequestInSync("getQueryHistory", {}); }, []); useEffect(() => {