diff --git a/packages/vscode-extension/resources/styles/components/collapsible.css b/packages/vscode-extension/resources/styles/components/collapsible.css index 0fd516b35..7f101e658 100644 --- a/packages/vscode-extension/resources/styles/components/collapsible.css +++ b/packages/vscode-extension/resources/styles/components/collapsible.css @@ -27,6 +27,12 @@ align-items: center; } +.collapsible-title:focus-visible { + outline-width: 2px; + outline-offset: 2px; + outline-color: rgb(var(--theme-palette-primary-focus)); +} + .collapsible-title-text { padding-left: 6px; overflow: hidden; diff --git a/packages/vscode-extension/resources/styles/components/vizWrapper.css b/packages/vscode-extension/resources/styles/components/vizWrapper.css index 4e053eceb..c088840fd 100644 --- a/packages/vscode-extension/resources/styles/components/vizWrapper.css +++ b/packages/vscode-extension/resources/styles/components/vizWrapper.css @@ -6,7 +6,9 @@ height: calc(100vh - 20px); width: calc(100vw - 20px); min-height: 100px; - overflow: scroll; + overflow: hidden; + display: flex; + flex-direction: column; } .vizWrapper-controls { @@ -18,7 +20,7 @@ .vizWrapper-controls.block { position: initial; - padding-bottom: 4px; + padding: 2px; } .vizWrapper-graph { @@ -28,24 +30,52 @@ height: 100%; } +.vizWrapper-table { + overflow-y: scroll; + flex: 1; +} + .vizWrapper table { border-collapse: collapse; - overflow: scroll; - background-color: rgb(var(--theme-palette-neutral-bg-weak)); + overflow-y: scroll; + flex: 1; + width: 100%; +} + +.vizWrapper thead { + position: sticky; + top: 0; + background-color: rgb(var(--theme-palette-neutral-bg-default)); } -.vizWrapper th, -.vizWrapper td { +.vizWrapper th:not(:first-child), +.vizWrapper td:not(:first-child) { padding: 10px; - border: 1px solid rgb(var(--theme-palette-neutral-border-weak)); + border-bottom: 1px solid rgb(var(--theme-palette-neutral-border-weak)); text-align: left; } .vizWrapper th { font-weight: bold; - text-align: center; +} + +.vizWrapper td:first-child, +.vizWrapper th:first-child { + color: rgb(var(--theme-palette-neutral-text-weakest)); + font-size: 8px; + padding-left: 4px; + border-bottom: 1px solid rgb(var(--theme-palette-neutral-border-weak)); + width: 6px; } .vizWrapper tr:hover { background-color: rgb(var(--theme-palette-neutral-bg-on-bg-weak)); } + +.vizWrapper-table-cell { + word-break: break-all; + overflow-wrap: break-word; + white-space: pre-wrap; + max-width: 100%; + font-size: var(--vscode-editor-font-size); +} diff --git a/packages/vscode-extension/src/components/collapsible.tsx b/packages/vscode-extension/src/components/collapsible.tsx index 111eae24f..80cd09113 100644 --- a/packages/vscode-extension/src/components/collapsible.tsx +++ b/packages/vscode-extension/src/components/collapsible.tsx @@ -2,7 +2,6 @@ import { IconButton } from '@neo4j-ndl/react'; import { ChevronDownIconOutline, ChevronRightIconOutline, - ExploreIcon, } from '@neo4j-ndl/react/icons'; import React from 'react'; @@ -43,25 +42,20 @@ export const Collapsible: React.FC = ({ )} - { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + onToggle(); + } + }} >

{title}

- - - -
+ {expanded &&
{children}
} diff --git a/packages/vscode-extension/src/components/viz-wrapper.tsx b/packages/vscode-extension/src/components/viz-wrapper.tsx index 837176f95..520b1bf5a 100644 --- a/packages/vscode-extension/src/components/viz-wrapper.tsx +++ b/packages/vscode-extension/src/components/viz-wrapper.tsx @@ -14,17 +14,18 @@ type VizWrapperProps = { }; // eslint-disable-next-line @typescript-eslint/no-explicit-any -function renderRow(keys: any[], row: Record) { +function renderRow(keys: any[], row: Record, index: number) { return ( + {index} {keys.map((key, i) => ( -
+          
{ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access JSON.stringify(row[key], null, 2) } -
+ ))} @@ -40,12 +41,15 @@ function renderTable(rows: ResultRows) { + {Object.keys(rows[0]).map((key) => ( ))} - {rows.map((row) => renderRow(Object.keys(row), row))} + + {rows.map((row, i) => renderRow(Object.keys(row), row, i + 1))} +
{key.toString()}
); } @@ -82,7 +86,7 @@ export const VizWrapper: React.FC = ({ )} {selectedView === 'table' ? ( - renderTable(rows) +
{renderTable(rows)}
) : (
diff --git a/packages/vscode-extension/src/registrationService.ts b/packages/vscode-extension/src/registrationService.ts index f425e6b55..98b1037ec 100644 --- a/packages/vscode-extension/src/registrationService.ts +++ b/packages/vscode-extension/src/registrationService.ts @@ -12,6 +12,7 @@ import { switchToDatabaseWithName, toggleConnectionItemsConnectionState, } from './commandHandlers/connection'; +import { views } from './webviews/queryResults/queryResultsTypes'; import { addParameter, clearAllParameters, @@ -38,6 +39,7 @@ import { Neo4jQueryVisualizationProvider } from './webviews/queryResults/queryVi export function registerDisposables(): Disposable[] { const disposables = Array(); const queryDetailsProvider = new Neo4jQueryDetailsProvider(); + const queryVisualizationProvider = new Neo4jQueryVisualizationProvider(); disposables.push( window.registerWebviewViewProvider( @@ -47,7 +49,7 @@ export function registerDisposables(): Disposable[] { ), window.registerWebviewViewProvider( 'neo4jQueryVisualization', - new Neo4jQueryVisualizationProvider(), + queryVisualizationProvider, { webviewOptions: { retainContextWhenHidden: true } }, ), window.registerTreeDataProvider( @@ -98,6 +100,11 @@ export function registerDisposables(): Disposable[] { ), commands.registerCommand(CONSTANTS.COMMANDS.RUN_CYPHER, () => runCypher(async (statements: string[]) => { + views.detailsView ?? + (await commands.executeCommand('neo4jQueryDetails.focus')); + views.visualizationView ?? + (await commands.executeCommand('neo4jQueryVisualization.focus')); + await queryVisualizationProvider.viewReadyPromise; await queryDetailsProvider.executeStatements(statements); }), ), diff --git a/packages/vscode-extension/src/webviews/controllers/queryDetails.tsx b/packages/vscode-extension/src/webviews/controllers/queryDetails.tsx index b178c7ae4..b789cc7c8 100644 --- a/packages/vscode-extension/src/webviews/controllers/queryDetails.tsx +++ b/packages/vscode-extension/src/webviews/controllers/queryDetails.tsx @@ -61,6 +61,11 @@ export function QueryDetails() { } return newState; }); + } else if (message.type === 'themeUpdate') { + document.documentElement.classList.toggle( + 'ndl-theme-dark', + message.isDarkTheme, + ); } }; diff --git a/packages/vscode-extension/src/webviews/controllers/queryVisualization.tsx b/packages/vscode-extension/src/webviews/controllers/queryVisualization.tsx index 941fdc9ae..6f971a5f4 100644 --- a/packages/vscode-extension/src/webviews/controllers/queryVisualization.tsx +++ b/packages/vscode-extension/src/webviews/controllers/queryVisualization.tsx @@ -49,6 +49,11 @@ export function QueryVisualization() { // passing the message to parent to update the title vscode.postMessage(message); setStatementResult(message.result); + } else if (message.type === 'themeUpdate') { + document.documentElement.classList.toggle( + 'ndl-theme-dark', + message.isDarkTheme, + ); } }; diff --git a/packages/vscode-extension/src/webviews/queryResults/queryDetailsProvider.ts b/packages/vscode-extension/src/webviews/queryResults/queryDetailsProvider.ts index 9670f2c99..471fff8dd 100644 --- a/packages/vscode-extension/src/webviews/queryResults/queryDetailsProvider.ts +++ b/packages/vscode-extension/src/webviews/queryResults/queryDetailsProvider.ts @@ -18,7 +18,6 @@ import { import path from 'path'; import { ColorThemeKind, - commands, Uri, WebviewView, WebviewViewProvider, @@ -60,10 +59,19 @@ export class Neo4jQueryDetailsProvider implements WebviewViewProvider { void views.visualizationView.webview.postMessage(msg); } }); + + window.onDidChangeActiveColorTheme(async (e) => { + await this.view.webview.postMessage({ + type: 'themeUpdate', + isDarkTheme: + e.kind === ColorThemeKind.Dark || + e.kind === ColorThemeKind.HighContrast, + to: 'detailsView', + }); + }); } async executeStatements(statements: string[]) { - await commands.executeCommand('neo4jQueryDetails.focus'); this.view ?? (await this.viewReadyPromise); const webview = this.view.webview; diff --git a/packages/vscode-extension/src/webviews/queryResults/queryResultsTypes.ts b/packages/vscode-extension/src/webviews/queryResults/queryResultsTypes.ts index 23927bff9..aac85f941 100644 --- a/packages/vscode-extension/src/webviews/queryResults/queryResultsTypes.ts +++ b/packages/vscode-extension/src/webviews/queryResults/queryResultsTypes.ts @@ -39,4 +39,9 @@ export type QueryResultsMessage = type: 'executionUpdate'; result: QueryResult; to: QueryResultViews; + } + | { + type: 'themeUpdate'; + isDarkTheme: boolean; + to: QueryResultViews; }; diff --git a/packages/vscode-extension/src/webviews/queryResults/queryVisualizationProvider.ts b/packages/vscode-extension/src/webviews/queryResults/queryVisualizationProvider.ts index 4d77e667c..31252c33f 100644 --- a/packages/vscode-extension/src/webviews/queryResults/queryVisualizationProvider.ts +++ b/packages/vscode-extension/src/webviews/queryResults/queryVisualizationProvider.ts @@ -12,10 +12,19 @@ import { QueryResultsMessage, views } from './queryResultsTypes'; export class Neo4jQueryVisualizationProvider implements WebviewViewProvider { private view: WebviewView | undefined; + private viewReadyResolver!: (view: WebviewView) => void; + public viewReadyPromise: Promise; + + constructor() { + this.viewReadyPromise = new Promise((resolve) => { + this.viewReadyResolver = resolve; + }); + } resolveWebviewView(webviewView: WebviewView) { this.view = webviewView; views.visualizationView = webviewView; + this.viewReadyResolver(webviewView); webviewView.webview.options = { enableScripts: true, @@ -30,6 +39,16 @@ export class Neo4jQueryVisualizationProvider implements WebviewViewProvider { : `Visualization`; } }); + + window.onDidChangeActiveColorTheme(async (e) => { + await this.view.webview.postMessage({ + type: 'themeUpdate', + isDarkTheme: + e.kind === ColorThemeKind.Dark || + e.kind === ColorThemeKind.HighContrast, + to: 'visualizationView', + }); + }); } renderQueryVisualization() {