From 41ccad442e866622cdad4debe10458812f0cf2e3 Mon Sep 17 00:00:00 2001 From: Denis Rosa Date: Mon, 22 Apr 2024 23:44:40 +0200 Subject: [PATCH] initial version --- Readme.md | 2 +- .../couchbase/intellij/VirtualFileKeys.java | 2 + .../intellij/database/ActiveCluster.java | 31 +- .../intellij/database/DataLoader.java | 94 ++++- .../searchworkbench/CBSJsonFileType.java | 43 ++ .../searchworkbench/SearchFileEditor.java | 375 ++++++++++++++++++ .../SearchFileEditorProvider.java | 36 ++ .../searchworkbench/SearchQueryExecutor.java | 141 +++++++ .../intellij/tree/CouchbaseWindowContent.java | 14 +- .../intellij/tree/TreeExpandListener.java | 6 +- .../intellij/tree/TreeRightClickListener.java | 84 ++++ .../cblite/nodes/CBLScopeNodeDescriptor.java | 2 +- .../tree/node/CollectionsNodeDescriptor.java | 21 + .../tree/node/ScopeNodeDescriptor.java | 2 +- .../tree/node/SearchIndexNodeDescriptor.java | 36 ++ .../tree/node/SearchNodeDescriptor.java | 21 + .../tree/overview/apis/CouchbaseRestAPI.java | 89 +++-- .../intellij/workbench/QueryExecutor.java | 50 +-- .../intellij/workbench/QueryResultUtil.java | 41 ++ .../error/CouchbaseQueryErrorUtil.java | 9 + src/main/java/utils/CBConfigUtil.java | 9 +- src/main/resources/META-INF/plugin.xml | 13 +- src/main/resources/assets/icons/cbs.svg | 38 ++ .../resources/assets/icons/collections.svg | 7 +- src/main/resources/assets/icons/scope.svg | 5 + 25 files changed, 1080 insertions(+), 91 deletions(-) create mode 100644 src/main/java/com/couchbase/intellij/searchworkbench/CBSJsonFileType.java create mode 100644 src/main/java/com/couchbase/intellij/searchworkbench/SearchFileEditor.java create mode 100644 src/main/java/com/couchbase/intellij/searchworkbench/SearchFileEditorProvider.java create mode 100644 src/main/java/com/couchbase/intellij/searchworkbench/SearchQueryExecutor.java create mode 100644 src/main/java/com/couchbase/intellij/tree/node/CollectionsNodeDescriptor.java create mode 100644 src/main/java/com/couchbase/intellij/tree/node/SearchIndexNodeDescriptor.java create mode 100644 src/main/java/com/couchbase/intellij/tree/node/SearchNodeDescriptor.java create mode 100644 src/main/java/com/couchbase/intellij/workbench/QueryResultUtil.java create mode 100644 src/main/resources/assets/icons/cbs.svg create mode 100644 src/main/resources/assets/icons/scope.svg diff --git a/Readme.md b/Readme.md index 08a3e84e..c5466d9e 100644 --- a/Readme.md +++ b/Readme.md @@ -1,4 +1,4 @@ -# Couchbase Jetbrains Plugin +3# Couchbase Jetbrains Plugin Welcome to the official Couchbase plugin for the Jetbrains Platform diff --git a/src/main/java/com/couchbase/intellij/VirtualFileKeys.java b/src/main/java/com/couchbase/intellij/VirtualFileKeys.java index d3f3fd8b..3c5526dd 100644 --- a/src/main/java/com/couchbase/intellij/VirtualFileKeys.java +++ b/src/main/java/com/couchbase/intellij/VirtualFileKeys.java @@ -12,6 +12,8 @@ public class VirtualFileKeys { public static final Key ID = new Key<>("ID"); public static final Key CAS = new Key<>("cas"); + public static final Key SEARCH_INDEX = new Key<>("searchindex"); + public static final Key CBL_CON_ID = new Key<>("cbl_conn_id"); public static final Key READ_ONLY = new Key<>("readonly"); diff --git a/src/main/java/com/couchbase/intellij/database/ActiveCluster.java b/src/main/java/com/couchbase/intellij/database/ActiveCluster.java index e1a48e23..825b6723 100644 --- a/src/main/java/com/couchbase/intellij/database/ActiveCluster.java +++ b/src/main/java/com/couchbase/intellij/database/ActiveCluster.java @@ -40,6 +40,8 @@ public class ActiveCluster implements CouchbaseClusterEntity { * Connection listeners are invoked only when a new cluster connection is established */ private final List newConnectionListener = new ArrayList<>(); + + private static List searchNodes; private Cluster cluster; private SavedCluster savedCluster; private String password; @@ -65,10 +67,6 @@ public class ActiveCluster implements CouchbaseClusterEntity { */ private static final List> clusterListeners = new ArrayList<>(); - /** - * Subscribers are invoked every time context is changed on this cluster - */ - private final List>> queryContextListeners = new ArrayList<>(); private Integer queryLimit = 200; protected ActiveCluster() { @@ -85,6 +83,10 @@ public static void subscribe(Consumer listener) { } } + public static void subscribeNew(Consumer listener) { + clusterListeners.add(listener); + } + public void setQueryContext(@Nullable QueryContext context) { this.queryContext.set(context); } @@ -103,6 +105,10 @@ public void registerNewConnectionListener(Runnable runnable) { this.newConnectionListener.add(runnable); } + public void deregisterNewConnectionListener(Runnable runnable) { + this.newConnectionListener.remove(runnable); + } + public Cluster get() { return cluster; } @@ -200,6 +206,14 @@ public void run(@NotNull ProgressIndicator indicator) { setVersion(overview.getNodes().get(0).getVersion() .substring(0, overview.getNodes().get(0).getVersion().indexOf('-'))); + if (hasSearchService()) { + searchNodes = new ArrayList<>(); + searchNodes.addAll(overview.getNodes().stream() + .filter(e -> e.getServices().contains("fts")) + .map(e -> e.getHostname().substring(0, e.getHostname().indexOf(":"))) + .collect(Collectors.toSet())); + } + //Notify Listeners that we connected to a new cluster. //NOTE: Only singletons can register here, otherwise we will get a memory leak CompletableFuture.runAsync(() -> { @@ -259,6 +273,7 @@ public void disconnect() { this.buckets = null; this.disconnectListener = null; this.permissions = null; + this.searchNodes = null; } public String getUsername() { @@ -455,6 +470,10 @@ public boolean hasQueryService() { return CBConfigUtil.hasQueryService(services); } + public boolean hasSearchService() { + return CBConfigUtil.hasSearchService(services); + } + public boolean isCapella() { return savedCluster != null && savedCluster.getUrl().contains("cloud.couchbase.com"); } @@ -473,4 +492,8 @@ public void setQueryLimit(Integer limit) { public @Nullable Integer getQueryLimit() { return this.queryLimit; } + + public static List searchNodes() { + return searchNodes; + } } \ No newline at end of file diff --git a/src/main/java/com/couchbase/intellij/database/DataLoader.java b/src/main/java/com/couchbase/intellij/database/DataLoader.java index 35921e19..16c32e2b 100644 --- a/src/main/java/com/couchbase/intellij/database/DataLoader.java +++ b/src/main/java/com/couchbase/intellij/database/DataLoader.java @@ -13,6 +13,8 @@ import com.couchbase.client.java.manager.collection.CollectionSpec; import com.couchbase.client.java.manager.collection.ScopeSpec; import com.couchbase.client.java.manager.query.QueryIndex; +import com.couchbase.client.java.manager.search.SearchIndex; +import com.couchbase.client.java.manager.search.SearchIndexManager; import com.couchbase.intellij.VirtualFileKeys; import com.couchbase.intellij.database.entity.CouchbaseCollection; import com.couchbase.intellij.persistence.*; @@ -51,6 +53,7 @@ import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; import static com.couchbase.intellij.VirtualFileKeys.READ_ONLY; @@ -119,7 +122,15 @@ public static void listScopes(DefaultMutableTreeNode parentNode, Tree tree) { for (ScopeSpec scopeSpec : scopes) { DefaultMutableTreeNode childNode = new DefaultMutableTreeNode(new ScopeNodeDescriptor(scopeSpec.name(), ActiveCluster.getInstance().getId(), bucketName)); - childNode.add(new DefaultMutableTreeNode(new LoadingNodeDescriptor())); + if (ActiveCluster.getInstance().hasSearchService()) { + DefaultMutableTreeNode search = new DefaultMutableTreeNode(new SearchNodeDescriptor(scopeSpec.name(), bucketName)); + search.add(new DefaultMutableTreeNode(new LoadingNodeDescriptor())); + childNode.add(search); + } + + DefaultMutableTreeNode collections = new DefaultMutableTreeNode(new CollectionsNodeDescriptor(scopeSpec.name(), bucketName)); + collections.add(new DefaultMutableTreeNode(new LoadingNodeDescriptor())); + childNode.add(collections); parentNode.add(childNode); } @@ -144,23 +155,25 @@ public static void listScopes(DefaultMutableTreeNode parentNode, Tree tree) { public static void listCollections(DefaultMutableTreeNode parentNode, Tree tree) { Object userObject = parentNode.getUserObject(); - if (userObject instanceof ScopeNodeDescriptor) { + if (userObject instanceof CollectionsNodeDescriptor) { CompletableFuture.runAsync(() -> { tree.setPaintBusy(true); try { parentNode.removeAllChildren(); - ScopeNodeDescriptor scopeDesc = (ScopeNodeDescriptor) userObject; - Map counts = CouchbaseRestAPI.getCollectionCounts(scopeDesc.getBucket(), scopeDesc.getText()); + CollectionsNodeDescriptor colsDesc = (CollectionsNodeDescriptor) userObject; + Map counts = CouchbaseRestAPI.getCollectionCounts(colsDesc.getBucket(), colsDesc.getScope()); + + List collections = ActiveCluster.getInstance().get().bucket(colsDesc.getBucket()).collections().getAllScopes().stream().filter(scope -> scope.name().equals(colsDesc.getScope())).flatMap(scope -> scope.collections().stream()).toList(); + + ((ScopeNodeDescriptor) ((DefaultMutableTreeNode) parentNode.getParent()).getUserObject()).setCounter(formatCount(collections.size())); - List collections = ActiveCluster.getInstance().get().bucket(scopeDesc.getBucket()).collections().getAllScopes().stream().filter(scope -> scope.name().equals(scopeDesc.getText())).flatMap(scope -> scope.collections().stream()).toList(); - scopeDesc.setCounter(formatCount(collections.size())); if (!collections.isEmpty()) { for (CollectionSpec spec : collections) { - String filter = QueryFiltersStorage.getInstance().getValue().getQueryFilter(ActiveCluster.getInstance().getId(), scopeDesc.getBucket(), scopeDesc.getText(), spec.name()); + String filter = QueryFiltersStorage.getInstance().getValue().getQueryFilter(ActiveCluster.getInstance().getId(), colsDesc.getBucket(), colsDesc.getScope(), spec.name()); - CollectionNodeDescriptor colNodeDesc = new CollectionNodeDescriptor(spec.name(), ActiveCluster.getInstance().getId(), scopeDesc.getBucket(), scopeDesc.getText(), filter); - colNodeDesc.setCounter(formatCount(counts.get(scopeDesc.getBucket() + "." + scopeDesc.getText() + "." + spec.name()))); + CollectionNodeDescriptor colNodeDesc = new CollectionNodeDescriptor(spec.name(), ActiveCluster.getInstance().getId(), colsDesc.getBucket(), colsDesc.getScope(), filter); + colNodeDesc.setCounter(formatCount(counts.get(colsDesc.getBucket() + "." + colsDesc.getScope() + "." + spec.name()))); DefaultMutableTreeNode childNode = new DefaultMutableTreeNode(colNodeDesc); childNode.add(new DefaultMutableTreeNode(new LoadingNodeDescriptor())); parentNode.add(childNode); @@ -648,4 +661,67 @@ public static String formatCount(Integer num) { return String.format("%.2fM", num / 1000000.0); } } + + + public static void listSearchIndexes(DefaultMutableTreeNode parentNode, Tree tree) { + Object userObject = parentNode.getUserObject(); + if (userObject instanceof SearchNodeDescriptor) { + CompletableFuture.runAsync(() -> { + tree.setPaintBusy(true); + parentNode.removeAllChildren(); + + try { + SearchNodeDescriptor searchDesc = (SearchNodeDescriptor) userObject; + + SearchIndexManager topSearch = ActiveCluster.getInstance().get().searchIndexes(); + List allIdx = topSearch.getAllIndexes(); + + List results = new ArrayList<>(); + + for (SearchIndex idx : allIdx) { + if (!searchDesc.getBucket().equals(idx.sourceName())) { + continue; + } + + Map mapping = (Map) idx.params().get("mapping"); + Map types = (Map) mapping.get("types"); + Map docConfig = (Map) idx.params().get("doc_config"); + String mode = docConfig.get("mode").toString(); + + if ("type_field".equals(mode) && "_default".equals(searchDesc.getScope())) { + results.add(idx); + } else if (!"type_field".equals(mode) && !types.keySet().stream() + .filter(e -> e.startsWith(searchDesc.getScope() + ".")) + .collect(Collectors.toSet()).isEmpty()) { + results.add(idx); + } + } + + if (!results.isEmpty()) { + for (SearchIndex searchIndex : results) { + + String fileName = searchIndex.name() + ".json"; + VirtualFile virtualFile = new LightVirtualFile(fileName, JsonFileType.INSTANCE, searchIndex.toJson()); + virtualFile.putUserData(READ_ONLY, "true"); + + SearchIndexNodeDescriptor node = new SearchIndexNodeDescriptor(searchIndex.name(), searchDesc.getBucket(), searchDesc.getScope(), fileName, virtualFile); + DefaultMutableTreeNode jsonFileNode = new DefaultMutableTreeNode(node); + parentNode.add(jsonFileNode); + } + } else { + parentNode.add(new DefaultMutableTreeNode(new NoResultsNodeDescriptor())); + } + ApplicationManager.getApplication().invokeLater(() -> { + ((DefaultTreeModel) tree.getModel()).nodeStructureChanged(parentNode); + tree.setPaintBusy(false); + }); + + } catch (Exception e) { + Log.error(e); + } finally { + tree.setPaintBusy(false); + } + }); + } + } } diff --git a/src/main/java/com/couchbase/intellij/searchworkbench/CBSJsonFileType.java b/src/main/java/com/couchbase/intellij/searchworkbench/CBSJsonFileType.java new file mode 100644 index 00000000..158d7412 --- /dev/null +++ b/src/main/java/com/couchbase/intellij/searchworkbench/CBSJsonFileType.java @@ -0,0 +1,43 @@ +package com.couchbase.intellij.searchworkbench; + +import com.intellij.json.JsonLanguage; +import com.intellij.openapi.fileTypes.LanguageFileType; +import com.intellij.openapi.util.IconLoader; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; + +public class CBSJsonFileType extends LanguageFileType { + public static final CBSJsonFileType INSTANCE = new CBSJsonFileType(); + + public static final Icon FILE = IconLoader.getIcon("/assets/cbs.png", CBSJsonFileType.class); + + private CBSJsonFileType() { + super(JsonLanguage.INSTANCE); + } + + @NotNull + @Override + public String getName() { + return "Couchbase Search Query"; + } + + @NotNull + @Override + public String getDescription() { + return "JSON query for couchbase search"; + } + + @NotNull + @Override + public String getDefaultExtension() { + return "cbs.json"; + } + + @Nullable + @Override + public Icon getIcon() { + return FILE; + } +} diff --git a/src/main/java/com/couchbase/intellij/searchworkbench/SearchFileEditor.java b/src/main/java/com/couchbase/intellij/searchworkbench/SearchFileEditor.java new file mode 100644 index 00000000..afbd17a3 --- /dev/null +++ b/src/main/java/com/couchbase/intellij/searchworkbench/SearchFileEditor.java @@ -0,0 +1,375 @@ +package com.couchbase.intellij.searchworkbench; + + +import com.couchbase.intellij.database.ActiveCluster; +import com.intellij.openapi.actionSystem.*; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.editor.EditorFactory; +import com.intellij.openapi.fileEditor.*; +import com.intellij.openapi.fileEditor.impl.text.TextEditorProvider; +import com.intellij.openapi.progress.ProgressIndicator; +import com.intellij.openapi.progress.ProgressManager; +import com.intellij.openapi.progress.Task; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.ComboBox; +import com.intellij.openapi.ui.Messages; +import com.intellij.openapi.util.IconLoader; +import com.intellij.openapi.util.Key; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.pom.Navigatable; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.ActionListener; +import java.beans.PropertyChangeListener; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.stream.Collectors; + +public class SearchFileEditor implements FileEditor, TextEditor { + private final EditorWrapper queryEditor; + private final VirtualFile file; + private final Project project; + private final Map, Object> data = new HashMap<>(); + JPanel panel; + private JComponent component; + private JPanel topPanel; + + private boolean isExecutingQuery = false; + private AnAction executeAction; + private AnAction cancelAction; + + private Runnable newConnectionListener; + + private BlockingQueue queryExecutionChannel; + private ComboBox bucketCombo; + + private ComboBox idxCombo; + + private String selectedBucket; + + private String selectedIdx; + + SearchFileEditor(Project project, VirtualFile file, String selectedBucket, String selectedIdx) { + this.file = file; + this.project = project; + this.selectedBucket = selectedBucket; + this.selectedIdx = selectedIdx; + this.queryEditor = new EditorWrapper(null, (TextEditor) TextEditorProvider.getInstance().createEditor(project, file)); + this.panel = new JPanel(new BorderLayout()); + init(); + } + + @NotNull + @Override + public VirtualFile getFile() { + return file; + } + + @NotNull + @Override + public JComponent getComponent() { + return this.component; + } + + public void init() { + + buildToolbar(); + panel.add(queryEditor.getComponent(), BorderLayout.CENTER); + queryEditor.getContentComponent().requestFocusInWindow(); + component = panel; + } + + private void buildToolbar() { + DefaultActionGroup executeGroup = new DefaultActionGroup(); + + Icon executeIcon = IconLoader.getIcon("/assets/icons/play.svg", SearchFileEditor.class); + Icon cancelIcon = IconLoader.getIcon("/assets/icons/cancel.svg", SearchFileEditor.class); + + executeAction = new AnAction("Execute", "Execute the query statement in the editor", executeIcon) { + @Override + public void actionPerformed(@NotNull AnActionEvent e) { + if (!isSameConnection()) { + return; + } + + queryExecutionChannel = new LinkedBlockingQueue<>(1); + if (!isExecutingQuery) { + isExecutingQuery = true; + + cancelAction = new AnAction("Cancel", "Cancel query execution", cancelIcon) { + @Override + public void actionPerformed(@NotNull AnActionEvent e) { + if (queryExecutionChannel.peek() == null) { + try { + queryExecutionChannel.put(true); + } catch (InterruptedException ex) { + throw new RuntimeException(ex); + } + } + executeGroup.replaceAction(this, executeAction); + isExecutingQuery = false; + } + }; + + executeAction.registerCustomShortcutSet(CommonShortcuts.CTRL_ENTER, queryEditor.getComponent()); + executeGroup.replaceAction(this, cancelAction); + isExecutingQuery = true; + + ProgressManager.getInstance().run(new Task.Backgroundable(project, "Running SQL++ query", true) { + @Override + public void run(@NotNull ProgressIndicator indicator) { + + ApplicationManager.getApplication().invokeLater(() -> { + boolean query = SearchQueryExecutor.executeQuery(queryExecutionChannel, + bucketCombo.getSelectedItem() == null ? null : bucketCombo.getSelectedItem().toString(), + idxCombo.getSelectedItem() == null ? null : idxCombo.getSelectedItem().toString(), + queryEditor.getDocument().getText(), + project); + executeGroup.replaceAction(cancelAction, executeAction); + isExecutingQuery = false; + }); + } + }); + } + } + }; + + executeGroup.add(executeAction); + + ActionToolbar executeToolbar = ActionManager.getInstance().createActionToolbar(ActionPlaces.TOOLBAR, executeGroup, true); + executeToolbar.setTargetComponent(queryEditor.getComponent()); + + JPanel contextPanel = new JPanel(new FlowLayout()); + JLabel conLabel = new JLabel("Bucket:"); + conLabel.setFont(conLabel.getFont().deriveFont(10.0f)); + contextPanel.add(conLabel); + + bucketCombo = new ComboBox<>(); + ActiveCluster.getInstance().get().buckets().getAllBuckets().keySet().forEach(bucketCombo::addItem); + bucketCombo.setFont(bucketCombo.getFont().deriveFont(10f)); + Dimension maxSize = new Dimension(250, 29); + bucketCombo.setMaximumSize(maxSize); + contextPanel.add(bucketCombo); + + JLabel idxLabel = new JLabel("Index:"); + idxLabel.setFont(conLabel.getFont().deriveFont(10.0f)); + contextPanel.add(idxLabel); + + idxCombo = new ComboBox<>(); + + + if (selectedBucket != null && selectedIdx != null) { + bucketCombo.setSelectedItem(selectedBucket); + getIndexByBucket(selectedBucket).forEach(idxCombo::addItem); + idxCombo.setSelectedItem(selectedIdx); + idxCombo.setEnabled(true); + + } else { + bucketCombo.setSelectedItem(null); + idxCombo.setSelectedItem(null); + idxCombo.setEnabled(false); + } + + idxCombo.setFont(bucketCombo.getFont().deriveFont(10f)); + + Dimension initialSize = new Dimension(250, 29); + + idxCombo.setMaximumSize(maxSize); + idxCombo.setPreferredSize(initialSize); + contextPanel.add(idxCombo); + + contextPanel.revalidate(); + topPanel = new JPanel(new BorderLayout()); + + ActionListener bucketComboListener = e -> { + idxCombo.removeAllItems(); + + if (bucketCombo.getSelectedItem() == null) { + idxCombo.setEnabled(false); + return; + } + String bucketId = (String) bucketCombo.getSelectedItem(); + + getIndexByBucket(bucketId).forEach(idxCombo::addItem); + idxCombo.setSelectedItem(null); + idxCombo.setEnabled(true); + }; + bucketCombo.addActionListener(bucketComboListener); + + + JPanel leftPanel = new JPanel(new BorderLayout()); + leftPanel.add(executeToolbar.getComponent(), BorderLayout.WEST); + topPanel.add(leftPanel, BorderLayout.WEST); + topPanel.add(contextPanel, BorderLayout.EAST); + + panel.add(topPanel, BorderLayout.NORTH); + + + Runnable newConnectionListener = () -> { + bucketCombo.removeAllItems(); + bucketCombo.removeActionListener(bucketComboListener); + ActiveCluster.getInstance().get().buckets().getAllBuckets().keySet().forEach(bucketCombo::addItem); + bucketCombo.setSelectedItem(null); + bucketCombo.addActionListener(bucketComboListener); + + idxCombo.removeAllItems(); + idxCombo.setEnabled(false); + bucketCombo.revalidate(); + idxCombo.revalidate(); + }; + ActiveCluster.getInstance().registerNewConnectionListener(newConnectionListener); + } + + @Override + public JComponent getPreferredFocusedComponent() { + return queryEditor.getComponent(); + } + + @NotNull + @Override + public String getName() { + return "Custom SQL File Editor"; + } + + @Override + public void setState(@NotNull FileEditorState state) { + // Do nothing + } + + @Override + public void dispose() { + + if (newConnectionListener != null) { + ActiveCluster.getInstance().deregisterNewConnectionListener(newConnectionListener); + } + queryEditor.release(); + } + + @Override + public boolean isModified() { + return false; + } + + @Override + public boolean isValid() { + return true; + } + + @Override + public void selectNotify() { + } + + @Override + public void deselectNotify() { + } + + @Override + public void addPropertyChangeListener(@NotNull PropertyChangeListener listener) { + } + + @Override + public void removePropertyChangeListener(@NotNull PropertyChangeListener listener) { + } + + @Override + public @Nullable FileEditorLocation getCurrentLocation() { + return null; + } + + @NotNull + @Override + public FileEditorState getState(@NotNull FileEditorStateLevel level) { + return (otherState, level1) -> false; + } + + @Nullable + @Override + public T getUserData(@NotNull Key key) { + @SuppressWarnings("unchecked") T result = (T) data.get(key); + + return result; + } + + @Override + public void putUserData(@NotNull Key key, @Nullable T value) { + if (value == null) { + data.remove(key); + } else { + data.put(key, value); + } + } + + private boolean isSameConnection() { + if (ActiveCluster.getInstance().get() == null) { + ApplicationManager.getApplication().invokeLater(() -> Messages.showErrorDialog("There is no active connection.", "Workbench Error")); + return false; + } + return true; + } + + + @Override + public @NotNull Editor getEditor() { + if (queryEditor.textEditor != null) { + return queryEditor.textEditor.getEditor(); + } else { + return queryEditor.viewer; + } + } + + @Override + public boolean canNavigateTo(@NotNull Navigatable navigatable) { + return false; + } + + @Override + public void navigateTo(@NotNull Navigatable navigatable) { + + } + + static class EditorWrapper { + private final Editor viewer; + private final TextEditor textEditor; + + public EditorWrapper(Editor viewer, TextEditor textEditor) { + this.textEditor = textEditor; + this.viewer = viewer; + } + + public JComponent getComponent() { + return textEditor == null ? viewer.getComponent() : textEditor.getComponent(); + } + + public JComponent getContentComponent() { + return textEditor == null ? viewer.getContentComponent() : textEditor.getEditor().getContentComponent(); + } + + public Document getDocument() { + return textEditor == null ? viewer.getDocument() : textEditor.getEditor().getDocument(); + } + + public void release() { + EditorFactory.getInstance().releaseEditor(viewer); + + } + } + + + private List getIndexByBucket(String bucketName) { + return ActiveCluster.getInstance().get().searchIndexes() + .getAllIndexes().stream() + .filter(e -> bucketName.equals(e.sourceName())) + .map(e -> e.name()) + .sorted() + .collect(Collectors.toList()); + } + +} \ No newline at end of file diff --git a/src/main/java/com/couchbase/intellij/searchworkbench/SearchFileEditorProvider.java b/src/main/java/com/couchbase/intellij/searchworkbench/SearchFileEditorProvider.java new file mode 100644 index 00000000..8478fa1b --- /dev/null +++ b/src/main/java/com/couchbase/intellij/searchworkbench/SearchFileEditorProvider.java @@ -0,0 +1,36 @@ +package com.couchbase.intellij.searchworkbench; + +import com.couchbase.intellij.VirtualFileKeys; +import com.intellij.openapi.fileEditor.FileEditor; +import com.intellij.openapi.fileEditor.FileEditorPolicy; +import com.intellij.openapi.fileEditor.FileEditorProvider; +import com.intellij.openapi.project.DumbAware; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.VirtualFile; +import org.jetbrains.annotations.NotNull; + +public class SearchFileEditorProvider implements FileEditorProvider, DumbAware { + + @Override + public boolean accept(@NotNull Project project, @NotNull VirtualFile file) { + return file.getName().endsWith(".cbs.json"); + } + + @NotNull + @Override + public FileEditor createEditor(@NotNull Project project, @NotNull VirtualFile file) { + return new SearchFileEditor(project, file, file.getUserData(VirtualFileKeys.BUCKET), file.getUserData(VirtualFileKeys.SEARCH_INDEX)); + } + + @NotNull + @Override + public String getEditorTypeId() { + return "couchbase-search-editor"; + } + + @NotNull + @Override + public FileEditorPolicy getPolicy() { + return FileEditorPolicy.HIDE_DEFAULT_EDITOR; + } +} diff --git a/src/main/java/com/couchbase/intellij/searchworkbench/SearchQueryExecutor.java b/src/main/java/com/couchbase/intellij/searchworkbench/SearchQueryExecutor.java new file mode 100644 index 00000000..27e81f12 --- /dev/null +++ b/src/main/java/com/couchbase/intellij/searchworkbench/SearchQueryExecutor.java @@ -0,0 +1,141 @@ +package com.couchbase.intellij.searchworkbench; + +import com.couchbase.client.java.json.JsonArray; +import com.couchbase.client.java.json.JsonObject; +import com.couchbase.intellij.database.ActiveCluster; +import com.couchbase.intellij.tree.overview.apis.CouchbaseRestAPI; +import com.couchbase.intellij.workbench.Log; +import com.couchbase.intellij.workbench.QueryResultToolWindowFactory; +import com.couchbase.intellij.workbench.QueryResultUtil; +import com.couchbase.intellij.workbench.error.CouchbaseQueryErrorUtil; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.application.ModalityState; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.Messages; +import com.intellij.openapi.wm.ToolWindow; +import com.intellij.openapi.wm.ToolWindowManager; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CompletableFuture; + +public class SearchQueryExecutor { + + public static Boolean executeQuery(BlockingQueue queue, String bucket, String indexName, String query, Project project) { + if (query == null || query.trim().isEmpty()) { + return false; + } + query = query.trim(); + + if (ActiveCluster.getInstance().get() == null) { + Messages.showMessageDialog("There is no active connection to run this query", "Couchbase Plugin Error", Messages.getErrorIcon()); + return false; + } + + if (bucket == null || bucket.trim().isEmpty()) { + Messages.showMessageDialog("Please select a bucket before running the query", "Couchbase Plugin Error", Messages.getErrorIcon()); + return false; + } + + if (indexName == null || indexName.trim().isEmpty()) { + Messages.showMessageDialog("Please select a search index before running the query", "Couchbase Plugin Error", Messages.getErrorIcon()); + return false; + } + + if (!ActiveCluster.getInstance().get().buckets().getAllBuckets().containsKey(bucket)) { + Messages.showMessageDialog("There is no bucket \"" + bucket + "\" in the current cluster", "Couchbase Plugin Error", Messages.getErrorIcon()); + return false; + } + + getOutputWindow(project).setStatusAsLoading(); + + long start = 0; + try { + start = System.currentTimeMillis(); + CompletableFuture futureResult = null; + + if (indexName.contains(".")) { + String[] split = indexName.split("\\."); + futureResult = CouchbaseRestAPI.callFTS(true, bucket, split[1], split[2], query); + } else { + futureResult = CouchbaseRestAPI.callFTS(false, bucket, "_default", indexName, query); + } + + + while (!futureResult.isDone()) { + if (queue.peek() != null) { + queue.poll(); + futureResult.cancel(true); + getOutputWindow(project).setStatusAsCanceled(); + } + } + String result = futureResult.get(); + + long end = System.currentTimeMillis(); + + JsonObject jsonObject = JsonObject.fromJson(result); + List metricsList = new ArrayList<>(); + if (!jsonObject.containsKey("error")) { + metricsList.add(end - start + " MS"); + metricsList.add(jsonObject.getInt("took") / 1000 + " MS"); + metricsList.add("-"); + metricsList.add(null); + metricsList.add(String.valueOf(jsonObject.getInt("total_hits"))); + metricsList.add(QueryResultUtil.getSizeText(result.length())); + + List resultList = new ArrayList<>(); + JsonArray jsonArray = jsonObject.getArray("hits"); + for (int i = 0; i < jsonArray.size(); i++) { + resultList.add((JsonObject) jsonArray.get(i)); + } + + getOutputWindow(project).updateQueryStats(metricsList, resultList, null, null, false); + + } else { + metricsList.add("-"); + metricsList.add("-"); + metricsList.add("-"); + metricsList.add("-"); + metricsList.add("-"); + metricsList.add("-"); + + getOutputWindow(project).updateQueryStats(Arrays.asList((end - start) + " MS", "-", "-", "-", "-", "-"), null, + CouchbaseQueryErrorUtil.parseQueryError(jsonObject.getString("error")), null, false); + + } + + } catch (Exception e) { + long end = System.currentTimeMillis(); + getOutputWindow(project).updateQueryStats(Arrays.asList((end - start) + " MS", "-", "-", "-", "-", "-"), null, + CouchbaseQueryErrorUtil.parseQueryError("An error occurred while executing the query: " + e.getMessage()), null, false); + Log.error(e); + } + + return true; + } + + + private static ToolWindow toolWindow; + private static QueryResultToolWindowFactory resultWindow; + + public static QueryResultToolWindowFactory getOutputWindow(Project project) { + if (toolWindow == null) { + ToolWindowManager toolWindowManager = ToolWindowManager.getInstance(project); + toolWindow = toolWindowManager.getToolWindow("Couchbase Output"); + } + + if (toolWindow != null) { + ApplicationManager.getApplication().invokeLater(() -> { + toolWindow.show(); + }, ModalityState.any()); + } + + if (resultWindow == null) { + resultWindow = QueryResultToolWindowFactory.instance; + } + + return resultWindow; + } +} diff --git a/src/main/java/com/couchbase/intellij/tree/CouchbaseWindowContent.java b/src/main/java/com/couchbase/intellij/tree/CouchbaseWindowContent.java index a8ddea09..aa585549 100644 --- a/src/main/java/com/couchbase/intellij/tree/CouchbaseWindowContent.java +++ b/src/main/java/com/couchbase/intellij/tree/CouchbaseWindowContent.java @@ -1,5 +1,6 @@ package com.couchbase.intellij.tree; +import com.couchbase.intellij.DocumentFormatter; import com.couchbase.intellij.database.ActiveCluster; import com.couchbase.intellij.database.DataLoader; import com.couchbase.intellij.persistence.SavedCluster; @@ -121,13 +122,22 @@ public void mousePressed(MouseEvent e) { } else { Log.debug("virtual file is null"); } - } else if (userObject instanceof IndexNodeDescriptor) { - IndexNodeDescriptor descriptor = (IndexNodeDescriptor) userObject; + } else if (userObject instanceof IndexNodeDescriptor descriptor) { VirtualFile virtualFile = descriptor.getVirtualFile(); if (virtualFile != null) { OpenFileDescriptor fileDescriptor = new OpenFileDescriptor(project, virtualFile); FileEditorManager.getInstance(project).openEditor(fileDescriptor, true); + } else { + Log.debug("virtual file is null"); + } + } else if (userObject instanceof SearchIndexNodeDescriptor descriptor) { + VirtualFile virtualFile = descriptor.getVirtualFile(); + if (virtualFile != null) { + DocumentFormatter.formatFile(project, virtualFile); + OpenFileDescriptor fileDescriptor = new OpenFileDescriptor(project, virtualFile); + FileEditorManager.getInstance(project).openEditor(fileDescriptor, true); + } else { Log.debug("virtual file is null"); } diff --git a/src/main/java/com/couchbase/intellij/tree/TreeExpandListener.java b/src/main/java/com/couchbase/intellij/tree/TreeExpandListener.java index 2d731f1b..2fa7e9b4 100644 --- a/src/main/java/com/couchbase/intellij/tree/TreeExpandListener.java +++ b/src/main/java/com/couchbase/intellij/tree/TreeExpandListener.java @@ -19,7 +19,7 @@ public static void handle(Tree tree, TreeExpansionEvent event) { DataLoader.listBuckets(expandedTreeNode, tree); } else if (expandedTreeNode.getUserObject() instanceof BucketNodeDescriptor) { DataLoader.listScopes(expandedTreeNode, tree); - } else if (expandedTreeNode.getUserObject() instanceof ScopeNodeDescriptor) { + } else if (expandedTreeNode.getUserObject() instanceof CollectionsNodeDescriptor) { DataLoader.listCollections(expandedTreeNode, tree); } else if (expandedTreeNode.getUserObject() instanceof CollectionNodeDescriptor) { DataLoader.listDocuments(expandedTreeNode, tree, 0); @@ -27,8 +27,12 @@ public static void handle(Tree tree, TreeExpansionEvent event) { DataLoader.showSchema(expandedTreeNode, (DefaultTreeModel) tree.getModel(), tree); } else if (expandedTreeNode.getUserObject() instanceof TooltipNodeDescriptor) { // Do Nothing + } else if (expandedTreeNode.getUserObject() instanceof ScopeNodeDescriptor) { + // Do Nothing } else if (expandedTreeNode.getUserObject() instanceof IndexesNodeDescriptor) { DataLoader.listIndexes(expandedTreeNode, tree); + } else if (expandedTreeNode.getUserObject() instanceof SearchNodeDescriptor) { + DataLoader.listSearchIndexes(expandedTreeNode, tree); } else { throw new UnsupportedOperationException("Not implemented yet"); } diff --git a/src/main/java/com/couchbase/intellij/tree/TreeRightClickListener.java b/src/main/java/com/couchbase/intellij/tree/TreeRightClickListener.java index 86f6395a..dc5121c4 100644 --- a/src/main/java/com/couchbase/intellij/tree/TreeRightClickListener.java +++ b/src/main/java/com/couchbase/intellij/tree/TreeRightClickListener.java @@ -1,7 +1,9 @@ package com.couchbase.intellij.tree; import com.couchbase.client.java.manager.collection.CollectionSpec; +import com.couchbase.client.java.manager.search.SearchIndexManager; import com.couchbase.intellij.DocumentFormatter; +import com.couchbase.intellij.VirtualFileKeys; import com.couchbase.intellij.database.ActiveCluster; import com.couchbase.intellij.database.DataLoader; import com.couchbase.intellij.database.InferHelper; @@ -61,6 +63,8 @@ public class TreeRightClickListener { + private static int searchWorkbenchCounter = 0; + public static void handle(Tree tree, Project project, JPanel toolbarPanel, MouseEvent e, DefaultMutableTreeNode clickedNode) { Object userObject = clickedNode.getUserObject(); int row = tree.getClosestRowForLocation(e.getX(), e.getY()); @@ -76,6 +80,8 @@ public static void handle(Tree tree, Project project, JPanel toolbarPanel, Mouse handleCollectionRightClick(project, e, clickedNode, (CollectionNodeDescriptor) userObject, tree); } else if (userObject instanceof FileNodeDescriptor) { handleDocumentRightClick(project, e, clickedNode, (FileNodeDescriptor) userObject, tree); + } else if (userObject instanceof SearchIndexNodeDescriptor) { + handleSearchIndexRightClick(project, e, clickedNode, (SearchIndexNodeDescriptor) userObject, tree); } else if (userObject instanceof IndexNodeDescriptor) { handleIndexRightClick(project, e, clickedNode, (IndexNodeDescriptor) userObject, tree); } else if (userObject instanceof SchemaDataNodeDescriptor) { @@ -788,4 +794,82 @@ public void actionPerformed(@NotNull AnActionEvent e) { showPopup(e, tree, actionGroup); } + + + private static void handleSearchIndexRightClick(Project project, MouseEvent e, DefaultMutableTreeNode clickedNode, SearchIndexNodeDescriptor idx, Tree tree) { + DefaultActionGroup actionGroup = new DefaultActionGroup(); + + + AnAction newWorkbench = new AnAction("New Search Workbench") { + @Override + public void actionPerformed(@NotNull AnActionEvent e) { + ApplicationManager.getApplication().runWriteAction(() -> { + try { + Project project = e.getProject(); + searchWorkbenchCounter++; + String fileName = "search" + searchWorkbenchCounter + ".cbs.json"; + String fileContent = """ + { + "query": { + "query": "your_query_here" + }, + "fields": ["*"] + } + """; + VirtualFile virtualFile = new LightVirtualFile(fileName, FileTypeManager.getInstance().getFileTypeByExtension("cbs.json"), fileContent); + virtualFile.putUserData(VirtualFileKeys.BUCKET, idx.getBucket()); + virtualFile.putUserData(VirtualFileKeys.SEARCH_INDEX, idx.getIndexName()); + FileEditorManager fileEditorManager = FileEditorManager.getInstance(project); + fileEditorManager.openFile(virtualFile, true); + } catch (Exception ex) { + Log.error(ex); + ex.printStackTrace(); + } + }); + } + }; + + actionGroup.add(newWorkbench); + + if (!ActiveCluster.getInstance().isReadOnlyMode()) { + AnAction deleteDoc = new AnAction("Delete Index") { + @Override + public void actionPerformed(@NotNull AnActionEvent e) { + int result = Messages.showYesNoDialog("Are you sure you want to delete the index " + idx.getIndexName() + "?", "Delete Document", Messages.getQuestionIcon()); + if (result != Messages.YES) { + return; + } + + try { + SearchIndexManager idxManager = ActiveCluster.getInstance().get().searchIndexes(); + idxManager.dropIndex(idx.getIndexName()); + + if (idx.getVirtualFile() != null) { + try { + FileEditorManager fileEditorManager = FileEditorManager.getInstance(project); + fileEditorManager.closeFile(idx.getVirtualFile()); + } catch (Exception ex) { + ex.printStackTrace(); + Log.debug("Could not close the file", ex); + } + } + + DefaultMutableTreeNode parentNode = (DefaultMutableTreeNode) clickedNode.getParent(); + if (parentNode != null) { + ((DefaultTreeModel) tree.getModel()).removeNodeFromParent(clickedNode); + } + ((DefaultTreeModel) tree.getModel()).nodeStructureChanged(parentNode); + } catch (Exception ex) { + ex.printStackTrace(); + Log.error("An error occurred while trying to delete the index " + idx.getIndexName(), ex); + Messages.showErrorDialog("Could not delete the index. Please check the logs for more.", "Couchbase Plugin Error"); + } + + } + }; + actionGroup.add(deleteDoc); + } + + showPopup(e, tree, actionGroup); + } } diff --git a/src/main/java/com/couchbase/intellij/tree/cblite/nodes/CBLScopeNodeDescriptor.java b/src/main/java/com/couchbase/intellij/tree/cblite/nodes/CBLScopeNodeDescriptor.java index 7b88777f..a882fad1 100644 --- a/src/main/java/com/couchbase/intellij/tree/cblite/nodes/CBLScopeNodeDescriptor.java +++ b/src/main/java/com/couchbase/intellij/tree/cblite/nodes/CBLScopeNodeDescriptor.java @@ -6,6 +6,6 @@ public class CBLScopeNodeDescriptor extends NodeDescriptor { public CBLScopeNodeDescriptor(String text) { - super(text, IconLoader.getIcon("/assets/icons/collections.svg", CBLScopeNodeDescriptor.class)); + super(text, IconLoader.getIcon("/assets/icons/scope.svg", CBLScopeNodeDescriptor.class)); } } diff --git a/src/main/java/com/couchbase/intellij/tree/node/CollectionsNodeDescriptor.java b/src/main/java/com/couchbase/intellij/tree/node/CollectionsNodeDescriptor.java new file mode 100644 index 00000000..dbeb4bcb --- /dev/null +++ b/src/main/java/com/couchbase/intellij/tree/node/CollectionsNodeDescriptor.java @@ -0,0 +1,21 @@ +package com.couchbase.intellij.tree.node; + +import com.intellij.openapi.util.IconLoader; +import lombok.Getter; +import lombok.Setter; + +public class CollectionsNodeDescriptor extends CounterNodeDescriptor { + + @Setter + @Getter + private String bucket; + + @Getter + private String scope; + + public CollectionsNodeDescriptor(String scope, String bucket) { + super("Collections", IconLoader.getIcon("/assets/icons/collections.svg", CollectionsNodeDescriptor.class)); + this.bucket = bucket; + this.scope = scope; + } +} diff --git a/src/main/java/com/couchbase/intellij/tree/node/ScopeNodeDescriptor.java b/src/main/java/com/couchbase/intellij/tree/node/ScopeNodeDescriptor.java index abdc0c84..6264eec3 100644 --- a/src/main/java/com/couchbase/intellij/tree/node/ScopeNodeDescriptor.java +++ b/src/main/java/com/couchbase/intellij/tree/node/ScopeNodeDescriptor.java @@ -8,7 +8,7 @@ public class ScopeNodeDescriptor extends CounterNodeDescriptor { private String bucket; public ScopeNodeDescriptor(String name, String connectionId, String bucket) { - super(name, IconLoader.getIcon("/assets/icons/collections.svg", ScopeNodeDescriptor.class)); + super(name, IconLoader.getIcon("/assets/icons/scope.svg", ScopeNodeDescriptor.class)); this.connectionId = connectionId; this.bucket = bucket; } diff --git a/src/main/java/com/couchbase/intellij/tree/node/SearchIndexNodeDescriptor.java b/src/main/java/com/couchbase/intellij/tree/node/SearchIndexNodeDescriptor.java new file mode 100644 index 00000000..6f104a17 --- /dev/null +++ b/src/main/java/com/couchbase/intellij/tree/node/SearchIndexNodeDescriptor.java @@ -0,0 +1,36 @@ +package com.couchbase.intellij.tree.node; + +import com.intellij.json.JsonFileType; +import com.intellij.openapi.vfs.VirtualFile; +import lombok.Getter; + +public class SearchIndexNodeDescriptor extends NodeDescriptor { + + @Getter + private final String bucket; + + @Getter + private final String scope; + + @Getter + private final String indexName; + private VirtualFile virtualFile; + + public SearchIndexNodeDescriptor(String indexName, String bucket, String scope, String name, VirtualFile virtualFile) { + super(name, JsonFileType.INSTANCE.getIcon()); + this.virtualFile = virtualFile; + this.bucket = bucket; + this.scope = scope; + this.indexName = indexName; + } + + public VirtualFile getVirtualFile() { + return virtualFile; + } + + public void setVirtualFile(VirtualFile virtualFile) { + this.virtualFile = virtualFile; + } + + +} diff --git a/src/main/java/com/couchbase/intellij/tree/node/SearchNodeDescriptor.java b/src/main/java/com/couchbase/intellij/tree/node/SearchNodeDescriptor.java new file mode 100644 index 00000000..87d5b0fd --- /dev/null +++ b/src/main/java/com/couchbase/intellij/tree/node/SearchNodeDescriptor.java @@ -0,0 +1,21 @@ +package com.couchbase.intellij.tree.node; + +import com.intellij.icons.AllIcons; +import lombok.Getter; +import lombok.Setter; + +public class SearchNodeDescriptor extends CounterNodeDescriptor { + + @Setter + @Getter + private String bucket; + + @Getter + private String scope; + + public SearchNodeDescriptor(String scope, String bucket) { + super("Search", AllIcons.Actions.ShortcutFilter); + this.bucket = bucket; + this.scope = scope; + } +} diff --git a/src/main/java/com/couchbase/intellij/tree/overview/apis/CouchbaseRestAPI.java b/src/main/java/com/couchbase/intellij/tree/overview/apis/CouchbaseRestAPI.java index 2d7cfe17..2de7a9ae 100644 --- a/src/main/java/com/couchbase/intellij/tree/overview/apis/CouchbaseRestAPI.java +++ b/src/main/java/com/couchbase/intellij/tree/overview/apis/CouchbaseRestAPI.java @@ -14,10 +14,8 @@ import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; -import java.io.BufferedReader; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStream; +import java.io.*; +import java.lang.reflect.Type; import java.net.HttpURLConnection; import java.net.URL; import java.util.Base64; @@ -25,22 +23,24 @@ import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.stream.Collectors; -import java.lang.reflect.Type; public class CouchbaseRestAPI { - public static String getMetaDocument(String bucket, String scope, String collection, String id ) throws Exception { + public static String getMetaDocument(String bucket, String scope, String collection, String id) throws Exception { String result = callSingleEndpoint((ActiveCluster.getInstance().isSSLEnabled() ? "18091" : "8091") + "/pools/default/buckets/" - +bucket+"/scopes/"+scope+"/collections/"+collection+"/docs/"+id, ActiveCluster.getInstance().getClusterURL()); + + bucket + "/scopes/" + scope + "/collections/" + collection + "/docs/" + id, ActiveCluster.getInstance().getClusterURL()); JsonObject object = JsonObject.fromJson(result); object.removeKey("json"); return object.toString(); } - public static List listKVDocuments(String bucket, String scope, String collection, int skip, int limit ) throws Exception { + + public static List listKVDocuments(String bucket, String scope, String collection, int skip, int limit) throws Exception { String result = callSingleEndpoint((ActiveCluster.getInstance().isSSLEnabled() ? "18091" : "8091") + "/pools/default/buckets/" - +bucket+"/scopes/"+scope+"/collections/"+collection+"/docs?skip="+skip+"&limit="+limit+"&include_doc=false", + + bucket + "/scopes/" + scope + "/collections/" + collection + "/docs?skip=" + skip + "&limit=" + limit + "&include_doc=false", ActiveCluster.getInstance().getClusterURL()); Gson gson = new Gson(); @@ -50,6 +50,7 @@ public static List listKVDocuments(String bucket, String scope, String c .map(KVRow::getId) .collect(Collectors.toList()); } + public static Map getCollectionCounts(String bucket, String scope) throws Exception { String payload = "[\n" + @@ -64,11 +65,11 @@ public static Map getCollectionCounts(String bucket, String sco " },\n" + " {\n" + " \"label\": \"bucket\",\n" + - " \"value\": \""+bucket+"\"\n" + + " \"value\": \"" + bucket + "\"\n" + " },\n" + " {\n" + " \"label\": \"scope\",\n" + - " \"value\": \""+scope+"\"\n" + + " \"value\": \"" + scope + "\"\n" + " }\n" + " ],\n" + " \"nodesAggregation\": \"sum\"\n" + @@ -185,18 +186,27 @@ private static String callGetEndpoint(String endpoint, String serverURL) throws return callEndpoint(true, endpoint, serverURL, null); } - private static String callEndpoint(boolean isGet, String endpoint, String serverURL, String data) throws Exception { + public static CompletableFuture callFTS(boolean isScoped, String bucket, String scope, String indexName, String query) throws Exception { + String endpoint = (ActiveCluster.getInstance().isSSLEnabled() ? "18094" : "8094"); + if (isScoped) { + endpoint += "/api/bucket/" + bucket + "/scope/" + scope + "/index/" + indexName + "/query"; + } else { + endpoint += "/api/index/" + indexName + "/query"; + } + return callEndpointAllowErrors(false, endpoint, ActiveCluster.searchNodes().get(0), query); + } + + private static HttpURLConnection setupConnection(boolean isGet, String endpoint, String serverURL, String data) throws Exception { String userpass = ActiveCluster.getInstance().getUsername() + ":" + ActiveCluster.getInstance().getPassword(); String basicAuth = "Basic " + new String(Base64.getEncoder().encode(userpass.getBytes())); String urlContent = (ActiveCluster.getInstance().isSSLEnabled() ? "https://" : "http://") + serverURL + ":" + endpoint; URL url = new URL(urlContent); - InputStream stream; - HttpURLConnection conn = null; + HttpURLConnection conn; if (ActiveCluster.getInstance().isSSLEnabled()) { - conn = (HttpsURLConnection) url.openConnection(); + conn = (HttpsURLConnection) url.openConnection(); TrustManager[] trustAllCerts = new TrustManager[]{ new X509TrustManager() { public java.security.cert.X509Certificate[] getAcceptedIssuers() { @@ -217,9 +227,10 @@ public void checkServerTrusted(java.security.cert.X509Certificate[] certs, Strin conn = (HttpURLConnection) url.openConnection(); } - conn.setRequestMethod(isGet?"GET":"POST"); + conn.setRequestMethod(isGet ? "GET" : "POST"); conn.setRequestProperty("Authorization", basicAuth); - if(!isGet) { + + if (!isGet) { conn.setDoOutput(true); conn.setRequestProperty("Content-Type", "application/json; charset=UTF-8"); @@ -229,17 +240,45 @@ public void checkServerTrusted(java.security.cert.X509Certificate[] certs, Strin } } - stream = conn.getInputStream(); + return conn; + } + + private static String readResponse(HttpURLConnection conn) throws IOException { + try (InputStream stream = (conn.getResponseCode() >= 400) ? conn.getErrorStream() : conn.getInputStream(); + BufferedReader in = new BufferedReader(new InputStreamReader(stream))) { + StringBuilder content = new StringBuilder(); + String inputLine; - BufferedReader in = new BufferedReader(new InputStreamReader(stream)); - String inputLine; - StringBuilder content = new StringBuilder(); - while ((inputLine = in.readLine()) != null) { - content.append(inputLine); + while ((inputLine = in.readLine()) != null) { + content.append(inputLine); + } + + return content.toString(); + } + } + + public static String callEndpoint(boolean isGet, String endpoint, String serverURL, String data) throws Exception { + HttpURLConnection conn = setupConnection(isGet, endpoint, serverURL, data); + + if (conn.getResponseCode() != 200) { + throw new Exception("HTTP error code: " + conn.getResponseCode()); } - in.close(); - return content.toString(); + return readResponse(conn); } + + public static CompletableFuture callEndpointAllowErrors(boolean isGet, String endpoint, String serverURL, String data) { + return CompletableFuture.supplyAsync(() -> { + try { + HttpURLConnection conn = setupConnection(isGet, endpoint, serverURL, data); + return readResponse(conn); + } catch (Exception e) { + throw new RuntimeException(e); + } + }, executor); + } + + private static final ExecutorService executor = Executors.newFixedThreadPool(10); + } diff --git a/src/main/java/com/couchbase/intellij/workbench/QueryExecutor.java b/src/main/java/com/couchbase/intellij/workbench/QueryExecutor.java index 58e1b977..d0502086 100644 --- a/src/main/java/com/couchbase/intellij/workbench/QueryExecutor.java +++ b/src/main/java/com/couchbase/intellij/workbench/QueryExecutor.java @@ -14,15 +14,11 @@ import com.couchbase.intellij.workbench.error.CouchbaseQueryError; import com.couchbase.intellij.workbench.error.CouchbaseQueryErrorUtil; import com.couchbase.intellij.workbench.error.CouchbaseQueryResultError; -import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.Messages; -import com.intellij.openapi.wm.ToolWindow; -import com.intellij.openapi.wm.ToolWindowManager; import reactor.core.publisher.Mono; import java.lang.reflect.Field; -import java.text.DecimalFormat; import java.time.Duration; import java.util.*; import java.util.concurrent.BlockingQueue; @@ -34,23 +30,9 @@ public class QueryExecutor { - private static final DecimalFormat df = new DecimalFormat("#.00"); - private static ToolWindow toolWindow; - private static QueryResultToolWindowFactory resultWindow; - private static boolean isQueryScript = false; - private static QueryResultToolWindowFactory getOutputWindow(Project project) { - if (toolWindow == null) { - ToolWindowManager toolWindowManager = ToolWindowManager.getInstance(project); - toolWindow = toolWindowManager.getToolWindow("Couchbase Output"); - } - ApplicationManager.getApplication().invokeLater(() -> ApplicationManager.getApplication().runWriteAction(() -> toolWindow.show())); + private static boolean isQueryScript = false; - if (resultWindow == null) { - resultWindow = QueryResultToolWindowFactory.instance; - } - return resultWindow; - } public static Boolean executeScript(BlockingQueue queue, QueryType type, QueryContext context, List statements, int historyIndex, Project project) { Cluster cluster = ActiveCluster.getInstance().getCluster(); @@ -61,7 +43,7 @@ public static Boolean executeScript(BlockingQueue queue, QueryType type Messages.showMessageDialog("There is no active connection to run this query", "Couchbase Plugin Error", Messages.getErrorIcon()); return false; } - getOutputWindow(project).setStatusAsLoading(); + QueryResultUtil.getOutputWindow(project).setStatusAsLoading(); List result = new ArrayList<>(); List metas = new ArrayList<>(); @@ -137,7 +119,7 @@ public static Boolean executeScript(BlockingQueue queue, QueryType type if (queue.peek() != null) { queue.poll(); future.cancel(true); - getOutputWindow(project).setStatusAsCanceled(); + QueryResultUtil.getOutputWindow(project).setStatusAsCanceled(); } } while (!future.isDone()); @@ -152,7 +134,7 @@ public static Boolean executeScript(BlockingQueue queue, QueryType type metricsList.add("-"); metricsList.add(String.valueOf(mutationCount.get())); metricsList.add(String.valueOf(resultCount.get())); - metricsList.add(getSizeText(resultSize.get())); + metricsList.add(QueryResultUtil.getSizeText(resultSize.get())); List timings; if (type == QueryType.EXPLAIN) { timings = result.stream() @@ -172,7 +154,7 @@ public static Boolean executeScript(BlockingQueue queue, QueryType type } else { timings = null; } - getOutputWindow(project).updateQueryStats(metricsList, result, error, timings, true); + QueryResultUtil.getOutputWindow(project).updateQueryStats(metricsList, result, error, timings, true); return error.getErrors().isEmpty(); } @@ -193,7 +175,7 @@ public static Boolean executeQuery(BlockingQueue queue, QueryType type, Messages.showMessageDialog("There is no active connection to run this query", "Couchbase Plugin Error", Messages.getErrorIcon()); return false; } - getOutputWindow(project).setStatusAsLoading(); + QueryResultUtil.getOutputWindow(project).setStatusAsLoading(); if (QueryType.EXPLAIN == type) { query = "EXPLAIN " + query; @@ -206,7 +188,7 @@ public static Boolean executeQuery(BlockingQueue queue, QueryType type, CouchbaseQueryResultError error = new CouchbaseQueryResultError(); error.setErrors(List.of(err)); - getOutputWindow(project).updateQueryStats(Arrays.asList("0 MS", "-", "-", "-", "-", "-"), + QueryResultUtil.getOutputWindow(project).updateQueryStats(Arrays.asList("0 MS", "-", "-", "-", "-", "-"), null, error, null, false); return false; } @@ -243,7 +225,7 @@ public static Boolean executeQuery(BlockingQueue queue, QueryType type, if (queue.peek() != null) { queue.poll(); futureResult.cancel(true); - getOutputWindow(project).setStatusAsCanceled(); + QueryResultUtil.getOutputWindow(project).setStatusAsCanceled(); } } QueryResult result = futureResult.get(); @@ -258,7 +240,7 @@ public static Boolean executeQuery(BlockingQueue queue, QueryType type, metricsList.add(metrics.get().executionTime().toMillis() + " MS"); metricsList.add(String.valueOf(metrics.get().mutationCount())); metricsList.add(String.valueOf(metrics.get().resultCount())); - metricsList.add(getSizeText(metrics.get().resultSize())); + metricsList.add(QueryResultUtil.getSizeText(metrics.get().resultSize())); } else { metricsList.add("-"); metricsList.add("-"); @@ -303,15 +285,15 @@ public static Boolean executeQuery(BlockingQueue queue, QueryType type, timings = null; } - getOutputWindow(project).updateQueryStats(metricsList, resultList, null, Collections.singletonList(timings), false); + QueryResultUtil.getOutputWindow(project).updateQueryStats(metricsList, resultList, null, Collections.singletonList(timings), false); } catch (CouchbaseException e) { long end = System.currentTimeMillis(); - getOutputWindow(project).updateQueryStats(Arrays.asList((end - start) + " MS", "-", "-", "-", "-", "-"), + QueryResultUtil.getOutputWindow(project).updateQueryStats(Arrays.asList((end - start) + " MS", "-", "-", "-", "-", "-"), null, CouchbaseQueryErrorUtil.parseQueryError(e), null, false); } catch (ExecutionException e) { long end = System.currentTimeMillis(); - getOutputWindow(project).updateQueryStats(Arrays.asList((end - start) + " MS", "-", "-", "-", "-", "-"), + QueryResultUtil.getOutputWindow(project).updateQueryStats(Arrays.asList((end - start) + " MS", "-", "-", "-", "-", "-"), null, CouchbaseQueryErrorUtil.parseQueryError(e), null, false); } catch (Exception e) { Log.error(e); @@ -339,14 +321,6 @@ private static JsonObject cleanupAutoLimitedMetadata(JsonObject metadata) { return metadata; } - private static String getSizeText(long size) { - if (size < 1024) { - return size + " Bytes"; - } else { - return df.format(size / 1024.0) + " KB"; - } - } - private static boolean updateQueryHistory(String query, int currentIndex) { List hist = QueryHistoryStorage.getInstance().getValue().getHistory(); if (query == null) { diff --git a/src/main/java/com/couchbase/intellij/workbench/QueryResultUtil.java b/src/main/java/com/couchbase/intellij/workbench/QueryResultUtil.java new file mode 100644 index 00000000..ac1e3892 --- /dev/null +++ b/src/main/java/com/couchbase/intellij/workbench/QueryResultUtil.java @@ -0,0 +1,41 @@ +package com.couchbase.intellij.workbench; + +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.application.ModalityState; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.wm.ToolWindow; +import com.intellij.openapi.wm.ToolWindowManager; + +import java.text.DecimalFormat; + +public class QueryResultUtil { + + private static final DecimalFormat df = new DecimalFormat("#.00"); + private static ToolWindow toolWindow; + private static QueryResultToolWindowFactory resultWindow; + + public static QueryResultToolWindowFactory getOutputWindow(Project project) { + if (toolWindow == null) { + ToolWindowManager toolWindowManager = ToolWindowManager.getInstance(project); + toolWindow = toolWindowManager.getToolWindow("Couchbase Output"); + } + ApplicationManager.getApplication().invokeLater(() -> { + ApplicationManager.getApplication().runWriteAction(() -> { + toolWindow.show(); + }); + }, ModalityState.any()); + + if (resultWindow == null) { + resultWindow = QueryResultToolWindowFactory.instance; + } + return resultWindow; + } + + public static String getSizeText(long size) { + if (size < 1024) { + return size + " Bytes"; + } else { + return df.format(size / 1024.0) + " KB"; + } + } +} diff --git a/src/main/java/com/couchbase/intellij/workbench/error/CouchbaseQueryErrorUtil.java b/src/main/java/com/couchbase/intellij/workbench/error/CouchbaseQueryErrorUtil.java index fc2a9396..5b8e9df6 100644 --- a/src/main/java/com/couchbase/intellij/workbench/error/CouchbaseQueryErrorUtil.java +++ b/src/main/java/com/couchbase/intellij/workbench/error/CouchbaseQueryErrorUtil.java @@ -16,4 +16,13 @@ public static CouchbaseQueryResultError parseQueryError(ExecutionException ex) { String json = ex.getMessage().substring(ex.getMessage().indexOf("{")); return new Gson().fromJson(json, CouchbaseQueryResultError.class); } + + public static CouchbaseQueryResultError parseQueryError(String message) { + String json = "{ \"errors\": [ {\n" + + " \"message\": \"" + message + "\"\n" + + " } ]}"; + return new Gson().fromJson(json, CouchbaseQueryResultError.class); + } + + } diff --git a/src/main/java/utils/CBConfigUtil.java b/src/main/java/utils/CBConfigUtil.java index c4f741af..f2cea5ba 100644 --- a/src/main/java/utils/CBConfigUtil.java +++ b/src/main/java/utils/CBConfigUtil.java @@ -23,9 +23,16 @@ public static boolean isSupported(String specifiedVersion) { } public static boolean hasQueryService(List services) { - if(services == null) { + if (services == null) { return false; } return services.contains("n1ql"); } + + public static boolean hasSearchService(List services) { + if (services == null) { + return false; + } + return services.contains("fts"); + } } diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 80254858..9f7d7b30 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -30,11 +30,7 @@ New Features
    -
  • You can now easily migrate from mongodb to Couchbase!
  • -
  • JSON Documents now suggests attribute keys
  • -
  • We automatically add limit to your SQL++ Queries
  • -
  • Favorite Queries has a new tab called "Built-in Queries"
  • -
  • Document Ids can now contain special chars
  • +
  • Now the collection listing can use whatever index you have in the collection
  • Bug fixes
]]> @@ -130,6 +126,13 @@ language="sqlppl" implementationClass="org.intellij.sdk.language.cblite.SqlppLiteSyntaxHighlighterFactory"/> + + + diff --git a/src/main/resources/assets/icons/cbs.svg b/src/main/resources/assets/icons/cbs.svg new file mode 100644 index 00000000..fa1b0737 --- /dev/null +++ b/src/main/resources/assets/icons/cbs.svg @@ -0,0 +1,38 @@ + + + + + + + + + diff --git a/src/main/resources/assets/icons/collections.svg b/src/main/resources/assets/icons/collections.svg index faba1863..73d404cd 100644 --- a/src/main/resources/assets/icons/collections.svg +++ b/src/main/resources/assets/icons/collections.svg @@ -1,5 +1,6 @@ - - - + + + folders + \ No newline at end of file diff --git a/src/main/resources/assets/icons/scope.svg b/src/main/resources/assets/icons/scope.svg new file mode 100644 index 00000000..faba1863 --- /dev/null +++ b/src/main/resources/assets/icons/scope.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file