From 5edadae11c61b05449bb673b80b04c7434ae4af9 Mon Sep 17 00:00:00 2001 From: Asaf Gabai <77976014+asafgabai@users.noreply.github.com> Date: Mon, 13 Mar 2023 10:57:21 +0200 Subject: [PATCH] Handle JCEF is not supported error (#309) * Changed message in case JCEF is not supported. * Created an empty state for view load error. * Fixed a bug regarding opening links inside the webview, instead of in a new browser window. --- .../ide/idea/ui/JFrogLocalToolWindow.java | 148 +++++++++++------- .../jfrog/ide/idea/ui/JFrogToolWindow.java | 1 + .../idea/ui/jcef/message/MessagePacker.java | 15 +- .../ide/idea/ui/utils/ComponentUtils.java | 24 +-- .../ide/idea/ui/webview/WebviewManager.java | 75 +++++++++ 5 files changed, 184 insertions(+), 79 deletions(-) create mode 100644 src/main/java/com/jfrog/ide/idea/ui/webview/WebviewManager.java diff --git a/src/main/java/com/jfrog/ide/idea/ui/JFrogLocalToolWindow.java b/src/main/java/com/jfrog/ide/idea/ui/JFrogLocalToolWindow.java index 3d6c87a1..462762a9 100644 --- a/src/main/java/com/jfrog/ide/idea/ui/JFrogLocalToolWindow.java +++ b/src/main/java/com/jfrog/ide/idea/ui/JFrogLocalToolWindow.java @@ -5,6 +5,8 @@ import com.intellij.openapi.actionSystem.DefaultActionGroup; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.project.Project; +import com.intellij.openapi.projectRoots.impl.jdkDownloader.RuntimeChooserUtil; +import com.intellij.openapi.util.Disposer; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.LocalFileSystem; import com.intellij.openapi.vfs.VirtualFile; @@ -13,10 +15,9 @@ import com.intellij.psi.PsiFile; import com.intellij.psi.PsiManager; import com.intellij.ui.*; +import com.intellij.ui.components.JBLabel; import com.intellij.ui.components.JBPanel; import com.intellij.ui.jcef.JBCefApp; -import com.intellij.ui.jcef.JBCefBrowser; -import com.intellij.ui.jcef.JBCefBrowserBase; import com.intellij.util.ui.UIUtil; import com.jfrog.ide.common.nodes.ApplicableIssueNode; import com.jfrog.ide.common.nodes.IssueNode; @@ -28,15 +29,13 @@ import com.jfrog.ide.idea.events.ApplicationEvents; import com.jfrog.ide.idea.log.Logger; import com.jfrog.ide.idea.scan.ScanManager; -import com.jfrog.ide.idea.ui.jcef.message.MessagePacker; import com.jfrog.ide.idea.ui.utils.ComponentUtils; +import com.jfrog.ide.idea.ui.webview.WebviewManager; import com.jfrog.ide.idea.ui.webview.WebviewObjectConverter; import com.jfrog.ide.idea.utils.Utils; import org.apache.commons.io.FileUtils; import org.apache.commons.lang.exception.ExceptionUtils; import org.cef.browser.CefBrowser; -import org.cef.browser.CefFrame; -import org.cef.handler.CefLoadHandlerAdapter; import org.jetbrains.annotations.NotNull; import javax.swing.*; @@ -55,14 +54,13 @@ */ public class JFrogLocalToolWindow extends AbstractJFrogToolWindow { private final LocalComponentsTree componentsTree; - private final JBCefBrowserBase browser; private final OnePixelSplitter verticalSplit; - private final JPanel leftPanelContent; - private final JComponent compTreeView; - private final MessagePacker messagePacker; - private final boolean isInitialized; + private JPanel leftPanelContent; + private JComponent compTreeView; + private boolean isInitialized; private IssueNode selectedIssue; private Path tempDirPath; + private WebviewManager webviewManager; /** * @param project - Currently opened IntelliJ project @@ -70,25 +68,34 @@ public class JFrogLocalToolWindow extends AbstractJFrogToolWindow { public JFrogLocalToolWindow(@NotNull Project project) { super(project); componentsTree = LocalComponentsTree.getInstance(project); - browser = new JBCefBrowser(); + JPanel leftPanel = new JBPanel<>(new BorderLayout()); + verticalSplit = new OnePixelSplitter(false, 0.4f); + verticalSplit.setFirstComponent(leftPanel); + setContent(verticalSplit); + if (!JBCefApp.isSupported()) { + leftPanel.add(createJcefNotSupportedView(), 0); + return; + } - messagePacker = new MessagePacker(browser); - initVulnerabilityInfoBrowser(); + JComponent browserComponent; + try { + browserComponent = initVulnerabilityInfoBrowser(); + } catch (IOException | URISyntaxException e) { + Logger.getInstance().error("Local view couldn't be initialized.", e); + leftPanel.removeAll(); + leftPanel.add(createLoadErrorView(), 0); + return; + } JPanel toolbar = createActionToolbar(); toolbar.setBorder(IdeBorderFactory.createBorder(SideBorder.BOTTOM)); - JPanel leftPanel = new JBPanel<>(new BorderLayout()); leftPanel.add(toolbar, BorderLayout.PAGE_START); leftPanelContent = new JBPanel<>(new BorderLayout()); leftPanel.add(leftPanelContent); compTreeView = createComponentsTreeView(); - verticalSplit = new OnePixelSplitter(false, 0.4f); - verticalSplit.setFirstComponent(leftPanel); - setContent(verticalSplit); - refreshView(); - registerListeners(); + registerListeners(browserComponent); isInitialized = true; } @@ -102,7 +109,7 @@ public JPanel createActionToolbar() { /** * Register the issues tree listeners. */ - public void registerListeners() { + public void registerListeners(JComponent browserComponent) { // Xray credentials were set listener appBusConnection.subscribe(ApplicationEvents.ON_CONFIGURATION_DETAILS_CHANGE, () -> ApplicationManager.getApplication().invokeLater(this::onConfigurationChange)); @@ -115,7 +122,7 @@ public void registerListeners() { selectedIssue = (IssueNode) e.getNewLeadSelectionPath().getLastPathComponent(); updateIssueOrLicenseInWebview(selectedIssue); - verticalSplit.setSecondComponent(browser.getComponent()); + verticalSplit.setSecondComponent(browserComponent); }); projectBusConnection.subscribe(ApplicationEvents.ON_SCAN_LOCAL_STARTED, () -> { setLeftPanelContent(compTreeView); @@ -138,56 +145,84 @@ private void refreshView() { @SuppressWarnings("UnstableApiUsage") private JComponent createReadyEnvView() { - JPanel noCredentialsPanel = new JBPanel<>(); - noCredentialsPanel.setLayout(new BoxLayout(noCredentialsPanel, BoxLayout.PAGE_AXIS)); + JPanel readyEnvPanel = new JBPanel<>(); + readyEnvPanel.setLayout(new BoxLayout(readyEnvPanel, BoxLayout.PAGE_AXIS)); // "We're all set!" - HyperlinkLabel allSetLabel = new HyperlinkLabel(); + JBLabel allSetLabel = new JBLabel(); allSetLabel.setText("We're all set."); - ComponentUtils.addCenteredHyperlinkLabel(noCredentialsPanel, allSetLabel); + ComponentUtils.addCenteredComponent(readyEnvPanel, allSetLabel); // "Scan your project" HyperlinkLabel scanLink = new HyperlinkLabel(); scanLink.setTextWithHyperlink("Scan your project"); scanLink.addHyperlinkListener(e -> ScanManager.getInstance(project).startScan()); - ComponentUtils.addCenteredHyperlinkLabel(noCredentialsPanel, scanLink); + ComponentUtils.addCenteredComponent(readyEnvPanel, scanLink); - return ComponentUtils.createUnsupportedPanel(noCredentialsPanel); + return ComponentUtils.createUnsupportedPanel(readyEnvPanel); } - private void setLeftPanelContent(JComponent component) { - leftPanelContent.removeAll(); - leftPanelContent.add(component, 0); + @SuppressWarnings("UnstableApiUsage") + private JComponent createJcefNotSupportedView() { + JPanel jcefNotSupportedPanel = new JBPanel<>(); + jcefNotSupportedPanel.setLayout(new BoxLayout(jcefNotSupportedPanel, BoxLayout.PAGE_AXIS)); + + // "Thank you for installing the JFrog IDEA Plugin!" + JBLabel thanksLabel = new JBLabel(); + thanksLabel.setText("Thank you for installing the JFrog IntelliJ IDEA Plugin!"); + ComponentUtils.addCenteredComponent(jcefNotSupportedPanel, thanksLabel); + + // "The plugin uses a component named JCEF that seem to be missing in your IDE." + JBLabel pluginNeedsJcefLabel = new JBLabel(); + pluginNeedsJcefLabel.setText("The plugin uses a component named JCEF that seem to be missing in your IDE."); + ComponentUtils.addCenteredComponent(jcefNotSupportedPanel, pluginNeedsJcefLabel); + + // "To make JCEF available in your IDE, you’ll need to have the IDE use a different boot runtime." + JBLabel replaceBootRuntimeLabel = new JBLabel(); + replaceBootRuntimeLabel.setText("To make JCEF available in your IDE, you’ll need to have the IDE use a different boot runtime."); + ComponentUtils.addCenteredComponent(jcefNotSupportedPanel, replaceBootRuntimeLabel); + + // "Click here, choose a boot runtime with JCEF, restart the IDE and come back." + HyperlinkLabel scanLink = new HyperlinkLabel(); + scanLink.setTextWithHyperlink("Click here, choose a boot runtime with JCEF, restart the IDE and come back."); + scanLink.addHyperlinkListener(e -> RuntimeChooserUtil.INSTANCE.showRuntimeChooserPopup()); + ComponentUtils.addCenteredComponent(jcefNotSupportedPanel, scanLink); + + return ComponentUtils.createUnsupportedPanel(jcefNotSupportedPanel); } - private void initVulnerabilityInfoBrowser() { - if (!JBCefApp.isSupported()) { - Logger.getInstance().error("Could not open the issue details view - JCEF is not supported"); - return; - } + private JComponent createLoadErrorView() { + JPanel loadErrorPanel = new JBPanel<>(); + loadErrorPanel.setLayout(new BoxLayout(loadErrorPanel, BoxLayout.PAGE_AXIS)); - try { - tempDirPath = Files.createTempDirectory("jfrog-idea-plugin"); - Utils.extractFromResources("/jfrog-ide-webview", tempDirPath); - } catch (IOException | URISyntaxException e) { - Logger.getInstance().error(e.getMessage()); - return; - } + // "The view couldn't be loaded." + JBLabel viewNotLoadedLabel = new JBLabel(); + viewNotLoadedLabel.setText("The view couldn't be loaded."); + ComponentUtils.addCenteredComponent(loadErrorPanel, viewNotLoadedLabel); - browser.getJBCefClient().addLoadHandler(new CefLoadHandlerAdapter() { - @Override - public void onLoadEnd(CefBrowser browser, CefFrame frame, int httpStatusCode) { - updateIssueOrLicenseInWebview(selectedIssue); - } + // "Check the Notifications / Event Log for more information." + JBLabel checkLogsLabel = new JBLabel(); + checkLogsLabel.setText("Check the Notifications / Event Log for more information."); + ComponentUtils.addCenteredComponent(loadErrorPanel, checkLogsLabel); - @Override - public void onLoadError(CefBrowser browser, CefFrame frame, ErrorCode errorCode, String errorText, String failedUrl) { - Logger.getInstance().error("An error occurred while opening the issue details view: " + errorText); - } - }, browser.getCefBrowser()); + return ComponentUtils.createUnsupportedPanel(loadErrorPanel); + } + + private void setLeftPanelContent(JComponent component) { + leftPanelContent.removeAll(); + leftPanelContent.add(component, 0); + } + + private JComponent initVulnerabilityInfoBrowser() throws IOException, URISyntaxException { + tempDirPath = Files.createTempDirectory("jfrog-idea-plugin"); + Utils.extractFromResources("/jfrog-ide-webview", tempDirPath); String pageUri = tempDirPath.resolve("index.html").toFile().toURI().toString(); - browser.loadURL(pageUri); + webviewManager = new WebviewManager(); + Disposer.register(this, webviewManager); + + CefBrowser browser = webviewManager.createBrowser(pageUri, () -> updateIssueOrLicenseInWebview(selectedIssue)); + return (JComponent) browser.getUIComponent(); } /** @@ -209,14 +244,14 @@ private JComponent createComponentsTreeView() { private void updateIssueOrLicenseInWebview(IssueNode vulnerabilityOrViolation) { if (vulnerabilityOrViolation instanceof VulnerabilityNode) { VulnerabilityNode issue = (VulnerabilityNode) vulnerabilityOrViolation; - messagePacker.send(WebviewObjectConverter.convertIssueToDepPage(issue)); + webviewManager.sendMessage(WebviewObjectConverter.convertIssueToDepPage(issue)); } else if (vulnerabilityOrViolation instanceof ApplicableIssueNode) { ApplicableIssueNode node = (ApplicableIssueNode) vulnerabilityOrViolation; - messagePacker.send(WebviewObjectConverter.convertIssueToDepPage(node.getIssue())); + webviewManager.sendMessage(WebviewObjectConverter.convertIssueToDepPage(node.getIssue())); navigateToFile(node); } else if (vulnerabilityOrViolation instanceof LicenseViolationNode) { LicenseViolationNode license = (LicenseViolationNode) vulnerabilityOrViolation; - messagePacker.send(WebviewObjectConverter.convertLicenseToDepPage(license)); + webviewManager.sendMessage(WebviewObjectConverter.convertLicenseToDepPage(license)); } } @@ -259,7 +294,6 @@ public void onConfigurationChange() { @Override public void dispose() { super.dispose(); - browser.dispose(); try { FileUtils.deleteDirectory(tempDirPath.toFile()); } catch (IOException e) { diff --git a/src/main/java/com/jfrog/ide/idea/ui/JFrogToolWindow.java b/src/main/java/com/jfrog/ide/idea/ui/JFrogToolWindow.java index 39f151d7..ada9b4f9 100644 --- a/src/main/java/com/jfrog/ide/idea/ui/JFrogToolWindow.java +++ b/src/main/java/com/jfrog/ide/idea/ui/JFrogToolWindow.java @@ -5,6 +5,7 @@ import com.intellij.ui.content.Content; import com.intellij.ui.content.ContentFactory; import com.intellij.ui.content.ContentManager; +import com.jfrog.ide.idea.log.Logger; import org.jetbrains.annotations.NotNull; diff --git a/src/main/java/com/jfrog/ide/idea/ui/jcef/message/MessagePacker.java b/src/main/java/com/jfrog/ide/idea/ui/jcef/message/MessagePacker.java index 2d7bec2c..c11f7f9a 100644 --- a/src/main/java/com/jfrog/ide/idea/ui/jcef/message/MessagePacker.java +++ b/src/main/java/com/jfrog/ide/idea/ui/jcef/message/MessagePacker.java @@ -1,15 +1,13 @@ package com.jfrog.ide.idea.ui.jcef.message; import com.fasterxml.jackson.core.JsonProcessingException; -import com.intellij.ui.jcef.JBCefBrowserBase; import com.jfrog.ide.idea.log.Logger; - -import static com.jfrog.ide.idea.ui.jcef.message.PackedMessage.IDE_SEND_FUNCTION_NAME; +import org.cef.browser.CefBrowser; public class MessagePacker implements MessagePipe { - JBCefBrowserBase browser; + CefBrowser browser; - public MessagePacker(JBCefBrowserBase browser) { + public MessagePacker(CefBrowser browser) { this.browser = browser; } @@ -24,11 +22,8 @@ public void send(Object data) { } private void send(String raw) { - browser.getCefBrowser().executeJavaScript( - "window." + IDE_SEND_FUNCTION_NAME + "=" + raw, - browser.getCefBrowser().getURL(), 0); - browser.getCefBrowser().executeJavaScript( + browser.executeJavaScript( "window.postMessage(" + raw + ")", - browser.getCefBrowser().getURL(), 0); + browser.getURL(), 0); } } diff --git a/src/main/java/com/jfrog/ide/idea/ui/utils/ComponentUtils.java b/src/main/java/com/jfrog/ide/idea/ui/utils/ComponentUtils.java index c7100bc7..336c026e 100644 --- a/src/main/java/com/jfrog/ide/idea/ui/utils/ComponentUtils.java +++ b/src/main/java/com/jfrog/ide/idea/ui/utils/ComponentUtils.java @@ -41,41 +41,41 @@ public static JComponent createNoCredentialsView() { noCredentialsPanel.setLayout(new BoxLayout(noCredentialsPanel, BoxLayout.PAGE_AXIS)); // "Thank you for installing the JFrog plugin" - HyperlinkLabel thanksLabel = new HyperlinkLabel(); + JBLabel thanksLabel = new JBLabel(); thanksLabel.setText("Thank you for installing the JFrog IntelliJ IDEA Plugin!"); - addCenteredHyperlinkLabel(noCredentialsPanel, thanksLabel); + addCenteredComponent(noCredentialsPanel, thanksLabel); // "If you already have a JFrog environment, configure its connection details." HyperlinkLabel configLink = new HyperlinkLabel(); configLink.setTextWithHyperlink("If you already have a JFrog environment,configure its connection details."); configLink.addHyperlinkListener(e -> ShowSettingsUtil.getInstance().showSettingsDialog(null, JFrogGlobalConfiguration.class)); - addCenteredHyperlinkLabel(noCredentialsPanel, configLink); + addCenteredComponent(noCredentialsPanel, configLink); // "Don't have a JFrog environment? Get one for FREE!" HyperlinkLabel getFreeLink = new HyperlinkLabel(); getFreeLink.setTextWithHyperlink("Don't have a JFrog environment?Get one for FREE!"); getFreeLink.addHyperlinkListener(e -> BrowserUtil.browse("https://github.com/jfrog/jfrog-idea-plugin#set-up-a-free-jfrog-environment-in-the-cloud")); - addCenteredHyperlinkLabel(noCredentialsPanel, getFreeLink); + addCenteredComponent(noCredentialsPanel, getFreeLink); // "Read more about the plugin." HyperlinkLabel readMoreLink = new HyperlinkLabel(); readMoreLink.setTextWithHyperlink("Read more about the plugin."); readMoreLink.addHyperlinkListener(e -> BrowserUtil.browse("https://github.com/jfrog/jfrog-idea-plugin#readme")); - addCenteredHyperlinkLabel(noCredentialsPanel, readMoreLink); + addCenteredComponent(noCredentialsPanel, readMoreLink); return createUnsupportedPanel(noCredentialsPanel); } /** - * Add centered HyperlinkLabel to the input panel. + * Add centered {@link JComponent} to the input panel. * - * @param panel - The input panel - * @param hyperlinkLabel - The hyperlink label + * @param panel - The input panel + * @param component - The component to add */ - public static void addCenteredHyperlinkLabel(JPanel panel, HyperlinkLabel hyperlinkLabel) { - hyperlinkLabel.setMaximumSize(hyperlinkLabel.getPreferredSize()); - hyperlinkLabel.setAlignmentX(Component.CENTER_ALIGNMENT); - panel.add(hyperlinkLabel); + public static void addCenteredComponent(JPanel panel, JComponent component) { + component.setMaximumSize(component.getPreferredSize()); + component.setAlignmentX(Component.CENTER_ALIGNMENT); + panel.add(component); } @SuppressWarnings("UnstableApiUsage") diff --git a/src/main/java/com/jfrog/ide/idea/ui/webview/WebviewManager.java b/src/main/java/com/jfrog/ide/idea/ui/webview/WebviewManager.java new file mode 100644 index 00000000..b877001c --- /dev/null +++ b/src/main/java/com/jfrog/ide/idea/ui/webview/WebviewManager.java @@ -0,0 +1,75 @@ +package com.jfrog.ide.idea.ui.webview; + +import com.intellij.ide.BrowserUtil; +import com.intellij.openapi.Disposable; +import com.intellij.openapi.util.Disposer; +import com.intellij.ui.jcef.JBCefBrowser; +import com.intellij.ui.jcef.JBCefBrowserBase; +import com.intellij.ui.jcef.JBCefJSQuery; +import com.jfrog.ide.idea.log.Logger; +import com.jfrog.ide.idea.ui.jcef.message.MessagePacker; +import org.cef.browser.CefBrowser; +import org.cef.browser.CefFrame; +import org.cef.handler.CefLoadHandlerAdapter; + +public class WebviewManager implements Disposable { + private JBCefBrowser jbCefBrowser; + private MessagePacker messagePacker; + + public CefBrowser createBrowser(String pageUri, Runnable onLoadEnd) { + jbCefBrowser = new JBCefBrowser(pageUri); + Disposer.register(this, jbCefBrowser); + CefBrowser cefBrowser = jbCefBrowser.getCefBrowser(); + handleLoadEvent(onLoadEnd); + messagePacker = new MessagePacker(cefBrowser); + return cefBrowser; + } + + private void handleLoadEvent(Runnable onLoadEnd) { + CefBrowser cefBrowser = jbCefBrowser.getCefBrowser(); + JBCefJSQuery openInBrowserQuery = JBCefJSQuery.create((JBCefBrowserBase) jbCefBrowser); + Disposer.register(this, openInBrowserQuery); + openInBrowserQuery.addHandler((link) -> { + BrowserUtil.browse(link); + return null; + }); + + String injectedCall = openInBrowserQuery.inject("link"); + jbCefBrowser.getJBCefClient().addLoadHandler(new CefLoadHandlerAdapter() { + @Override + public void onLoadEnd(CefBrowser browser, CefFrame frame, int httpStatusCode) { + super.onLoadEnd(browser, frame, httpStatusCode); + if (onLoadEnd != null) { + onLoadEnd.run(); + } + + // Add a click event listener to the whole page. Any click on an element (even if it wasn't created + // yet), will trigger this function which sends the link address to openInBrowserQuery. + cefBrowser.executeJavaScript( + "function callback(e) {\n" + + " if (e.target.tagName !== 'A')\n" + + " return;\n" + + " let link = e.target.href;\n" + + injectedCall + + " e.preventDefault();\n" + + "}\n" + + "document.addEventListener('click', callback, false);", + cefBrowser.getURL(), 0); + } + + @Override + public void onLoadError(CefBrowser browser, CefFrame frame, ErrorCode errorCode, String errorText, String failedUrl) { + super.onLoadError(browser, frame, errorCode, errorText, failedUrl); + Logger.getInstance().error("An error occurred while opening the issue details view: " + errorText); + } + }, cefBrowser); + } + + public void sendMessage(Object data) { + messagePacker.send(data); + } + + @Override + public void dispose() { + } +}