diff --git a/src/main/java/com/couchbase/intellij/listener/DependenciesDownloader.java b/src/main/java/com/couchbase/intellij/listener/DependenciesDownloader.java index d821397a..05026147 100644 --- a/src/main/java/com/couchbase/intellij/listener/DependenciesDownloader.java +++ b/src/main/java/com/couchbase/intellij/listener/DependenciesDownloader.java @@ -67,6 +67,7 @@ private Map getToolsMap(String toolKey, String os) { } else if (ALL_TOOLS.equals(toolKey)) { map.put(CBTools.Type.CBC_PILLOW_FIGHT, path + "cbc-pillowfight" + suffix); map.put(CBTools.Type.MCTIMINGS, path + "mctimings" + suffix); + map.put(CBTools.Type.CBSTATS, path + "cbstats" + suffix); } else { throw new IllegalStateException("Not implemented yet"); @@ -155,16 +156,24 @@ public void downloadDependencies() throws Exception { ToolSpec cbTools = downloads.get(ALL_TOOLS); String toolsDir = toolsPath + File.separator + cbTools.getInstallationPath(); - if (CBTools.getTool(CBTools.Type.CBC_PILLOW_FIGHT).getStatus() == ToolStatus.NOT_AVAILABLE - && !isInstalled(toolsPath, downloads.get(ALL_TOOLS), CBTools.Type.CBC_PILLOW_FIGHT)) { - Log.info("Downloading CB tools. The feature will be automatically enabled when the download is complete."); - CBTools.getTool(CBTools.Type.CBC_PILLOW_FIGHT).setStatus(ToolStatus.DOWNLOADING); - CBTools.getTool(CBTools.Type.MCTIMINGS).setStatus(ToolStatus.DOWNLOADING); + CBTools.Type[] toolTypes = {CBTools.Type.CBC_PILLOW_FIGHT, CBTools.Type.MCTIMINGS, CBTools.Type.CBSTATS}; + + boolean shouldDownload = false; + for (CBTools.Type toolType : toolTypes) { + if (CBTools.getTool(toolType).getStatus() == ToolStatus.NOT_AVAILABLE + && !isInstalled(toolsPath, downloads.get(ALL_TOOLS), toolType)) { + shouldDownload = true; + CBTools.getTool(toolType).setStatus(ToolStatus.DOWNLOADING); + } else { + Log.debug(toolType + " tool is already installed"); + setToolActive(ToolStatus.AVAILABLE, toolsDir, cbTools); + } + } + + if (shouldDownload) { + Log.info("Downloading tools. The features will be automatically enabled when the download is complete."); downloadAndUnzip(toolsDir, cbTools); - } else { - Log.debug("CB Tools are already installed"); - setToolActive(ToolStatus.AVAILABLE, toolsDir, cbTools); } } diff --git a/src/main/java/com/couchbase/intellij/tools/CBStats.java b/src/main/java/com/couchbase/intellij/tools/CBStats.java new file mode 100644 index 00000000..a8a65946 --- /dev/null +++ b/src/main/java/com/couchbase/intellij/tools/CBStats.java @@ -0,0 +1,82 @@ +package com.couchbase.intellij.tools; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import com.couchbase.intellij.database.ActiveCluster; +import com.couchbase.intellij.workbench.Log; + +import utils.OSUtil; +import utils.ProcessUtils; + +public class CBStats { + private final String bucketName; + private final String scopeName; + private final String collectionName; + + private final String type; + + public CBStats(String bucketName, String scopeName, String collectionName, String type) { + this.bucketName = bucketName; + this.scopeName = scopeName; + this.collectionName = collectionName; + this.type = type; + } + + public String executeCommand() throws IOException, InterruptedException { + StringBuilder output; + + List command = new ArrayList<>(); + // command.add(CBTools.getTool(CBTools.Type.CBSTATS).getPath()); + String osArch = OSUtil.getOSArch(); + + // Determine the command path based on the operating system and architecture + switch (osArch) { + case OSUtil.MACOS_ARM: + case OSUtil.MACOS_64: + command.add("/Applications/Couchbase Server.app/Contents/Resources/couchbase-core/bin/cbstats"); + break; + case OSUtil.WINDOWS_ARM: + case OSUtil.WINDOWS_64: + command.add("C:\\Program Files\\Couchbase\\Server\\bin\\cbstats"); + break; + case OSUtil.LINUX_ARM: + case OSUtil.LINUX_64: + command.add("/opt/couchbase/bin/cbstats"); + break; + default: + throw new UnsupportedOperationException("Unsupported operating system: " + osArch); + } + command.add((ActiveCluster.getInstance().getClusterURL().replaceFirst("^couchbase://", "")) + ":" + + (ActiveCluster.getInstance().isSSLEnabled() ? "11207" : "11210")); + command.add("-u"); + command.add(ActiveCluster.getInstance().getUsername()); + command.add("-p"); + command.add(ActiveCluster.getInstance().getPassword()); + + command.add("-b"); + command.add(bucketName); + + if (type.equalsIgnoreCase("collection")) { + command.add("collections"); + command.add(scopeName + "." + collectionName); + } else if (type.equalsIgnoreCase("scope")) { + command.add("scopes"); + command.add(scopeName); + } + + ProcessBuilder processBuilder = new ProcessBuilder(command); + Process process = processBuilder.start(); + + output = new StringBuilder(ProcessUtils.returnOutput(process)); + + if (process.waitFor() == 0) { + Log.info("Command executed successfully"); + } else { + Log.error("Command execution failed"); + } + + return output.toString(); + } +} diff --git a/src/main/java/com/couchbase/intellij/tools/CBTools.java b/src/main/java/com/couchbase/intellij/tools/CBTools.java index bd77b523..399e47f2 100644 --- a/src/main/java/com/couchbase/intellij/tools/CBTools.java +++ b/src/main/java/com/couchbase/intellij/tools/CBTools.java @@ -20,6 +20,7 @@ public enum Type { CB_IMPORT, CB_EXPORT, CBC_PILLOW_FIGHT, - MCTIMINGS + CBSTATS, + MCTIMINGS, } } diff --git a/src/main/java/com/couchbase/intellij/tools/dialog/CbstatsDialog.java b/src/main/java/com/couchbase/intellij/tools/dialog/CbstatsDialog.java new file mode 100644 index 00000000..8abddb5a --- /dev/null +++ b/src/main/java/com/couchbase/intellij/tools/dialog/CbstatsDialog.java @@ -0,0 +1,151 @@ +package com.couchbase.intellij.tools.dialog; + +import java.awt.BorderLayout; + +import javax.swing.JComponent; +import javax.swing.JPanel; + +import org.jetbrains.annotations.Nullable; + +import com.couchbase.intellij.tools.CBStats; +import com.couchbase.intellij.workbench.Log; +import com.intellij.openapi.ui.DialogWrapper; + +import utils.TemplateUtil; + +public class CbstatsDialog extends DialogWrapper { + + private final String bucketName; + private final String scopeName; + private final String collectionName; + private final String type; + + private static final String MEMORY_USED_BY_COLLECTION = "Memory Used By Collection"; + private static final String COLLECTION_DATA_SIZE = "Collection Data Size"; + private static final String NUMBER_OF_ITEMS = "Number Of Items"; + private static final String COLLECTION_NAME = "Collection Name"; + private static final String NUMBER_OF_DELETE_OPERATIONS = "Number Of Delete Operations"; + private static final String NUMBER_OF_GET_OPERATIONS = "Number Of Get Operations"; + private static final String NUMBER_OF_STORE_OPERATIONS = "Number Of Store Operations"; + private static final String SCOPE_NAME = "Scope Name"; + + public CbstatsDialog(String bucket, String scope, String collection, String type) { + super(true); + this.bucketName = bucket; + this.scopeName = scope; + this.collectionName = collection; + this.type = type; + init(); + setTitle("Stats for " + type); + setResizable(true); + } + + @Nullable + @Override + protected JComponent createCenterPanel() { + JPanel dialogPanel = new JPanel(new BorderLayout()); + + CBStats cbStats = new CBStats(bucketName, scopeName, collectionName, type); + String output = ""; + try { + output = cbStats.executeCommand(); + } catch (Exception ex) { + Log.error(ex); + } + + String[] lines = output.split("\n"); + String[] keys = new String[lines.length]; + String[] values = new String[lines.length]; + String[] helpTexts = new String[lines.length]; // array for help texts + + for (int i = 0; i < lines.length; i++) { + int keyStartIndex = lines[i].indexOf(':', lines[i].indexOf(':') + 1) + 1; + int valueStartIndex = lines[i].lastIndexOf(':') + 1; + keys[i] = getFriendlyKeyName(lines[i].substring(keyStartIndex, valueStartIndex - 1).trim()); + values[i] = getFriendlyValue(lines[i].substring(valueStartIndex).trim(), keys[i]); + helpTexts[i] = getHelpText(keys[i]); // get help text for each key + } + + JPanel keyValuePanel = TemplateUtil.createKeyValuePanelWithHelp(keys, values, helpTexts, 1); + + dialogPanel.add(keyValuePanel, BorderLayout.CENTER); + + return dialogPanel; + } + + private String getHelpText(String key) { + switch (key) { + case MEMORY_USED_BY_COLLECTION: + return "This is the memory used by the collection. It is measured in bytes."; + case COLLECTION_DATA_SIZE: + return "This is the size of the data in the collection. It is also measured in bytes."; + case NUMBER_OF_ITEMS: + return "This represents the number of items in the collection. It doesn't have a unit as it's a count."; + case COLLECTION_NAME: + return "This is the name of the collection. It's a string and doesn't have a unit."; + case NUMBER_OF_DELETE_OPERATIONS: + return "This is the number of delete operations that have been performed on the collection. It doesn't have a unit as it's a count."; + case NUMBER_OF_GET_OPERATIONS: + return "This is the number of get operations that have been performed on the collection. It doesn't have a unit as it's a count."; + case NUMBER_OF_STORE_OPERATIONS: + return "This is the number of store operations that have been performed on the collection. It doesn't have a unit as it's a count."; + case SCOPE_NAME: + return "This is the name of the scope that contains the collection. It's a string and doesn't have a unit."; + default: + return ""; + } + } + + private String getFriendlyKeyName(String key) { + switch (key) { + case "collections_mem_used": + return MEMORY_USED_BY_COLLECTION; + case "data_size": + return COLLECTION_DATA_SIZE; + case "items": + return NUMBER_OF_ITEMS; + case "name": + return COLLECTION_NAME; + case "ops_delete": + return NUMBER_OF_DELETE_OPERATIONS; + case "ops_get": + return NUMBER_OF_GET_OPERATIONS; + case "ops_store": + return NUMBER_OF_STORE_OPERATIONS; + case "scope_name": + return SCOPE_NAME; + default: + return key; + } + } + + private String getFriendlyValue(String value, String key) { + switch (key) { + case MEMORY_USED_BY_COLLECTION: + case COLLECTION_DATA_SIZE: + long bytes = Long.parseLong(value); + double sizeInMB = bytes / (1024.0 * 1024.0); + if (sizeInMB >= 1024) { + double sizeInGB = sizeInMB / 1024.0; + return String.format("%.2f GB", sizeInGB); + } else { + return String.format("%.2f MB", sizeInMB); + } + case NUMBER_OF_DELETE_OPERATIONS: + case NUMBER_OF_GET_OPERATIONS: + case NUMBER_OF_STORE_OPERATIONS: + case NUMBER_OF_ITEMS: + long count = Long.parseLong(value); + if (count >= 1_000_000) { + return String.format("%.2f M", count / 1_000_000.0); + } else if (count >= 1_000) { + return String.format("%.2f K", count / 1_000.0); + } else { + return value; + } + default: + return value; + } + } + +} diff --git a/src/main/java/com/couchbase/intellij/tree/TreeRightClickListener.java b/src/main/java/com/couchbase/intellij/tree/TreeRightClickListener.java index 6dfb780f..b408a3bb 100644 --- a/src/main/java/com/couchbase/intellij/tree/TreeRightClickListener.java +++ b/src/main/java/com/couchbase/intellij/tree/TreeRightClickListener.java @@ -10,9 +10,9 @@ import com.couchbase.intellij.tools.CBImport; import com.couchbase.intellij.tools.CBTools; import com.couchbase.intellij.tools.PillowFightDialog; +import com.couchbase.intellij.tools.dialog.CbstatsDialog; import com.couchbase.intellij.tools.dialog.DDLExportDialog; import com.couchbase.intellij.tools.dialog.ExportDialog; -import com.couchbase.intellij.tree.NewEntityCreationDialog.EntityType; import com.couchbase.intellij.tree.docfilter.DocumentFilterDialog; import com.couchbase.intellij.tree.node.*; import com.couchbase.intellij.tree.overview.IndexOverviewDialog; @@ -596,6 +596,21 @@ public void actionPerformed(@NotNull AnActionEvent e) { actionGroup.add(simpleExport); } + actionGroup.addSeparator(); + AnAction viewStats = new AnAction("View Collection Statistics") { + @Override + public void actionPerformed(@NotNull AnActionEvent e) { + CbstatsDialog cbstatsDialog = new CbstatsDialog( + col.getBucket(), + col.getScope(), + col.getText(), + "collection" + ); + cbstatsDialog.show(); + } + }; + actionGroup.add(viewStats); + showPopup(e, tree, actionGroup); } } diff --git a/src/main/java/utils/ProcessUtils.java b/src/main/java/utils/ProcessUtils.java index a1e81ea5..45df901d 100644 --- a/src/main/java/utils/ProcessUtils.java +++ b/src/main/java/utils/ProcessUtils.java @@ -24,4 +24,26 @@ public static void printOutput(Process process, String message) throws IOExcepti } } } + + public static String returnOutput(Process process) throws IOException { + StringBuilder output = new StringBuilder(); + + // Process standard output + try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { + String line; + while ((line = reader.readLine()) != null) { + output.append(line).append("\n"); + } + } + + // Process standard error + try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) { + String line; + while ((line = reader.readLine()) != null) { + output.append(line).append("\n"); + } + } + + return output.toString(); + } } diff --git a/src/main/java/utils/TemplateUtil.java b/src/main/java/utils/TemplateUtil.java index 7c154408..e8fe464e 100644 --- a/src/main/java/utils/TemplateUtil.java +++ b/src/main/java/utils/TemplateUtil.java @@ -80,6 +80,67 @@ public static JPanel createKeyValuePanel(String[] keys, String[] values, int col return finalPanel; } + public static JPanel createKeyValuePanelWithHelp(String[] keys, String[] values, String[] helpTexts, int cols) { + JPanel finalPanel = new JPanel(new GridBagLayout()); + GridBagConstraints gbc = new GridBagConstraints(); + gbc.fill = GridBagConstraints.BOTH; + gbc.gridy = 0; + + int firstColumnRows = (int) Math.ceil((double) keys.length / cols); + int remainingColumnRows = keys.length % cols == 0 ? keys.length / cols : (keys.length / cols) + 1; + + boolean isFirstColumnOdd = firstColumnRows % 2 != 0; + + for (int i = 0; i < cols; i++) { + JPanel columnPanel = new JPanel(new GridBagLayout()); + GridBagConstraints columnGbc = new GridBagConstraints(); + columnGbc.fill = GridBagConstraints.HORIZONTAL; + columnGbc.gridy = GridBagConstraints.RELATIVE; + columnGbc.anchor = GridBagConstraints.NORTHWEST; + columnGbc.insets = JBUI.insets(5, 5, 5, 15); + columnGbc.weightx = 0; + + int columnRows = (i == 0 && cols % 2 != 0) ? firstColumnRows : remainingColumnRows; + + for (int j = 0; j < columnRows; j++) { + int index = i * firstColumnRows + j - (i > 0 ? firstColumnRows - remainingColumnRows : 0); + + if (index < keys.length) { + JPanel keyPanelWithHelpIcon = getLabelWithHelp(keys[index], helpTexts[index]); + columnPanel.add(keyPanelWithHelpIcon, columnGbc); + + columnGbc.gridx = 1; + columnGbc.weightx = 1; + columnGbc.fill = GridBagConstraints.HORIZONTAL; + + columnPanel.add(new JLabel(values[index]), columnGbc); + + columnGbc.gridx = 0; + columnGbc.weightx = 0; + columnGbc.fill = GridBagConstraints.HORIZONTAL; + } else if (isFirstColumnOdd && i > 0) { + columnPanel.add(new JLabel(" "), columnGbc); + + columnGbc.gridx = 1; + columnGbc.weightx = 1; + columnGbc.fill = GridBagConstraints.HORIZONTAL; + + columnPanel.add(new JLabel(""), columnGbc); + + columnGbc.gridx = 0; + columnGbc.weightx = 0; + columnGbc.fill = GridBagConstraints.HORIZONTAL; + } + } + + gbc.weightx = 1.0 / cols; + finalPanel.add(columnPanel, gbc); + } + + finalPanel.setMaximumSize(new Dimension(Integer.MAX_VALUE, finalPanel.getPreferredSize().height)); + return finalPanel; + } + public static TitledSeparator getSeparator(String title) { TitledSeparator titledSeparator = new TitledSeparator(); titledSeparator.setText(title); @@ -146,7 +207,6 @@ public static String fmtByte(long bytes) { } } - public static String fmtDouble(Double value) { if (value == null) {