diff --git a/images/dark/search-icon.svg b/images/dark/search-icon.svg
new file mode 100644
index 00000000..5f31154b
--- /dev/null
+++ b/images/dark/search-icon.svg
@@ -0,0 +1 @@
+
diff --git a/images/light/search-icon.svg b/images/light/search-icon.svg
new file mode 100644
index 00000000..8a7fbf3f
--- /dev/null
+++ b/images/light/search-icon.svg
@@ -0,0 +1,8 @@
+
\ No newline at end of file
diff --git a/package.json b/package.json
index b149c708..3039ec6a 100644
--- a/package.json
+++ b/package.json
@@ -357,6 +357,18 @@
"SQL++"
],
"configuration": "./language/language-configuration.json"
+ },
+
+ {
+ "id":"searchQuery",
+ "extensions": [
+ ".cbs.json"
+ ],
+ "aliases": [
+ "SEARCH"
+ ],
+ "configuration": "./language/language-configuration.json"
+
}
],
"grammars": [
@@ -434,6 +446,12 @@
"category": "Couchbase",
"icon": "images/create.svg"
},
+ {
+ "command": "vscode-couchbase.openSearchWorkbench",
+ "title": "New Search Workbench",
+ "category": "Couchbase",
+ "icon": "images/create.svg"
+ },
{
"command": "vscode-couchbase.openQueryWorkbench",
"title": "New Workbench",
@@ -546,6 +564,14 @@
"title": "Show Output Console",
"category": "Couchbase"
},
+ {
+ "command": "vscode-couchbase.runSearch",
+ "title": "Run Search",
+ "category": "Couchbase",
+ "icon": "$(play)",
+ "tooltip": "Run Search",
+ "enablement": "vscode-couchbase.runSearchButtonEnabled"
+ },
{
"command": "vscode-couchbase.runQuery",
"title": "Run Query",
@@ -569,6 +595,12 @@
"title": "MongoDB Migrate",
"category": "Couchbase"
},
+ {
+ "title": "Search Context",
+ "command": "vscode-couchbase.searchContext",
+ "category": "Couchbase",
+ "icon": "$(group-by-ref-type)"
+ },
{
"command": "vscode-couchbase.tools.dataImport",
"title": "Data Import",
@@ -695,6 +727,12 @@
"key": "ctrl+shift+e",
"mac": "cmd+shift+e",
"when": "(editorLangId == sqlpp || resourceFilename =~ /.sqlpp$/) && !isKVCluster"
+ },
+ {
+ "command": "vscode-couchbase.runSearch",
+ "key": "ctrl+shift+e",
+ "mac": "cmd+shift+e",
+ "when": "(editorLangId == cbs.json || resourceFilename =~ /.cbs.json$/) && !isKVCluster"
}
],
"menus": {
@@ -705,6 +743,12 @@
"when": "(editorLangId == sqlpp || resourceFilename =~ /.sqlpp$/) && !isKVCluster",
"group": "navigation@1"
},
+ {
+ "command": "vscode-couchbase.runSearch",
+ "category": "Couchbase",
+ "when": "(editorLangId == cbs.json || resourceFilename =~ /.cbs.json$/) && !isKVCluster",
+ "group": "navigation@1"
+ },
{
"title": "Query Context",
"command": "vscode-couchbase.queryContext",
@@ -712,6 +756,13 @@
"group": "navigation@2",
"when": "(editorLangId == sqlpp || resourceFilename =~ /.sqlpp$/) && !isKVCluster"
},
+ {
+ "title": "Search Context",
+ "command": "vscode-couchbase.searchContext",
+ "category": "Couchbase",
+ "group": "navigation@2",
+ "when": "(editorLangId == cbs.json || resourceFilename =~ /.cbs.json$/) && !isKVCluster"
+ },
{
"title": "Show Favorite Queries",
"command": "vscode-couchbase.showFavoriteQueries",
@@ -760,6 +811,11 @@
"command": "vscode-couchbase.runQuery",
"when": "(editorLangId == sqlpp || resourceFilename =~ /.sqlpp$/) && !isKVCluster",
"group": "navigation@1"
+ },
+ {
+ "command": "vscode-couchbase.runSearch",
+ "when": "(editorLangId == .cbs.json || resourceFilename =~ /.cbs.json$/) && !isKVCluster",
+ "group": "navigation@1"
}
],
"commandPalette": [
@@ -779,6 +835,10 @@
"command": "vscode-couchbase.openQueryWorkbench",
"when": "false"
},
+ {
+ "command": "vscode-couchbase.openSearchWorkbench",
+ "when": "false"
+ },
{
"command": "vscode-couchbase.deleteClusterConnection",
"when": "false"
@@ -862,6 +922,10 @@
"command": "vscode-couchbase.queryContext",
"when": "false"
},
+ {
+ "command": "vscode-couchbase.searchContext",
+ "when": "false"
+ },
{
"command": "vscode-couchbase.showFavoriteQueries",
"when": "false"
@@ -957,6 +1021,11 @@
"when": "view == couchbase && viewItem == active_connection && !isKVCluster",
"group": "workbench@1"
},
+ {
+ "command": "vscode-couchbase.openSearchWorkbench",
+ "when": "view == couchbase && viewItem == searchIndex && !isKVCluster",
+ "group": "workbench@1"
+ },
{
"submenu": "vscode-couchbase.toolsMenu",
"when": "view == couchbase && viewItem == active_connection"
diff --git a/src/commands/extensionCommands/commands.ts b/src/commands/extensionCommands/commands.ts
index eb74b5f1..2c4d7ecb 100644
--- a/src/commands/extensionCommands/commands.ts
+++ b/src/commands/extensionCommands/commands.ts
@@ -29,6 +29,7 @@ export namespace Commands {
export const getBucketMetaData: string = "vscode-couchbase.getBucketInfo";
export const createDocument: string = "vscode-couchbase.createDocument";
export const openDocument: string = "vscode-couchbase.openDocument";
+ export const openSearchIndex: string = "vscode-couchbase.openSearchIndex";
export const removeDocument: string = "vscode-couchbase.removeDocument";
export const getDocumentMetaData: string = "vscode-couchbase.getDocumentMetaData";
export const searchDocument: string = "vscode-couchbase.searchDocument";
@@ -36,13 +37,17 @@ export namespace Commands {
export const refreshIndexes: string = "vscode-couchbase.refreshIndexes";
export const openQueryNotebook: string = "vscode-couchbase.openQueryNotebook";
export const openQueryWorkbench: string = "vscode-couchbase.openQueryWorkbench";
+ export const openSearchWorkbench: string = "vscode-couchbase.openSearchWorkbench";
export const getSampleProjects: string = "vscode-couchbase.openSampleProjects";
export const loadMore: string = "vscode-couchbase.loadMore";
export const showOutputConsole: string = "vscode-couchbase.showOutputConsole";
export const runQuery: string = "vscode-couchbase.runQuery";
+ export const runSearchQuery: string = "vscode-couchbase.runSearch";
export const queryWorkbench: string = "vscode-couchbase.couchbase-query-workbench";
+ export const searchWorkbench: string = "vscode-couchbase.couchbase-search-workbench";
export const getClusterOverview: string = "vscode-couchbase.getClusterOverview";
export const queryContext: string = "vscode-couchbase.queryContext";
+ export const searchContext: string = "vscode-couchbase.searchContext";
export const showFavoriteQueries: string = "vscode-couchbase.showFavoriteQueries";
export const markFavoriteQuery: string = "vscode-couchbase.markFavoriteQuery";
export const showNamedParameters: string = "vscode-couchbase.showNamedParameters";
diff --git a/src/commands/fts/SearchWorkbench/controller.ts b/src/commands/fts/SearchWorkbench/controller.ts
new file mode 100644
index 00000000..1195d81e
--- /dev/null
+++ b/src/commands/fts/SearchWorkbench/controller.ts
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2011-2020 Couchbase, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import * as vscode from 'vscode';
+import { MemFS } from '../../../util/fileSystemProvider';
+import SearchIndexNode from '../../../model/SearchIndexNode';
+
+class SearchJsonDocumentContentProvider implements vscode.TextDocumentContentProvider {
+ provideTextDocumentContent(uri: vscode.Uri): string {
+ return '';
+ }
+}
+
+export default class UntitledSearchJsonDocumentService {
+ public searchJsonProvider = new SearchJsonDocumentContentProvider();
+ private untitledCount: number = 1;
+ private searchJsonScheme: string = '.cbs.json';
+
+ public disposable = vscode.workspace.registerTextDocumentContentProvider(this.searchJsonScheme, this.searchJsonProvider);
+
+ constructor() {
+ }
+
+
+
+ public async openSearchJsonTextDocument(searchIndexNode: SearchIndexNode,memFs: MemFS): Promise {
+ const uri = vscode.Uri.parse(`couchbase:/search-workbench-${searchIndexNode.searchIndexName}-${this.untitledCount}.cbs.json`);
+ this.untitledCount++;
+ let documentContent = Buffer.from('');
+ memFs.writeFile(uri, documentContent, {
+ create: true,
+ overwrite: true,
+ });
+
+
+ const document = await vscode.workspace.openTextDocument(uri);
+ document.save();
+ await vscode.window.showTextDocument(document, { preview: false });
+ return await vscode.window.showTextDocument(document, { preview: false });
+ }
+
+
+ public showTextDocument(document: vscode.TextDocument, column?: vscode.ViewColumn, preserveFocus?: boolean): Thenable {
+ return vscode.window.showTextDocument(document, column, preserveFocus);
+ }
+
+
+ public newQuery(searchIndexNode: SearchIndexNode, memFs: MemFS): Promise {
+ return new Promise((resolve, reject) => {
+ this.openSearchJsonTextDocument(searchIndexNode, memFs).then(resolve).catch(reject);
+ });
+ }
+}
\ No newline at end of file
diff --git a/src/commands/fts/SearchWorkbench/openSearchIndex.ts b/src/commands/fts/SearchWorkbench/openSearchIndex.ts
new file mode 100644
index 00000000..37c042ab
--- /dev/null
+++ b/src/commands/fts/SearchWorkbench/openSearchIndex.ts
@@ -0,0 +1,51 @@
+import * as vscode from "vscode";
+import SearchIndexNode from "../../../model/SearchIndexNode";
+import { DocumentNotFoundError } from "couchbase/dist/errors";
+import { logger } from "../../../logger/logger";
+import { MemFS } from "../../../util/fileSystemProvider";
+import ClusterConnectionTreeProvider from "../../../tree/ClusterConnectionTreeProvider";
+import { getActiveConnection } from "../../../util/connections";
+import { CouchbaseRestAPI } from "../../../util/apis/CouchbaseRestAPI";
+
+export const openSearchIndex = async (searchIndexNode: SearchIndexNode, clusterConnectionTreeProvider: ClusterConnectionTreeProvider, uriToCasMap: Map, memFs: MemFS) => {
+ try {
+ const connection = getActiveConnection();
+ if (!connection) {
+ return false;
+ }
+ const api = new CouchbaseRestAPI(connection);
+ const result = await api.fetchSearchIndexDefinition(searchIndexNode.indexName);
+
+ if (result?.indexDef instanceof Uint8Array || result?.indexDef instanceof Uint16Array || result?.indexDef instanceof Uint32Array) {
+ vscode.window.showInformationMessage("Unable to open Index definition: It is not a valid JSON", { modal: true });
+ return false;
+ }
+ const uri = vscode.Uri.parse(
+ `couchbase:/${searchIndexNode.bucketName}/${searchIndexNode.scopeName}/Search/${searchIndexNode.indexName}.json`
+ );
+ if (result) {
+ uriToCasMap.set(uri.toString(), result.indexDef.toString());
+ }
+ try {
+ memFs.writeFile(
+ uri,
+ Buffer.from(JSON.stringify(result?.indexDef, null, 2)),
+ { create: true, overwrite: true }
+ );
+ } catch (error) {
+ vscode.window.showInformationMessage("Unable to open Index definition: It is not a valid JSON ", { modal: true });
+ return false;
+ }
+ const document = await vscode.workspace.openTextDocument(uri);
+ await vscode.window.showTextDocument(document, { preview: false });
+ return true;
+ } catch (err: any) {
+ if (err instanceof vscode.FileSystemError && err.name === 'EntryNotFound (FileSystemError)' || err instanceof DocumentNotFoundError) {
+ clusterConnectionTreeProvider.refresh();
+ }
+ logger.error("Failed to open Document");
+ logger.debug(err);
+ }
+
+
+}
\ No newline at end of file
diff --git a/src/commands/fts/SearchWorkbench/searchWorkbench.ts b/src/commands/fts/SearchWorkbench/searchWorkbench.ts
new file mode 100644
index 00000000..18506303
--- /dev/null
+++ b/src/commands/fts/SearchWorkbench/searchWorkbench.ts
@@ -0,0 +1,105 @@
+import * as vscode from 'vscode';
+import { getActiveConnection } from '../../../util/connections';
+import UntitledSqlppDocumentService from './controller';
+import { WorkbenchWebviewProvider } from '../../../workbench/workbenchWebviewProvider';
+import { CouchbaseError, QueryOptions, QueryProfileMode, QueryStatus } from "couchbase";
+import { ISearchQueryContext } from '../../../types/ISearchQueryContext';
+import { CouchbaseRestAPI } from '../../../util/apis/CouchbaseRestAPI';
+import { MemFS } from "../../../util/fileSystemProvider";
+import UntitledSearchJsonDocumentService from './controller';
+import SearchIndexNode from '../../../model/SearchIndexNode';
+
+export class SearchWorkbench {
+ private _untitledSearchJsonDocumentService: UntitledSearchJsonDocumentService;
+ public editorToContext: Map;
+
+ constructor() {
+ this._untitledSearchJsonDocumentService =
+ new UntitledSearchJsonDocumentService();
+ this.editorToContext = new Map();
+ }
+
+
+ runCouchbaseSearchQuery = async (
+ workbenchWebviewProvider: WorkbenchWebviewProvider
+ ) => {
+ const connection = getActiveConnection();
+ if (!connection) {
+ vscode.window.showInformationMessage(
+ "Kindly establish a connection with the cluster before running an index query."
+ );
+ return false;
+ }
+
+ // Get the active text editor
+ const activeTextEditor = vscode.window.activeTextEditor;
+ if (activeTextEditor && activeTextEditor.document.languageId === "searchQuery") {
+
+ activeTextEditor.document.save();
+ const indexQueryPayload = activeTextEditor.selection.isEmpty ? activeTextEditor.document.getText() : activeTextEditor.document.getText(activeTextEditor.selection);
+ const queryContext = this.editorToContext.get(activeTextEditor.document.uri.toString());
+
+ try {
+ // Reveal the webview when the extension is activated
+ vscode.commands.executeCommand('workbench.view.extension.couchbase-workbench-panel');
+ vscode.commands.executeCommand("workbench.action.focusPanel");
+ await new Promise((resolve) => setTimeout(resolve, 500));
+ await workbenchWebviewProvider.sendQueryResult(JSON.stringify([{ "status": "Executing statement" }]), { queryStatus: QueryStatus.Running }, null);
+ const explainPlan = JSON.stringify("");
+ const couchbbaseRestAPI = new CouchbaseRestAPI(connection);
+ const searchQueryResult = await couchbbaseRestAPI.runSearchIndexes(queryContext?.indexName, indexQueryPayload);
+ workbenchWebviewProvider.setSearchQueryResult(
+ JSON.stringify(searchQueryResult?.hits),
+ {},
+ explainPlan
+ );
+ } catch (err) {
+ const errorArray = [];
+ if (err instanceof CouchbaseError) {
+ const { first_error_code, first_error_message, statement } =
+ err.cause as any;
+ if (
+ first_error_code !== undefined ||
+ first_error_message !== undefined ||
+ statement !== undefined
+ ) {
+ errorArray.push({
+ code: first_error_code,
+ msg: first_error_message,
+ query: statement,
+ });
+ } else {
+ errorArray.push(err);
+ }
+ } else {
+ errorArray.push(err);
+ }
+ const queryStatusProps = {
+ queryStatus: QueryStatus.Fatal,
+ rtt: "-",
+ elapsed: "-",
+ executionTime: "-",
+ numDocs: "-",
+ size: "-",
+ };
+ workbenchWebviewProvider.setQueryResult(
+ JSON.stringify(errorArray),
+ queryStatusProps,
+ null
+ );
+ }
+
+ }
+
+ }
+ openSearchWorkbench(searchIndexNode: SearchIndexNode, memFs: MemFS) {
+ try {
+ return this._untitledSearchJsonDocumentService.newQuery(searchIndexNode, memFs);
+ } catch (error) {
+ console.log("Error in openSearchWorkbench:", error);
+ throw error;
+ }
+ }
+
+
+}
\ No newline at end of file
diff --git a/src/extension.ts b/src/extension.ts
index d3ff3f04..be9a9baa 100644
--- a/src/extension.ts
+++ b/src/extension.ts
@@ -52,7 +52,7 @@ import { WorkbenchWebviewProvider } from "./workbench/workbenchWebviewProvider";
import { fetchClusterOverview } from "./pages/overviewCluster/overviewCluster";
import DependenciesDownloader from "./handlers/handleCLIDownloader";
import { sqlppFormatter } from "./commands/formatting/sqlppFormatter";
-import { fetchQueryContext } from "./pages/queryContext/queryContext";
+import { fetchQueryContext, fetchSearchContext } from "./pages/queryContext/queryContext";
import { fetchFavoriteQueries } from "./pages/FavoriteQueries/FavoriteQueries";
import { markFavoriteQuery } from "./commands/favoriteQueries/markFavoriteQuery";
import { QueryHistoryTreeProvider } from "./tree/QueryHistoryTreeProvider";
@@ -76,6 +76,10 @@ import { newChatHandler } from "./commands/iq/chat/newChatHandler";
import { SecretService } from "./util/secretService";
import { kvTypeFilterDocuments } from "./commands/documents/documentFilters/kvTypeFilterDocuments";
import { fetchNamedParameters } from "./pages/namedParameters/namedParameters";
+import { SearchWorkbench } from "./commands/fts/SearchWorkbench/searchWorkbench";
+import SearchIndexNode from "./model/SearchIndexNode";
+import { openSearchIndex } from "./commands/fts/SearchWorkbench/openSearchIndex";
+import { handleSearchContextStatusbar } from "./handlers/handleSearchQueryContextStatusBar";
export function activate(context: vscode.ExtensionContext) {
Global.setState(context.globalState);
@@ -92,6 +96,10 @@ export function activate(context: vscode.ExtensionContext) {
const uriToCasMap = new Map();
const workbench = new QueryWorkbench();
+ const searchWorkbench = new SearchWorkbench();
+
+ let currentSearchIndexNode: SearchIndexNode;
+ let currentSearchWorkbench: SearchWorkbench;
const subscriptions = context.subscriptions;
const cacheService = new CacheService();
@@ -185,7 +193,8 @@ export function activate(context: vscode.ExtensionContext) {
if (
editor &&
editor.document.languageId === "json" &&
- editor.document.uri.scheme === "couchbase"
+ editor.document.uri.scheme === "couchbase" &&
+ !editor.document.uri.path.includes("Search")
) {
await handleActiveEditorChange(editor, uriToCasMap, memFs);
}
@@ -291,6 +300,15 @@ export function activate(context: vscode.ExtensionContext) {
)
);
+ subscriptions.push(
+ vscode.commands.registerCommand(
+ Commands.openSearchIndex,
+ async (searchIndexNode: SearchIndexNode) => {
+ await openSearchIndex(searchIndexNode, clusterConnectionTreeProvider, uriToCasMap, memFs);
+ }
+ )
+ );
+
subscriptions.push(
vscode.commands.registerCommand(
Commands.openIndexInfo,
@@ -520,6 +538,10 @@ export function activate(context: vscode.ExtensionContext) {
)
);
+ // Initialize the global status bar item which will be used for query and search query context
+ let globalStatusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 1000);
+ globalStatusBarItem.hide();
+
vscode.languages.registerDocumentFormattingEditProvider('SQL++', {
provideDocumentFormattingEdits(document: vscode.TextDocument): vscode.TextEdit[] {
return sqlppFormatter(document);
@@ -573,19 +595,19 @@ export function activate(context: vscode.ExtensionContext) {
subscriptions.push(
vscode.commands.registerCommand(Commands.queryContext, () => {
- fetchQueryContext(workbench, context);
+ fetchQueryContext(workbench, context, globalStatusBarItem);
})
);
// subscription to make sure query context status bar is only visible on sqlpp files
subscriptions.push(
vscode.window.onDidChangeActiveTextEditor(async (editor) => {
- await handleQueryContextStatusbar(editor, workbench);
+ await handleQueryContextStatusbar(editor, workbench, globalStatusBarItem);
})
);
- // Handle initial view of context status bar
+ // // Handle initial view of context status bar
const activeEditor = vscode.window.activeTextEditor;
- handleQueryContextStatusbar(activeEditor, workbench);
+ handleQueryContextStatusbar(activeEditor, workbench, globalStatusBarItem);
subscriptions.push(
vscode.commands.registerCommand(
@@ -609,7 +631,7 @@ export function activate(context: vscode.ExtensionContext) {
vscode.commands.registerCommand(
Commands.showNamedParameters,
() => {
- fetchNamedParameters();
+ fetchNamedParameters();
}
)
);
@@ -668,6 +690,37 @@ export function activate(context: vscode.ExtensionContext) {
})
);
+ const searchContextCommand = vscode.commands.registerCommand(Commands.searchContext, () => {
+ fetchSearchContext(currentSearchIndexNode, currentSearchWorkbench, context, globalStatusBarItem);
+ });
+ context.subscriptions.push(searchContextCommand);
+
+
+
+ const openSearchWorkbenchCommand = vscode.commands.registerCommand(Commands.openSearchWorkbench, async (searchIndexNode: SearchIndexNode) => {
+ const connection = Memory.state.get(Constants.ACTIVE_CONNECTION);
+ if (!connection) {
+ vscode.window.showErrorMessage("No active connection available.");
+ return;
+ }
+
+ currentSearchIndexNode = searchIndexNode;
+ currentSearchWorkbench = searchWorkbench;
+
+ searchWorkbench.openSearchWorkbench(searchIndexNode, memFs);
+
+ const editorChangeSubscription = vscode.window.onDidChangeActiveTextEditor(async (editor) => {
+ if (editor && editor.document.languageId === "searchQuery") {
+ await handleSearchContextStatusbar(editor, searchIndexNode, searchWorkbench, globalStatusBarItem);
+ }
+ });
+ context.subscriptions.push(editorChangeSubscription);
+
+ });
+ context.subscriptions.push(openSearchWorkbenchCommand);
+
+
+
context.subscriptions.push(
vscode.workspace.registerNotebookSerializer(
Constants.notebookType, new QueryContentSerializer(), { transientOutputs: true }
@@ -699,6 +752,30 @@ export function activate(context: vscode.ExtensionContext) {
true
); // Required to enable run query button at the start
+ context.subscriptions.push(
+ vscode.commands.registerCommand(Commands.runSearchQuery, async (searchIndexNode: SearchIndexNode) => {
+ vscode.commands.executeCommand(
+ "setContext",
+ "vscode-couchbase.runSearchButtonEnabled",
+ undefined
+ );
+ await searchWorkbench.runCouchbaseSearchQuery(
+ workbenchWebviewProvider
+ );
+ vscode.commands.executeCommand(
+ "setContext",
+ "vscode-couchbase.runSearchButtonEnabled",
+ true
+ );
+ })
+ );
+ vscode.commands.executeCommand(
+ "setContext",
+ "vscode-couchbase.runSearchButtonEnabled",
+ true
+ ); // Required to enable run search query button at the start
+
+
context.subscriptions.push(
vscode.commands.registerCommand(
Commands.checkAndCreatePrimaryIndex,
diff --git a/src/handlers/handleQueryContextStatusbar.ts b/src/handlers/handleQueryContextStatusbar.ts
index 1f6c1f3a..df4895f9 100644
--- a/src/handlers/handleQueryContextStatusbar.ts
+++ b/src/handlers/handleQueryContextStatusbar.ts
@@ -1,12 +1,12 @@
import * as vscode from 'vscode';
import { QueryWorkbench } from '../workbench/queryWorkbench';
-import { hideQueryContextStatusbar, showQueryContextStatusbar } from '../util/queryContextUtils';
-export const handleQueryContextStatusbar = async (editor: vscode.TextEditor | undefined, workbench: QueryWorkbench) => {
+import { showQueryContextStatusbar } from '../util/queryContextUtils';
+export const handleQueryContextStatusbar = async (editor: vscode.TextEditor | undefined, workbench: QueryWorkbench, globalStatusBarItem: vscode.StatusBarItem) => {
if( editor && editor.document.languageId === "SQL++"){
// Case 1: Show Status bar
- showQueryContextStatusbar(editor, workbench);
+ showQueryContextStatusbar(editor, workbench, globalStatusBarItem);
} else {
// Case 2: Don't show status bar
- hideQueryContextStatusbar();
+ globalStatusBarItem.hide();
}
};
\ No newline at end of file
diff --git a/src/handlers/handleSearchQueryContextStatusBar.ts b/src/handlers/handleSearchQueryContextStatusBar.ts
new file mode 100644
index 00000000..478e6eb6
--- /dev/null
+++ b/src/handlers/handleSearchQueryContextStatusBar.ts
@@ -0,0 +1,14 @@
+import * as vscode from 'vscode';
+import { SearchWorkbench } from '../commands/fts/SearchWorkbench/searchWorkbench';
+import { showSearchContextStatusbar } from '../util/queryContextUtils';
+import SearchIndexNode from '../model/SearchIndexNode';
+
+export const handleSearchContextStatusbar = async (editor: vscode.TextEditor | undefined,searchNode:SearchIndexNode, workbench:SearchWorkbench, globalStatusBarItem: vscode.StatusBarItem) => {
+ if( editor && editor.document.languageId === "searchQuery"){
+ // Case 1: Show Status bar
+ showSearchContextStatusbar(editor,searchNode, workbench, globalStatusBarItem);
+ } else {
+ // Case 2: Don't show status bar
+ globalStatusBarItem.hide();
+ }
+};
\ No newline at end of file
diff --git a/src/model/CollectionDirectory.ts b/src/model/CollectionDirectory.ts
new file mode 100644
index 00000000..fbcba068
--- /dev/null
+++ b/src/model/CollectionDirectory.ts
@@ -0,0 +1,99 @@
+import * as vscode from "vscode";
+import { IConnection } from "../types/IConnection";
+import { INode } from "../types/INode";
+import { logger } from "../logger/logger";
+import { CouchbaseRestAPI } from "../util/apis/CouchbaseRestAPI";
+import { CacheService } from "../util/cacheService/cacheService";
+import { getActiveConnection } from "../util/connections";
+import { Memory } from "../util/util";
+import { hasQueryService } from "../util/common";
+import { ParsingFailureError, PlanningFailureError } from "couchbase";
+import CollectionNode from "./CollectionNode";
+import InformationNode from "./InformationNode";
+
+export class CollectionDirectory implements INode {
+ constructor(
+ public readonly parentNode: INode,
+ public readonly scopeName: string,
+ public readonly bucketName: string,
+ public readonly collections: any[],
+ public readonly collapsibleState: vscode.TreeItemCollapsibleState,
+ public cacheService: CacheService
+ ) {}
+
+ public getTreeItem(): vscode.TreeItem {
+ return {
+ label: `Collections`,
+ collapsibleState: vscode.TreeItemCollapsibleState.Collapsed,
+ contextValue: "collectionDirectory",
+ };
+ }
+
+ public async getChildren(): Promise {
+ const connection = getActiveConnection();
+ if (!connection) {
+ return [];
+ }
+ const collectionList: any[] = [];
+ const couchbaseRestAPI = new CouchbaseRestAPI(connection);
+ const KVCollectionCount: Map = await couchbaseRestAPI.getKVDocumentCount(this.bucketName, this.scopeName);
+
+ for (const collection of this.collections) {
+ try {
+ const queryTypeFilter = Memory.state.get(
+ `queryTypeFilterDocuments-${connection.connectionIdentifier}-${this.bucketName}-${this.scopeName}-${collection.name}`
+ ) ?? "";
+
+ const filterDocumentsType = Memory.state.get(
+ `filterDocumentsType-${connection.connectionIdentifier}-${this.bucketName}-${this.scopeName}-${collection.name}`
+ ) ?? "";
+
+ let rowCount = 0;
+ try {
+ if (!hasQueryService(connection?.services) || filterDocumentsType !== "query") {
+ rowCount = KVCollectionCount.get(`kv_collection_item_count-${this.bucketName}-${this.scopeName}-${collection.name}`) ?? 0;
+
+ if(filterDocumentsType === "kv") {
+ rowCount = -1;
+ }
+ }
+ else {
+ const queryResult = await connection?.cluster?.query(
+ `select count(1) as count from \`${this.bucketName}\`.\`${this.scopeName
+ }\`.\`${collection.name}\` ${queryTypeFilter.length > 0 ? "WHERE " + queryTypeFilter : ""
+ };`
+ );
+ rowCount = queryResult?.rows[0].count;
+ }
+ } catch (err: any) {
+ if (err instanceof PlanningFailureError) {
+ vscode.window.showErrorMessage(
+ "Unable to find primary index for document and filter seems to be applied, showing count as 0"
+ );
+ } else if (err instanceof ParsingFailureError) {
+ logger.error(`In Scope Node: ${this.scopeName}: Parsing Failed: Incorrect filter definition`);
+ }
+ }
+
+ const collectionTreeItem = new CollectionNode(
+ this,
+ this.scopeName,
+ rowCount,
+ this.bucketName,
+ collection.name,
+ vscode.TreeItemCollapsibleState.None,
+ this.cacheService
+ );
+ collectionList.push(collectionTreeItem);
+ } catch (err: any) {
+ logger.error("Failed to load Collections");
+ logger.debug(err);
+ throw new Error(err);
+ }
+ }
+ if (collectionList.length === 0) {
+ collectionList.push(new InformationNode("No Collections found"));
+ }
+ return collectionList;
+ }
+}
\ No newline at end of file
diff --git a/src/model/ScopeNode.ts b/src/model/ScopeNode.ts
index 265e3295..51bebee4 100644
--- a/src/model/ScopeNode.ts
+++ b/src/model/ScopeNode.ts
@@ -16,15 +16,10 @@
import * as vscode from "vscode";
import * as path from "path";
import { INode } from "../types/INode";
-import { Memory } from "../util/util";
import { getActiveConnection } from "../util/connections";
-import CollectionNode from "./CollectionNode";
-import { logger } from "../logger/logger";
-import InformationNode from "./InformationNode";
-import { ParsingFailureError, PlanningFailureError } from "couchbase";
-import { hasQueryService } from "../util/common";
-import { CouchbaseRestAPI } from "../util/apis/CouchbaseRestAPI";
import { CacheService } from "../../src/util/cacheService/cacheService";
+import { SearchDirectory } from "./SearchDirectory";
+import { CollectionDirectory } from "./CollectionDirectory";
export class ScopeNode implements INode {
@@ -35,7 +30,13 @@ export class ScopeNode implements INode {
public readonly collections: any[],
public readonly collapsibleState: vscode.TreeItemCollapsibleState,
public cacheService: CacheService
- ) { }
+ ) {
+ vscode.workspace.fs.createDirectory(
+ vscode.Uri.parse(
+ `couchbase:/${bucketName}/${scopeName}/Search`
+ )
+ );
+}
public getTreeItem(): vscode.TreeItem {
return {
@@ -64,70 +65,36 @@ export class ScopeNode implements INode {
* @returns Two Directory one contains Index definitions and other contains Collections
* */
public async getChildren(): Promise {
+
const connection = getActiveConnection();
if (!connection) {
return [];
}
- const collectionList: any[] = [];
- const couchbaseRestAPI = new CouchbaseRestAPI(connection);
- const KVCollectionCount: Map = await couchbaseRestAPI.getKVDocumentCount(this.bucketName, this.scopeName);
- for (const collection of this.collections) {
- try {
- const queryTypeFilter = Memory.state.get(
- `queryTypeFilterDocuments-${connection.connectionIdentifier}-${this.bucketName}-${this.scopeName}-${collection.name}`
- ) ?? "";
+ const childrenDirectoriesList: INode[] = [];
- const filterDocumentsType = Memory.state.get(
- `filterDocumentsType-${connection.connectionIdentifier}-${this.bucketName}-${this.scopeName}-${collection.name}`
- ) ?? "";
+ // Search Directory
+ childrenDirectoriesList.push(
+ new SearchDirectory(
+ this,
+ "Search",
+ this.bucketName,
+ this.scopeName
+ )
+ );
- let rowCount = 0;
- try {
- if (!hasQueryService(connection?.services) || filterDocumentsType !== "query") {
- rowCount = KVCollectionCount.get(`kv_collection_item_count-${this.bucketName}-${this.scopeName}-${collection.name}`) ?? 0;
+ // Collections Directory
+ const collectionDirectory = new CollectionDirectory(
+ this,
+ this.scopeName,
+ this.bucketName,
+ this.collections,
+ vscode.TreeItemCollapsibleState.Collapsed,
+ this.cacheService
+ );
+ childrenDirectoriesList.push(collectionDirectory);
- if(filterDocumentsType === "kv") {
- rowCount = -1;
- }
- }
- else {
- const queryResult = await connection?.cluster?.query(
- `select count(1) as count from \`${this.bucketName}\`.\`${this.scopeName
- }\`.\`${collection.name}\` ${queryTypeFilter.length > 0 ? "WHERE " + queryTypeFilter : ""
- };`
- );
- rowCount = queryResult?.rows[0].count;
- }
- } catch (err: any) {
- if (err instanceof PlanningFailureError) {
- vscode.window.showErrorMessage(
- "Unable to find primary index for document and filter seems to be applied, showing count as 0"
- );
- } else if (err instanceof ParsingFailureError) {
- logger.error(`In Scope Node: ${this.scopeName}: Parsing Failed: Incorrect filter definition`);
- }
- }
+ return childrenDirectoriesList;
- const collectionTreeItem = new CollectionNode(
- this,
- this.scopeName,
- rowCount,
- this.bucketName,
- collection.name,
- vscode.TreeItemCollapsibleState.None,
- this.cacheService
- );
- collectionList.push(collectionTreeItem);
- } catch (err: any) {
- logger.error("Failed to load Collections");
- logger.debug(err);
- throw new Error(err);
- }
- }
- if (collectionList.length === 0) {
- collectionList.push(new InformationNode("No Collections found"));
- }
- return collectionList;
}
-}
+}
\ No newline at end of file
diff --git a/src/model/SearchDirectory.ts b/src/model/SearchDirectory.ts
new file mode 100644
index 00000000..fcb1ef87
--- /dev/null
+++ b/src/model/SearchDirectory.ts
@@ -0,0 +1,78 @@
+import { TreeItem, TreeItemCollapsibleState } from "vscode";
+import { IConnection } from "../types/IConnection";
+import { INode } from "../types/INode";
+import SearchIndexNode from "./SearchIndexNode";
+import { logger } from "../logger/logger";
+import InformationNode from "./InformationNode";
+import { CouchbaseRestAPI } from "../util/apis/CouchbaseRestAPI";
+import { getActiveConnection } from "../util/connections";
+import * as path from "path";
+
+export class SearchDirectory implements INode {
+ constructor(public readonly parentNode: INode,
+ public readonly itemName: string,
+ public readonly bucketName: string,
+ public readonly scopeName: string) {
+ }
+
+ public getTreeItem(): TreeItem {
+ return {
+ label: `${this.itemName}`,
+ collapsibleState: TreeItemCollapsibleState.Collapsed,
+ contextValue: "searchDirectory",
+ iconPath: {
+ light: path.join(
+ __filename,
+ "..",
+ "..",
+ "images/light",
+ "search-icon.svg"
+ ),
+ dark: path.join(
+ __filename,
+ "..",
+ "..",
+ "images/dark",
+ "search-icon.svg"
+ ),
+ }
+ };
+ }
+
+ public async getChildren(): Promise {
+ try {
+ // get all search indexes
+ const connection = getActiveConnection();
+ if (!connection){
+
+ return []
+ }
+ const searchIndexesManager = connection?.cluster?.searchIndexes();
+ const ftsIndexes = await searchIndexesManager?.getAllIndexes();
+ const bucketIndexes = ftsIndexes?.filter(index => index.sourceName === this.bucketName);
+ if (bucketIndexes === undefined) {
+ return [];
+ }
+ const searchIndexChildren: INode[] = bucketIndexes.map((searchIndex) =>
+ new SearchIndexNode(
+ searchIndex.name,
+ this.bucketName,
+ this.scopeName,
+ searchIndex.name
+ )
+
+
+
+ ) || [];
+
+ if (searchIndexChildren.length === 0) {
+ searchIndexChildren.push(new InformationNode("No search indexes found"));
+ }
+ return searchIndexChildren;
+ }
+ catch (error) {
+ logger.error(`Error getting search indexes: ${error}`);
+ return [];
+ }
+ }
+}
diff --git a/src/model/SearchIndexNode.ts b/src/model/SearchIndexNode.ts
new file mode 100644
index 00000000..53f0e35a
--- /dev/null
+++ b/src/model/SearchIndexNode.ts
@@ -0,0 +1,44 @@
+import * as vscode from "vscode";
+import { INode } from "../types/INode";
+import * as path from "path";
+
+export default class SearchIndexNode implements INode {
+ constructor(
+ public readonly searchIndexName: string,
+ public readonly bucketName: string,
+ public readonly scopeName: string,
+ public readonly indexName: string,
+ ) { }
+ public async getTreeItem(): Promise {
+ return {
+ label: `${this.searchIndexName}`,
+ collapsibleState: vscode.TreeItemCollapsibleState.None,
+ contextValue: "searchIndex",
+ command: {
+ command: "vscode-couchbase.openSearchIndex",
+ title: "Open Search Index",
+ arguments: [this],
+ },
+ iconPath: {
+ light: path.join(
+ __filename,
+ "..",
+ "..",
+ "images/light",
+ "document.svg"
+ ),
+ dark: path.join(
+ __filename,
+ "..",
+ "..",
+ "images/dark",
+ "document.svg"
+ ),
+ },
+ };
+ }
+
+ public async getChildren(): Promise {
+ return [];
+ }
+}
\ No newline at end of file
diff --git a/src/pages/queryContext/queryContext.ts b/src/pages/queryContext/queryContext.ts
index 8d78dea8..30b5051c 100644
--- a/src/pages/queryContext/queryContext.ts
+++ b/src/pages/queryContext/queryContext.ts
@@ -5,8 +5,10 @@ import { logger } from "../../logger/logger";
import { Bucket, BucketSettings } from "couchbase";
import { QueryWorkbench } from "../../workbench/queryWorkbench";
import { showQueryContextStatusbar } from "../../util/queryContextUtils";
-import { Constants } from "../../util/constants";
import { getActiveConnection } from "../../util/connections";
+import { SearchWorkbench } from "../../commands/fts/SearchWorkbench/searchWorkbench";
+import SearchIndexNode from "../../model/SearchIndexNode";
+import { Commands } from "../../commands/extensionCommands/commands";
const fetchBucketNames = (bucketsSettings: BucketSettings[] | undefined, connection: IConnection): Array => {
const allBuckets: Array = [];
@@ -22,7 +24,7 @@ const fetchBucketNames = (bucketsSettings: BucketSettings[] | undefined, connect
return allBuckets;
};
-export async function fetchQueryContext(workbench: QueryWorkbench, context: vscode.ExtensionContext) {
+export async function fetchQueryContext(workbench: QueryWorkbench, context: vscode.ExtensionContext, globalStatusBarItem:any) {
const connection = getActiveConnection();
if (!connection) {
@@ -36,7 +38,7 @@ export async function fetchQueryContext(workbench: QueryWorkbench, context: vsco
!(activeEditor &&
activeEditor.document.languageId === "SQL++")
) {
- vscode.window.showErrorMessage("workbench is not active");
+ vscode.window.showErrorMessage("Please ensure that the workbench is open/active");
return;
}
@@ -67,7 +69,7 @@ export async function fetchQueryContext(workbench: QueryWorkbench, context: vsco
const bucketNameSelected = selectedItem.label;
if (bucketNameSelected === 'Clear Context') {
workbench.editorToContext.delete(activeEditor.document.uri.toString());
- showQueryContextStatusbar(activeEditor, workbench);
+ showQueryContextStatusbar(activeEditor, workbench,globalStatusBarItem);
return;
}
const scopes = await connection.cluster
@@ -88,10 +90,93 @@ export async function fetchQueryContext(workbench: QueryWorkbench, context: vsco
bucketName: bucketNameSelected,
scopeName: scopeNameSelected.label
});
- showQueryContextStatusbar(activeEditor, workbench);
+ showQueryContextStatusbar(activeEditor, workbench,globalStatusBarItem);
} catch (err) {
logger.error(`failed to open and set query context: ${err}`);
logger.debug(err);
}
-}
\ No newline at end of file
+}
+
+export async function fetchSearchContext(searchIndexNode: SearchIndexNode, workbench: SearchWorkbench, context: vscode.ExtensionContext, globalStatusBarItem: vscode.StatusBarItem) {
+ const connection = getActiveConnection();
+ if (!connection) {
+ vscode.window.showErrorMessage("Please connect to a cluster before setting query context");
+ return;
+ }
+ try {
+ const activeEditor = vscode.window.activeTextEditor;
+ if (!(activeEditor && activeEditor.document.languageId === "searchQuery")) {
+ vscode.window.showErrorMessage("Please ensure that the workbench is open/active");
+ return;
+ }
+
+ // Fetching bucket names
+ const bucketsSettings = await connection?.cluster?.buckets().getAllBuckets();
+ const allBuckets = fetchBucketNames(bucketsSettings, connection);
+ if (!allBuckets || allBuckets.length === 0) {
+ vscode.window.showErrorMessage('No buckets found.');
+ return;
+ }
+
+ // Displaying QuickPick for buckets
+ const selectedItem = await vscode.window.showQuickPick(allBuckets.map(bucket => ({
+ label: bucket.name,
+ iconPath: new vscode.ThemeIcon("database")
+ })), {
+ placeHolder: 'Query Context: Select a bucket',
+ canPickMany: false
+ });
+
+ if (!selectedItem) {
+ vscode.window.showInformationMessage("No buckets selected.");
+ return;
+ }
+
+ const bucketNameSelected = selectedItem.label;
+
+ // Fetching search indexes specific to the selected bucket
+ const searchIndexesManager = connection?.cluster?.searchIndexes();
+ const ftsIndexes = await searchIndexesManager?.getAllIndexes();
+ const bucketIndexes = ftsIndexes?.filter(index => index.sourceName === bucketNameSelected);
+ if (!bucketIndexes || bucketIndexes.length === 0) {
+ vscode.window.showErrorMessage('No Indexes found.');
+ return;
+ }
+
+ // Displaying QuickPick for indexes
+ const indexNameSelected = await vscode.window.showQuickPick(bucketIndexes.map(index => ({
+ label: index.name,
+ iconPath: new vscode.ThemeIcon("file-submodule")
+ })), {
+ placeHolder: 'Query Context: Select an Index',
+ canPickMany: false
+ });
+
+ if (!indexNameSelected) {
+ vscode.window.showInformationMessage('No index selected.');
+ return;
+ }
+
+ const editorId = activeEditor.document.uri.toString();
+
+ // Setting new context
+ workbench.editorToContext.set(editorId, {
+ bucketName: bucketNameSelected,
+ indexName: indexNameSelected.label,
+ statusBarItem: globalStatusBarItem,
+ searchNode: searchIndexNode
+ });
+
+ // Update the status bar directly
+ let displayBucketName = bucketNameSelected.length > 15 ? `${bucketNameSelected.substring(0, 13)}...` : bucketNameSelected;
+ let displayIndexName = indexNameSelected.label.length > 15 ? `${indexNameSelected.label.substring(0, 13)}...` : indexNameSelected.label;
+ globalStatusBarItem.text = `$(group-by-ref-type) ${displayBucketName} > ${displayIndexName}`;
+ globalStatusBarItem.tooltip = "Search Query Context";
+ globalStatusBarItem.command = Commands.searchContext;
+
+ } catch (err) {
+ logger.error(`Failed to open and set query context: ${err}`);
+ logger.debug(err);
+ }
+}
diff --git a/src/types/ISearchQueryContext.ts b/src/types/ISearchQueryContext.ts
new file mode 100644
index 00000000..3e9527ab
--- /dev/null
+++ b/src/types/ISearchQueryContext.ts
@@ -0,0 +1,9 @@
+import * as vscode from 'vscode';
+import SearchIndexNode from '../model/SearchIndexNode';
+
+export interface ISearchQueryContext {
+ bucketName: string;
+ indexName: string;
+ statusBarItem: vscode.StatusBarItem;
+ searchNode: SearchIndexNode;
+}
diff --git a/src/util/apis/CouchbaseRestAPI.ts b/src/util/apis/CouchbaseRestAPI.ts
index 0b12425c..e6e5ad8a 100644
--- a/src/util/apis/CouchbaseRestAPI.ts
+++ b/src/util/apis/CouchbaseRestAPI.ts
@@ -130,6 +130,57 @@ export class CouchbaseRestAPI {
}
}
+ public async runSearchIndexes(indexName:string | undefined, payload:any) {
+ const username = this.connection.username;
+ const secretService = SecretService.getInstance();
+ const password = await secretService.get(`${Constants.extensionID}-${getConnectionId(this.connection)}`);
+ if (!password) {
+ return undefined;
+ }
+ let url = (await getServerURL(this.connection.url))[0];
+ url = (this.connection.isSecure ? `https://${url}:18094` : `http://${url}:8094`);
+ url += `/api/index/${indexName}/query`;
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
+ let content = await axios.post(url, payload, {
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`
+ },
+ httpsAgent: new https.Agent({
+ rejectUnauthorized: false
+ })
+ });
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED = '1';
+ return content.data;
+ }
+
+ public async fetchSearchIndexDefinition(indexName:string) {
+
+ const username = this.connection.username;
+ const secretService = SecretService.getInstance();
+ const password = await secretService.get(`${Constants.extensionID}-${getConnectionId(this.connection)}`);
+ if (!password) {
+ return undefined;
+ }
+ let url = (await getServerURL(this.connection.url))[0];
+ url = (this.connection.isSecure ? `https://${url}:18094` : `http://${url}:8094`);
+ url += `/api/index/${indexName}`;
+
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
+ let content = await axios.get(url, {
+ method: "GET",
+ headers: {
+ Authorization: `Basic ${btoa(`${username}:${password}`)}`
+ },
+ httpsAgent: new https.Agent({
+ rejectUnauthorized: false,
+ })
+ });
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED = '1';
+ return content.data;
+
+ }
+
public async getKVDocumentCount(bucketName: string, scopeName: string): Promise