diff --git a/src/main/java/dev/openfga/intellijplugin/Notifier.java b/src/main/java/dev/openfga/intellijplugin/Notifier.java deleted file mode 100644 index f87a936..0000000 --- a/src/main/java/dev/openfga/intellijplugin/Notifier.java +++ /dev/null @@ -1,29 +0,0 @@ -package dev.openfga.intellijplugin; - -import com.intellij.notification.NotificationGroupManager; -import com.intellij.notification.NotificationType; -import com.intellij.openapi.project.Project; - -public class Notifier { - - public static void notifyError(String content) { - notifyError((Project) null, content); - } - - public static void notifyError(Project project, String content) { - NotificationGroupManager.getInstance() - .getNotificationGroup("OpenFGA Notifications") - .createNotification(content, NotificationType.ERROR) - .notify(project); - } - public static void notifyError(String title, String content) { - notifyError(null, title, content); - } - - public static void notifyError(Project project, String title, String content) { - NotificationGroupManager.getInstance() - .getNotificationGroup("OpenFGA Notifications") - .createNotification(title, content, NotificationType.ERROR) - .notify(project); - } -} diff --git a/src/main/java/dev/openfga/intellijplugin/OpenFGAIcons.java b/src/main/java/dev/openfga/intellijplugin/OpenFGAIcons.java index 9ea2afc..16a5f98 100644 --- a/src/main/java/dev/openfga/intellijplugin/OpenFGAIcons.java +++ b/src/main/java/dev/openfga/intellijplugin/OpenFGAIcons.java @@ -7,6 +7,8 @@ public interface OpenFGAIcons { Icon FILE = IconLoader.getIcon("/icons/openfga-color-transparent-16x16.svg", OpenFGAIcons.class); + Icon SERVER_NODE = IconLoader.getIcon("/icons/openfga-color-transparent-16x16.svg", OpenFGAIcons.class); + Icon STORE_NODE = IconLoader.getIcon("/icons/openfga-store.svg", OpenFGAIcons.class); Icon TOOL_WINDOW = IconLoader.getIcon("/icons/tool-window.svg", OpenFGAIcons.class); } diff --git a/src/main/java/dev/openfga/intellijplugin/cli/tasks/DslToJsonTask.java b/src/main/java/dev/openfga/intellijplugin/cli/tasks/DslToJsonTask.java index e65047a..f8f3980 100644 --- a/src/main/java/dev/openfga/intellijplugin/cli/tasks/DslToJsonTask.java +++ b/src/main/java/dev/openfga/intellijplugin/cli/tasks/DslToJsonTask.java @@ -1,10 +1,5 @@ package dev.openfga.intellijplugin.cli.tasks; -import dev.openfga.intellijplugin.Notifier; -import dev.openfga.intellijplugin.cli.CliProcess; -import dev.openfga.intellijplugin.cli.CliProcessTask; -import dev.openfga.intellijplugin.cli.CliTaskException; -import dev.openfga.intellijplugin.settings.OpenFGASettingsState; import com.intellij.codeInsight.actions.ReformatCodeProcessor; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.ModalityState; @@ -17,6 +12,12 @@ import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiFile; import com.intellij.psi.PsiManager; +import dev.openfga.intellijplugin.cli.CliProcess; +import dev.openfga.intellijplugin.cli.CliProcessTask; +import dev.openfga.intellijplugin.cli.CliTaskException; +import dev.openfga.intellijplugin.settings.OpenFGASettingsState; +import dev.openfga.intellijplugin.util.notifications.Notifier; +import dev.openfga.intellijplugin.util.notifications.ProjectNotifier; import org.jetbrains.annotations.NotNull; import java.io.File; @@ -32,6 +33,7 @@ public class DslToJsonTask extends Task.Backgroundable implements CliProcessTask private final PsiFile dslFile; private final Path targetPath; private final CliProcess process; + private final Notifier notifier; public static Optional create(@NotNull PsiFile dslFile, @NotNull Path dslFilePath) { var targetName = computeJsonGeneratedFileName(dslFile); @@ -52,6 +54,7 @@ private DslToJsonTask(@NotNull PsiFile dslFile, @NotNull Path dslFilePath, @NotN super(dslFile.getProject(), "Generating json model for " + dslFile.getName(), true); this.dslFile = dslFile; this.targetPath = targetPath; + notifier = new ProjectNotifier(dslFile.getProject()); process = new CliProcess( OpenFGASettingsState.getInstance().requireCli(), @@ -74,7 +77,7 @@ public void run(@NotNull ProgressIndicator indicator) { try { process.start(indicator, this); } catch (CliTaskException e) { - notifyError(dslFile, e.getMessage()); + notifier.notifyError("Error generating json authorization model", e); } } @@ -84,11 +87,11 @@ public void onCancel() { } @Override - public Void onSuccess(File stdOutFile, File stdErrFile) throws IOException, CliTaskException { + public Void onSuccess(File stdOutFile, File stdErrFile) throws IOException { Files.copy(stdOutFile.toPath(), targetPath, StandardCopyOption.REPLACE_EXISTING); ApplicationManager.getApplication().invokeLater( () -> show(new GeneratedFile(dslFile.getProject(), targetPath)), - ModalityState.NON_MODAL); + ModalityState.nonModal()); return null; } @@ -102,10 +105,6 @@ public CliTaskException onFailure(File stdOutFile, File stdErrFile) { } } - private static void notifyError(PsiFile psiFile, String message) { - Notifier.notifyError(psiFile.getProject(), "Error generating json authorization model", message); - } - private void show(GeneratedFile generatedFile) { generatedFile.refreshInTreeView(); generatedFile.openInEditor(); diff --git a/src/main/java/dev/openfga/intellijplugin/sdk/OpenFgaApiClient.java b/src/main/java/dev/openfga/intellijplugin/sdk/OpenFgaApiClient.java new file mode 100644 index 0000000..8b4db6e --- /dev/null +++ b/src/main/java/dev/openfga/intellijplugin/sdk/OpenFgaApiClient.java @@ -0,0 +1,23 @@ +package dev.openfga.intellijplugin.sdk; + +import dev.openfga.intellijplugin.servers.model.Server; +import dev.openfga.intellijplugin.servers.util.ServersUtil; +import dev.openfga.sdk.api.model.Store; +import dev.openfga.sdk.errors.FgaInvalidParameterException; + +import java.util.List; +import java.util.concurrent.CompletableFuture; + +public interface OpenFgaApiClient { + + CompletableFuture> listStores(); + + static OpenFgaApiClient ForServer(Server server) { + try { + var openFgaClient = ServersUtil.createClient(server); + return new SdkClient(openFgaClient); + } catch (FgaInvalidParameterException e) { + throw new RuntimeException(e.getMessage(), e); + } + } +} diff --git a/src/main/java/dev/openfga/intellijplugin/sdk/SdkClient.java b/src/main/java/dev/openfga/intellijplugin/sdk/SdkClient.java new file mode 100644 index 0000000..19b33fa --- /dev/null +++ b/src/main/java/dev/openfga/intellijplugin/sdk/SdkClient.java @@ -0,0 +1,52 @@ +package dev.openfga.intellijplugin.sdk; + +import dev.openfga.sdk.api.client.OpenFgaClient; +import dev.openfga.sdk.api.client.model.ClientListStoresResponse; +import dev.openfga.sdk.api.configuration.ClientListStoresOptions; +import dev.openfga.sdk.api.model.Store; +import dev.openfga.sdk.errors.FgaInvalidParameterException; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Stream; + +class SdkClient implements OpenFgaApiClient { + + private static final int STORES_PAGE_SIZE = 50; + + private final OpenFgaClient fgaClient; + + public SdkClient(OpenFgaClient fgaClient) { + this.fgaClient = fgaClient; + } + + @Override + public CompletableFuture> listStores() { + return listStores(new ClientListStoresOptions().pageSize(STORES_PAGE_SIZE)); + } + + private CompletableFuture> listStores(ClientListStoresOptions options) { + try { + return fgaClient.listStores(options).thenCompose(this::listNextStores); + } catch (FgaInvalidParameterException e) { + throw new RuntimeException(e.getMessage(), e); + } + } + + private CompletableFuture> listNextStores(ClientListStoresResponse response) { + var continuationToken = response.getContinuationToken(); + if (continuationToken == null || continuationToken.isBlank()) { + return CompletableFuture.completedFuture(response.getStores()); + } + + try { + return fgaClient + .listStores(new ClientListStoresOptions().pageSize(STORES_PAGE_SIZE).continuationToken(continuationToken)) + .thenCompose((ClientListStoresResponse nextResponse) -> + listNextStores(nextResponse).thenApply(nextStores -> Stream.concat(response.getStores().stream(), nextStores.stream()).toList())); + } catch (FgaInvalidParameterException e) { + throw new RuntimeException(e.getMessage(), e); + } + } + +} diff --git a/src/main/java/dev/openfga/intellijplugin/servers/model/Oidc.java b/src/main/java/dev/openfga/intellijplugin/servers/model/Oidc.java index 969ff6a..3d228c5 100644 --- a/src/main/java/dev/openfga/intellijplugin/servers/model/Oidc.java +++ b/src/main/java/dev/openfga/intellijplugin/servers/model/Oidc.java @@ -6,4 +6,5 @@ public record Oidc( String clientSecret, String scope ) { + public static final Oidc EMPTY = new Oidc("", "", "", ""); } diff --git a/src/main/java/dev/openfga/intellijplugin/servers/model/Server.java b/src/main/java/dev/openfga/intellijplugin/servers/model/Server.java index 88de2dd..9787110 100644 --- a/src/main/java/dev/openfga/intellijplugin/servers/model/Server.java +++ b/src/main/java/dev/openfga/intellijplugin/servers/model/Server.java @@ -1,62 +1,28 @@ package dev.openfga.intellijplugin.servers.model; -import com.intellij.credentialStore.CredentialAttributes; -import com.intellij.credentialStore.CredentialAttributesKt; -import com.intellij.credentialStore.Credentials; -import com.intellij.ide.passwordSafe.PasswordSafe; -import org.jetbrains.annotations.NotNull; - +import java.util.Objects; import java.util.UUID; -public class Server { +public final class Server { + private String id; private String name; + private String url; private AuthenticationMethod authenticationMethod = AuthenticationMethod.NONE; + private String apiToken; + private Oidc oidc = Oidc.EMPTY; public Server() { - this("new server"); - } - - public Server(String name) { this.id = UUID.randomUUID().toString(); - this.name = name; - } - - public String loadUrl() { - var credentials = getCredentials(CredentialKey.URL); - if (credentials == null) { - return ""; - } - return credentials.getPasswordAsString(); } - public void storeUrl(String url) { - var attributes = getCredentialAttributes(CredentialKey.URL); - PasswordSafe.getInstance().set(attributes, new Credentials(id, url)); - } - - public String loadApiToken() { - var credentials = getCredentials(CredentialKey.API_TOKEN); - if (credentials == null) { - return ""; - } - return credentials.getPasswordAsString(); - } - - public void storeApiToken(String token) { - var attributes = getCredentialAttributes(CredentialKey.API_TOKEN); - PasswordSafe.getInstance().set(attributes, new Credentials(id, token)); - } - - public Credentials getCredentials(String keySuffix) { - CredentialAttributes attributes = getCredentialAttributes(keySuffix); - return PasswordSafe.getInstance().get(attributes); - } - - @NotNull - private CredentialAttributes getCredentialAttributes(String keySuffix) { - var key = id + "_" + keySuffix; - return new CredentialAttributes(CredentialAttributesKt.generateServiceName("OpenFGAServer", key)); + public Server(String id, String name, String url, AuthenticationMethod authenticationMethod, String apiToken, Oidc oidc) { + this.id = id; + this.name = name; + this.url = url; + this.authenticationMethod = authenticationMethod; + this.apiToken = apiToken; + this.oidc = oidc; } public String getId() { @@ -75,35 +41,39 @@ public void setName(String name) { this.name = name; } + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + public AuthenticationMethod getAuthenticationMethod() { return authenticationMethod; } public void setAuthenticationMethod(AuthenticationMethod authenticationMethod) { + if (authenticationMethod == null) { + authenticationMethod = AuthenticationMethod.NONE; + } this.authenticationMethod = authenticationMethod; } - public Oidc loadOidc() { - var credentials = getCredentials(CredentialKey.OIDC_CLIENT); - var clientId = credentials != null ? credentials.getUserName() : ""; - var clientSecret = credentials != null ? credentials.getPasswordAsString() : ""; - - credentials = getCredentials(CredentialKey.OIDC_TOKEN_ENDPOINT); - var tokenEndpoint = credentials != null ? credentials.getPasswordAsString() : ""; + public String getApiToken() { + return apiToken; + } - credentials = getCredentials(CredentialKey.OIDC_SCOPE); - var audience = credentials != null ? credentials.getPasswordAsString() : ""; + public void setApiToken(String apiToken) { + this.apiToken = apiToken; + } - return new Oidc(tokenEndpoint, clientId, clientSecret, audience); + public Oidc getOidc() { + return oidc; } - public void storeOidc(Oidc oidc) { - var attributes = getCredentialAttributes(CredentialKey.OIDC_CLIENT); - PasswordSafe.getInstance().set(attributes, new Credentials(oidc.clientId(), oidc.clientSecret())); - attributes = getCredentialAttributes(CredentialKey.OIDC_TOKEN_ENDPOINT); - PasswordSafe.getInstance().set(attributes, new Credentials(id, oidc.tokenEndpoint())); - attributes = getCredentialAttributes(CredentialKey.OIDC_SCOPE); - PasswordSafe.getInstance().set(attributes, new Credentials(id, oidc.scope())); + public void setOidc(Oidc oidc) { + this.oidc = oidc; } @Override @@ -111,12 +81,16 @@ public String toString() { return name; } - private interface CredentialKey { + @Override + public boolean equals(Object object) { + if (this == object) return true; + if (object == null || getClass() != object.getClass()) return false; + Server that = (Server) object; + return Objects.equals(id, that.id); + } - String URL = "url"; - String API_TOKEN = "apiToken"; - String OIDC_CLIENT = "oidc_client"; - String OIDC_TOKEN_ENDPOINT = "oidc_token_endpoint"; - String OIDC_SCOPE = "oidc_scope"; + @Override + public int hashCode() { + return Objects.hash(id); } } diff --git a/src/main/java/dev/openfga/intellijplugin/servers/model/ServersState.java b/src/main/java/dev/openfga/intellijplugin/servers/model/ServersState.java deleted file mode 100644 index dc4ab8f..0000000 --- a/src/main/java/dev/openfga/intellijplugin/servers/model/ServersState.java +++ /dev/null @@ -1,17 +0,0 @@ -package dev.openfga.intellijplugin.servers.model; - -import java.util.ArrayList; -import java.util.List; - -public class ServersState { - - private List servers = new ArrayList<>(); - - public List getServers() { - return servers; - } - - public void setServers(List servers) { - this.servers = servers; - } -} diff --git a/src/main/java/dev/openfga/intellijplugin/servers/model/SharedKeysMetadata.java b/src/main/java/dev/openfga/intellijplugin/servers/model/SharedKeysMetadata.java deleted file mode 100644 index 3df3bad..0000000 --- a/src/main/java/dev/openfga/intellijplugin/servers/model/SharedKeysMetadata.java +++ /dev/null @@ -1,13 +0,0 @@ -package dev.openfga.intellijplugin.servers.model; - -public class SharedKeysMetadata { - private int count = 0; - - public int getCount() { - return count; - } - - public void setCount(int count) { - this.count = count; - } -} diff --git a/src/main/java/dev/openfga/intellijplugin/servers/service/OpenFGAServers.java b/src/main/java/dev/openfga/intellijplugin/servers/service/OpenFGAServers.java index a92344b..8e6828b 100644 --- a/src/main/java/dev/openfga/intellijplugin/servers/service/OpenFGAServers.java +++ b/src/main/java/dev/openfga/intellijplugin/servers/service/OpenFGAServers.java @@ -1,25 +1,29 @@ package dev.openfga.intellijplugin.servers.service; import dev.openfga.intellijplugin.servers.model.Server; -import dev.openfga.intellijplugin.servers.model.ServersState; +import dev.openfga.intellijplugin.util.ListUtil; import com.intellij.openapi.application.ApplicationManager; -import com.intellij.openapi.components.*; +import com.intellij.openapi.components.PersistentStateComponent; +import com.intellij.openapi.components.Service; +import com.intellij.openapi.components.State; +import com.intellij.openapi.components.Storage; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.Collections; -import java.util.List; +import java.util.*; import java.util.function.IntUnaryOperator; +import java.util.function.Predicate; @Service(Service.Level.APP) @State( name = "OpenFGAServers.State", storages = { - @Storage(value = "openfga-servers.xml", roamingType = RoamingType.DISABLED) + @Storage(value = "openfga-servers.xml") } ) public final class OpenFGAServers implements PersistentStateComponent { + private final List servers = new ArrayList<>(); private ServersState state = new ServersState(); public static OpenFGAServers getInstance() { @@ -27,15 +31,42 @@ public static OpenFGAServers getInstance() { } public List getServers() { - return state.getServers(); + return servers; + } + + @Nullable + @Override + public ServersState getState() { + return state; + } + + @Override + public void loadState(@NotNull ServersState state) { + this.state = state; + var loadedServers = this.state.getServers().stream() + .map(ServerState::toModel) + .toList(); + servers.clear(); + servers.addAll(loadedServers); } public void add(Server server) { - state.getServers().add(server); + var serverState = new ServerState(); + serverState.setId(server.getId()); + serverState.populateWith(server); + state.getServers().add(serverState); + servers.add(server); + } + + public void update(Server server) { + var serverState = ListUtil.get(state.getServers(), withId(server.getId())) + .orElseThrow(() -> new NoSuchElementException("could not found server state with id " + server.getId())); + serverState.populateWith(server); } public void remove(Server server) { - state.getServers().remove(server); + ListUtil.remove(state.getServers(), withId(server.getId())); + servers.remove(server); } public int moveDown(Server server) { @@ -47,22 +78,17 @@ public int moveUp(Server server) { } private int move(Server server, IntUnaryOperator f) { - var index = state.getServers().indexOf(server); + var index = ListUtil.indexOf(state.getServers(), withId(server.getId())).orElse(-1); var targetIndex = f.applyAsInt(index); if (targetIndex < 0 || targetIndex >= state.getServers().size()) { return index; } Collections.swap(state.getServers(), index, targetIndex); + loadState(state); return targetIndex; } - @Override - public @Nullable ServersState getState() { - return state; - } - - @Override - public void loadState(@NotNull ServersState state) { - this.state = state; + private static Predicate withId(String id) { + return serverState -> Objects.equals(serverState.getId(), id); } } diff --git a/src/main/java/dev/openfga/intellijplugin/servers/service/ServerState.java b/src/main/java/dev/openfga/intellijplugin/servers/service/ServerState.java new file mode 100644 index 0000000..c300b64 --- /dev/null +++ b/src/main/java/dev/openfga/intellijplugin/servers/service/ServerState.java @@ -0,0 +1,144 @@ +package dev.openfga.intellijplugin.servers.service; + +import dev.openfga.intellijplugin.servers.model.AuthenticationMethod; +import dev.openfga.intellijplugin.servers.model.Oidc; +import dev.openfga.intellijplugin.servers.model.Server; +import com.intellij.credentialStore.CredentialAttributes; +import com.intellij.credentialStore.CredentialAttributesKt; +import com.intellij.credentialStore.Credentials; +import com.intellij.ide.passwordSafe.PasswordSafe; +import org.jetbrains.annotations.NotNull; + +import java.util.Objects; + +public class ServerState { + + private String id; + private String name; + private AuthenticationMethod authenticationMethod = AuthenticationMethod.NONE; + + public ServerState() { +// this("new server"); + } + +// public ServerState(String name) { +// this.id = UUID.randomUUID().toString(); +// this.name = name; +// } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public AuthenticationMethod getAuthenticationMethod() { + return authenticationMethod; + } + + public void setAuthenticationMethod(AuthenticationMethod authenticationMethod) { + if (authenticationMethod == null) { + authenticationMethod = AuthenticationMethod.NONE; + } + this.authenticationMethod = authenticationMethod; + } + + public Server toModel() { + return new Server(id, name, loadUrl(), authenticationMethod, loadApiToken(), loadOidc()); + } + + public void populateWith(Server server) { + if (!Objects.equals(server.getId(), id)) { + throw new IllegalArgumentException("invalid server id " + server.getId() + ", expected is " + id); + } + setName(server.getName()); + storeUrl(server.getUrl()); + setAuthenticationMethod(server.getAuthenticationMethod()); + storeApiToken(server.getApiToken()); + storeOidc(server.getOidc()); + } + + public String loadUrl() { + var credentials = getCredentials(CredentialKey.URL); + if (credentials == null) { + return ""; + } + return credentials.getPasswordAsString(); + } + + public void storeUrl(String url) { + var attributes = getCredentialAttributes(CredentialKey.URL); + PasswordSafe.getInstance().set(attributes, new Credentials(id, url)); + } + + public String loadApiToken() { + var credentials = getCredentials(CredentialKey.API_TOKEN); + if (credentials == null) { + return ""; + } + return credentials.getPasswordAsString(); + } + + public void storeApiToken(String token) { + var attributes = getCredentialAttributes(CredentialKey.API_TOKEN); + PasswordSafe.getInstance().set(attributes, new Credentials(id, token)); + } + + public Oidc loadOidc() { + var credentials = getCredentials(CredentialKey.OIDC_CLIENT); + var clientId = credentials != null ? credentials.getUserName() : ""; + var clientSecret = credentials != null ? credentials.getPasswordAsString() : ""; + + credentials = getCredentials(CredentialKey.OIDC_TOKEN_ENDPOINT); + var authority = credentials != null ? credentials.getPasswordAsString() : ""; + + credentials = getCredentials(CredentialKey.OIDC_SCOPE); + var audience = credentials != null ? credentials.getPasswordAsString() : ""; + + return new Oidc(authority, clientId, clientSecret, audience); + } + + public void storeOidc(Oidc oidc) { + var attributes = getCredentialAttributes(CredentialKey.OIDC_CLIENT); + PasswordSafe.getInstance().set(attributes, new Credentials(oidc.clientId(), oidc.clientSecret())); + attributes = getCredentialAttributes(CredentialKey.OIDC_TOKEN_ENDPOINT); + PasswordSafe.getInstance().set(attributes, new Credentials(id, oidc.tokenEndpoint())); + attributes = getCredentialAttributes(CredentialKey.OIDC_SCOPE); + PasswordSafe.getInstance().set(attributes, new Credentials(id, oidc.scope())); + } + + private Credentials getCredentials(String keySuffix) { + CredentialAttributes attributes = getCredentialAttributes(keySuffix); + return PasswordSafe.getInstance().get(attributes); + } + + @NotNull + private CredentialAttributes getCredentialAttributes(String keySuffix) { + var key = id + "_" + keySuffix; + return new CredentialAttributes(CredentialAttributesKt.generateServiceName("OpenFGAServer", key)); + } + + @Override + public String toString() { + return name; + } + + private interface CredentialKey { + + String URL = "url"; + String API_TOKEN = "apiToken"; + String OIDC_CLIENT = "oidc_client"; + String OIDC_TOKEN_ENDPOINT = "oidc_token_endpoint"; + String OIDC_SCOPE = "oidc_scope"; + } +} diff --git a/src/main/java/dev/openfga/intellijplugin/servers/service/ServersState.java b/src/main/java/dev/openfga/intellijplugin/servers/service/ServersState.java new file mode 100644 index 0000000..e8bb1d0 --- /dev/null +++ b/src/main/java/dev/openfga/intellijplugin/servers/service/ServersState.java @@ -0,0 +1,18 @@ +package dev.openfga.intellijplugin.servers.service; + + +import java.util.ArrayList; +import java.util.List; + +public class ServersState { + + private List servers = new ArrayList<>(); + + public List getServers() { + return servers; + } + + public void setServers(List servers) { + this.servers = servers; + } +} diff --git a/src/main/java/dev/openfga/intellijplugin/servers/ui/AddEditDeleteStringListPanel.java b/src/main/java/dev/openfga/intellijplugin/servers/ui/AddEditDeleteStringListPanel.java deleted file mode 100644 index abb6c51..0000000 --- a/src/main/java/dev/openfga/intellijplugin/servers/ui/AddEditDeleteStringListPanel.java +++ /dev/null @@ -1,40 +0,0 @@ -package dev.openfga.intellijplugin.servers.ui; - -import com.intellij.openapi.ui.Messages; -import com.intellij.openapi.ui.NonEmptyInputValidator; -import com.intellij.ui.AddEditDeleteListPanel; -import org.jetbrains.annotations.Nullable; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -public class AddEditDeleteStringListPanel extends AddEditDeleteListPanel { - - private final String inputDialogMessage; - - public AddEditDeleteStringListPanel(String title, String inputDialogMessage) { - super(title, new ArrayList<>()); - this.inputDialogMessage = inputDialogMessage; - } - - @Override - protected @Nullable String editSelectedItem(String item) { - var intput = Messages.showInputDialog(this, inputDialogMessage, "Edit", null, item, new NonEmptyInputValidator()); - return intput != null ? intput : item; - } - - @Override - protected @Nullable String findItemToAdd() { - return Messages.showInputDialog(this, inputDialogMessage, "Add", null, "", new NonEmptyInputValidator()); - } - - public List getItems() { - return Collections.list(myListModel.elements()); - } - - public void setItems(List items) { - myListModel.clear(); - myListModel.addAll(items); - } -} diff --git a/src/main/java/dev/openfga/intellijplugin/servers/ui/OpenFGAToolWindowContent.java b/src/main/java/dev/openfga/intellijplugin/servers/ui/OpenFGAToolWindowContent.java deleted file mode 100644 index 704bd53..0000000 --- a/src/main/java/dev/openfga/intellijplugin/servers/ui/OpenFGAToolWindowContent.java +++ /dev/null @@ -1,120 +0,0 @@ -package dev.openfga.intellijplugin.servers.ui; - -import dev.openfga.intellijplugin.servers.service.OpenFGAServers; -import com.intellij.openapi.ui.Messages; -import com.intellij.openapi.wm.ToolWindow; -import com.intellij.ui.ToolbarDecorator; -import com.intellij.ui.treeStructure.Tree; -import org.jetbrains.annotations.NotNull; - -import javax.swing.*; -import javax.swing.tree.TreePath; -import javax.swing.tree.TreeSelectionModel; -import java.awt.*; -import java.util.Arrays; -import java.util.Optional; - -class OpenFGAToolWindowContent { - private final ToolWindow toolWindow; - private Tree tree; - - OpenFGAToolWindowContent(ToolWindow toolWindow) { - this.toolWindow = toolWindow; - } - - public JComponent getContentPanel() { - var mainPanel = new JPanel(new BorderLayout()); - - var root = new RootTreeNode(); - tree = new Tree(root); - tree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION); - - var toolbarDecorator = ToolbarDecorator.createDecorator(tree); - - - toolbarDecorator.setAddAction(anActionButton -> { - var server = ServerDialog.showAddServerDialog(toolWindow); - if (server == null) { - return; - } - - servers().add(server); - var newNode = new ServerTreeNode(server); - root.add(newNode); - var treePath = new TreePath(newNode.getPath()); - tree.setSelectionPath(treePath); - tree.scrollPathToVisible(treePath); - tree.updateUI(); - - }); - toolbarDecorator.setEditActionUpdater(updater -> getSelectedNode().isPresent()); - toolbarDecorator.setEditAction(anActionButton -> getSelectedNode() - .ifPresent(node -> { - ServerDialog.showEditServerDialog(toolWindow, node.getServer()); - tree.updateUI(); - })); - toolbarDecorator.setRemoveActionUpdater(updater -> getSelectedNode().isPresent()); - toolbarDecorator.setRemoveAction(anActionButton -> { - var selectedNode = getSelectedNode(); - selectedNode.ifPresent(node -> { - var server = node.getServer(); - - var title = "Confirm OpenFGA Server Deletion"; - var message = "Deleting the OpenFGA server '" + server.getName() + "' is not reversible. Do you confirm the server deletion?"; - Messages.showYesNoDialog(mainPanel, message, title, null); - - servers().remove(server); - var childIndex = root.getIndex(node); - var pathToSelect = new TreePath(root.getPath()); - if (root.getChildCount() != 0) { - var indexToSelect = childIndex >= root.getChildCount() ? root.getChildCount() - 1 : childIndex; - var serverNode = (ServerTreeNode) root.getChildAt(indexToSelect); - pathToSelect = new TreePath(serverNode.getPath()); - } - tree.setSelectionPath(pathToSelect); - tree.updateUI(); - }); - }); - - toolbarDecorator.setMoveDownAction(anActionButton -> { - getSelectedNode().ifPresent(node -> { - var indexToSelect = servers().moveDown(node.getServer()); - root.reload(); - var serverNode = (ServerTreeNode) root.getChildAt(indexToSelect); - var pathToSelect = new TreePath(serverNode.getPath()); - tree.setSelectionPath(pathToSelect); - tree.updateUI(); - }); - }); - - toolbarDecorator.setMoveUpAction(anActionButton -> { - getSelectedNode().ifPresent(node -> { - var indexToSelect = servers().moveUp(node.getServer()); - root.reload(); - var serverNode = (ServerTreeNode) root.getChildAt(indexToSelect); - var pathToSelect = new TreePath(serverNode.getPath()); - tree.setSelectionPath(pathToSelect); - tree.updateUI(); - }); - }); - - mainPanel.add(toolbarDecorator.createPanel(), BorderLayout.CENTER); - return mainPanel; - } - - private static OpenFGAServers servers() { - return OpenFGAServers.getInstance(); - } - - @NotNull - private Optional getSelectedNode() { - return Arrays.stream(getSelectedNodes()).findFirst(); - } - - @NotNull - private ServerTreeNode[] getSelectedNodes() { - return tree.getSelectedNodes(ServerTreeNode.class, null); - } - - -} diff --git a/src/main/java/dev/openfga/intellijplugin/servers/ui/OpenFGAToolWindowFactory.java b/src/main/java/dev/openfga/intellijplugin/servers/ui/OpenFGAToolWindowFactory.java index 38dd993..955bcbb 100644 --- a/src/main/java/dev/openfga/intellijplugin/servers/ui/OpenFGAToolWindowFactory.java +++ b/src/main/java/dev/openfga/intellijplugin/servers/ui/OpenFGAToolWindowFactory.java @@ -1,18 +1,19 @@ package dev.openfga.intellijplugin.servers.ui; +import dev.openfga.intellijplugin.servers.service.OpenFGAServers; import com.intellij.openapi.project.Project; import com.intellij.openapi.wm.ToolWindow; import com.intellij.openapi.wm.ToolWindowFactory; -import com.intellij.ui.content.Content; import com.intellij.ui.content.ContentFactory; import org.jetbrains.annotations.NotNull; public class OpenFGAToolWindowFactory implements ToolWindowFactory { @Override public void createToolWindowContent(@NotNull Project project, @NotNull ToolWindow toolWindow) { - - var toolWindowContent = new OpenFGAToolWindowContent(toolWindow); - Content content = ContentFactory.getInstance().createContent(toolWindowContent.getContentPanel(), "", false); + var content = ContentFactory.getInstance().createContent( + new ServersTreePanel(toolWindow, OpenFGAServers.getInstance()), + "Servers", + false); toolWindow.getContentManager().addContent(content); } } diff --git a/src/main/java/dev/openfga/intellijplugin/servers/ui/RootTreeNode.java b/src/main/java/dev/openfga/intellijplugin/servers/ui/RootTreeNode.java deleted file mode 100644 index 4cec701..0000000 --- a/src/main/java/dev/openfga/intellijplugin/servers/ui/RootTreeNode.java +++ /dev/null @@ -1,27 +0,0 @@ -package dev.openfga.intellijplugin.servers.ui; - -import dev.openfga.intellijplugin.servers.model.Server; -import dev.openfga.intellijplugin.servers.service.OpenFGAServers; -import com.intellij.openapi.application.ApplicationManager; - -import javax.swing.tree.DefaultMutableTreeNode; -import java.util.List; - -class RootTreeNode extends DefaultMutableTreeNode { - - public RootTreeNode() { - super("OpenFGA Servers"); - reload(); - } - - private List getServers() { - return ApplicationManager.getApplication().getService(OpenFGAServers.class).getServers(); - } - - void reload() { - removeAllChildren(); - for (Server server : getServers()) { - this.add(new ServerTreeNode(server)); - } - } -} diff --git a/src/main/java/dev/openfga/intellijplugin/servers/ui/ServerDialog.java b/src/main/java/dev/openfga/intellijplugin/servers/ui/ServerDialog.java index 6a8e7a6..51ac25d 100644 --- a/src/main/java/dev/openfga/intellijplugin/servers/ui/ServerDialog.java +++ b/src/main/java/dev/openfga/intellijplugin/servers/ui/ServerDialog.java @@ -4,14 +4,17 @@ import dev.openfga.intellijplugin.servers.model.Oidc; import dev.openfga.intellijplugin.servers.model.Server; import dev.openfga.intellijplugin.servers.util.ServersUtil; +import dev.openfga.intellijplugin.util.notifications.ToolWindowNotifier; import com.intellij.icons.AllIcons; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.progress.ProgressManager; import com.intellij.openapi.progress.Task; -import com.intellij.openapi.ui.*; +import com.intellij.openapi.ui.ComboBox; +import com.intellij.openapi.ui.DialogPanel; +import com.intellij.openapi.ui.DialogWrapper; +import com.intellij.openapi.ui.ValidationInfo; import com.intellij.openapi.wm.ToolWindow; -import com.intellij.openapi.wm.ToolWindowManager; import com.intellij.ui.AnimatedIcon; import com.intellij.ui.components.ActionLink; import com.intellij.ui.components.JBLabel; @@ -23,14 +26,15 @@ import javax.swing.*; import java.awt.*; +import java.awt.event.FocusAdapter; +import java.awt.event.FocusEvent; import java.awt.event.ItemEvent; +import java.util.Optional; public class ServerDialog extends DialogWrapper { private static final Logger logger = Logger.getInstance(ServerDialog.class); private final ToolWindow toolWindow; private final Server server; - - private DialogPanel dialogPanel; private final JBTextField nameField = new JBTextField(); private final JBTextField urlField = new JBTextField(); private final ComboBox authenticationMethodField = new ComboBox<>(AuthenticationMethod.values()); @@ -42,21 +46,37 @@ public class ServerDialog extends DialogWrapper { private final ActionLink connectionTestButton = new ActionLink("Test connexion"); private final JBLabel connectionTestLabel = new JBLabel(); - protected ServerDialog(ToolWindow toolWindow) { - this(toolWindow, null); - } + private final ToolWindowNotifier notifier; public ServerDialog(ToolWindow toolWindow, @Nullable Server server) { super(true); this.toolWindow = toolWindow; + notifier = new ToolWindowNotifier(toolWindow); this.server = server != null ? server : new Server(); setTitle(server != null ? "Edit Server" : "Add Server"); init(); + if (server == null) { + urlField.setText("http://localhost:8080"); + } + urlField.addFocusListener(new FocusAdapter() { + @Override + public void focusGained(FocusEvent e) { + urlField.selectAll(); + } + }); + } + + @Override + protected @Nullable ValidationInfo doValidate() { + if (nameField.getText().isBlank()) { + return new ValidationInfo("Name is required", nameField); + } + return null; } @Override protected @Nullable JComponent createCenterPanel() { - dialogPanel = new DialogPanel(new MigLayout("fillx,wrap 2", "[left]rel[grow,fill]")); + DialogPanel dialogPanel = new DialogPanel(new MigLayout("fillx,wrap 2", "[left]rel[grow,fill]")); dialogPanel.add(new JBLabel("Name")); dialogPanel.add(nameField); @@ -77,9 +97,9 @@ public ServerDialog(ToolWindow toolWindow, @Nullable Server server) { connectionTestButton.addActionListener(evt -> { var testServer = writeToModel(new Server()); - connectionTestLabel.setText("testing connection with " + testServer.loadUrl()); + connectionTestLabel.setText("testing connection with " + testServer.getUrl()); connectionTestLabel.setIcon(new AnimatedIcon.Default()); - ProgressManager.getInstance().run(new ServerDialog.ConnectionTestTask(testServer)); + ProgressManager.getInstance().run(new dev.openfga.intellijplugin.servers.ui.ServerDialog.ConnectionTestTask(testServer)); }); loadModel(); @@ -145,10 +165,10 @@ private JPanel createOidcPanel() { private void loadModel() { nameField.setText(server.getName()); - urlField.setText(server.loadUrl()); + urlField.setText(server.getUrl()); authenticationMethodField.setSelectedItem(server.getAuthenticationMethod()); - apiTokenField.setText(server.loadApiToken()); - var oidc = server.loadOidc(); + apiTokenField.setText(server.getApiToken()); + var oidc = server.getOidc(); oidcClientIdField.setText(oidc.clientId()); oidcClientSecretField.setText(oidc.clientSecret()); oidcTokenEndpointField.setText(oidc.tokenEndpoint()); @@ -162,14 +182,14 @@ private Server updateModel() { private Server writeToModel(Server server) { server.setName(nameField.getText()); - server.storeUrl(urlField.getText()); + server.setUrl(urlField.getText()); var authenticationMethod = authenticationMethodField.getItem(); server.setAuthenticationMethod(authenticationMethod); switch (authenticationMethod) { case NONE -> { } - case API_TOKEN -> server.storeApiToken(new String(apiTokenField.getPassword())); - case OIDC -> server.storeOidc(new Oidc( + case API_TOKEN -> server.setApiToken(new String(apiTokenField.getPassword())); + case OIDC -> server.setOidc(new Oidc( oidcTokenEndpointField.getText(), oidcClientIdField.getText(), new String(oidcClientSecretField.getPassword()), oidcScopeField.getText() @@ -178,14 +198,19 @@ private Server writeToModel(Server server) { return server; } - public static Server showAddServerDialog(ToolWindow toolWindow) { - var dialog = new ServerDialog(toolWindow); - return dialog.showAndGet() ? dialog.updateModel() : null; + public static Optional showAddServerDialog(ToolWindow toolWindow) { + return showDialog(toolWindow, null); } - public static Server showEditServerDialog(ToolWindow toolWindow, Server server) { + public static Optional showEditServerDialog(ToolWindow toolWindow, Server server) { + return showDialog(toolWindow, server); + } + + private static Optional showDialog(ToolWindow toolWindow, Server server) { var dialog = new ServerDialog(toolWindow, server); - return dialog.showAndGet() ? dialog.updateModel() : null; + return dialog.showAndGet() + ? Optional.of(dialog.updateModel()) + : Optional.empty(); } @@ -236,9 +261,7 @@ private void taskFailed(String errorMessage, Throwable throwable) { if (throwable != null) { logger.warn(errorMessage, throwable); } - ToolWindowManager - .getInstance(toolWindow.getProject()) - .notifyByBalloon(toolWindow.getId(), MessageType.ERROR, errorMessage); + notifier.notifyError(errorMessage); } private void taskSucceeded() { @@ -247,7 +270,6 @@ private void taskSucceeded() { connectionTestLabel.setIcon(AllIcons.RunConfigurations.TestPassed); }); } - } } diff --git a/src/main/java/dev/openfga/intellijplugin/servers/ui/ServerTreeNode.java b/src/main/java/dev/openfga/intellijplugin/servers/ui/ServerTreeNode.java deleted file mode 100644 index 59b0c6c..0000000 --- a/src/main/java/dev/openfga/intellijplugin/servers/ui/ServerTreeNode.java +++ /dev/null @@ -1,18 +0,0 @@ -package dev.openfga.intellijplugin.servers.ui; - -import dev.openfga.intellijplugin.servers.model.Server; - -import javax.swing.tree.DefaultMutableTreeNode; - -class ServerTreeNode extends DefaultMutableTreeNode { - private final Server server; - - public ServerTreeNode(Server server) { - super(server, false); - this.server = server; - } - - public Server getServer() { - return server; - } -} diff --git a/src/main/java/dev/openfga/intellijplugin/servers/ui/ServersTreePanel.java b/src/main/java/dev/openfga/intellijplugin/servers/ui/ServersTreePanel.java new file mode 100644 index 0000000..abbf58c --- /dev/null +++ b/src/main/java/dev/openfga/intellijplugin/servers/ui/ServersTreePanel.java @@ -0,0 +1,217 @@ +package dev.openfga.intellijplugin.servers.ui; + +import dev.openfga.intellijplugin.servers.model.Server; +import dev.openfga.intellijplugin.servers.service.OpenFGAServers; +import dev.openfga.intellijplugin.servers.ui.tree.*; +import dev.openfga.intellijplugin.util.notifications.ToolWindowNotifier; +import com.intellij.openapi.progress.ProgressIndicator; +import com.intellij.openapi.progress.ProgressManager; +import com.intellij.openapi.progress.Task; +import com.intellij.openapi.ui.Messages; +import com.intellij.openapi.wm.ToolWindow; +import com.intellij.ui.AnActionButton; +import com.intellij.ui.ToolbarDecorator; +import com.intellij.ui.treeStructure.Tree; +import org.jetbrains.annotations.NotNull; + +import javax.swing.*; +import javax.swing.tree.TreePath; +import javax.swing.tree.TreeSelectionModel; +import java.awt.*; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.util.Arrays; +import java.util.Optional; +import java.util.function.Function; + +import static dev.openfga.intellijplugin.servers.ui.tree.NodeType.SERVER_NODE; + +public class ServersTreePanel extends JPanel { + + private final ToolWindow toolWindow; + private final ToolWindowNotifier toolWindowNotifier; + private final OpenFGAServers servers; + private final Tree tree; + private final RootNode root; + private final OpenFgaTreeModel treeModel; + + + public ServersTreePanel(ToolWindow toolWindow, OpenFGAServers servers) { + super(new BorderLayout()); + this.toolWindow = toolWindow; + this.toolWindowNotifier = new ToolWindowNotifier(toolWindow); + this.servers = servers; + treeModel = new OpenFgaTreeModel(toolWindowNotifier); + tree = new Tree(treeModel); + root = treeModel.getRootNode(); + tree.setExpandableItemsEnabled(true); + tree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION); + tree.setCellRenderer(new CellRenderer()); + tree.addTreeWillExpandListener(treeModel); + + var toolbarDecorator = ToolbarDecorator.createDecorator(tree); + + toolbarDecorator.setAddActionUpdater(updater -> { + var selectedNode = getSelectedNode(); + return selectedNode.isEmpty() || selectedNode.get().is(NodeType.ROOT_NODE); + + }); + toolbarDecorator.setAddAction(this::addAction); + + toolbarDecorator.setEditActionUpdater(updater -> isSelectedNode(SERVER_NODE)); + toolbarDecorator.setEditAction(this::editAction); + + toolbarDecorator.setRemoveActionUpdater(updater -> isSelectedNode(SERVER_NODE)); + toolbarDecorator.setRemoveAction(this::removeAction); + + toolbarDecorator.setMoveDownActionUpdater(updater -> isSelectedNode(SERVER_NODE)); + toolbarDecorator.setMoveDownAction(this::moveDownAction); + toolbarDecorator.setMoveUpActionUpdater(updater -> isSelectedNode(SERVER_NODE)); + toolbarDecorator.setMoveUpAction(this::moveUpAction); + + add(toolbarDecorator.createPanel(), BorderLayout.CENTER); + + tree.addMouseListener(new MouseAdapter() { + @Override + public void mousePressed(MouseEvent event) { + if (!SwingUtilities.isRightMouseButton(event)) { + return; + } + int selectedRow = tree.getLeadSelectionRow(); + TreePath selectedPath = tree.getLeadSelectionPath(); + + TreeNode selectedNode = null; + if (selectedPath != null) { + selectedNode = (TreeNode) selectedPath.getLastPathComponent(); + } + var nodeType = selectedNode != null + ? selectedNode.getType() + : NodeType.ROOT_NODE; + + JPopupMenu popupMenu = null; + switch (nodeType) { + case ROOT_NODE -> popupMenu = new RootContextualPopup(() -> addAction(null)); + case SERVER_NODE -> { + var serverNode = (ServerNode) selectedNode; + tree.setSelectionRow(selectedRow); + popupMenu = new ServerContextualPopup( + treeModel, + serverNode, + () -> editAction(null), + () -> removeAction(null) + ); + } + case STORE_NODE -> { + var storeNode = (StoreNode) selectedNode; + tree.setSelectionRow(selectedRow); + popupMenu = new StoreContextualPopup(treeModel, storeNode); + } + + } + if (popupMenu != null) { + popupMenu.show(event.getComponent(), event.getX(), event.getY()); + } + } + }); + } + + private void runInBackground(String name, Runnable runnable) { + ProgressManager.getInstance().run( + new Task.Backgroundable(toolWindow.getProject(), name, false) { + @Override + public void run(@NotNull ProgressIndicator indicator) { + runnable.run(); + } + } + ); + + } + + private void selectTreePath(TreePath treePath) { + SwingUtilities.invokeLater(() -> { + tree.setSelectionPath(treePath); + tree.scrollPathToVisible(treePath); + tree.updateUI(); + }); + } + + private void addAction(AnActionButton anActionButton) { + ServerDialog.showAddServerDialog(toolWindow) + .ifPresent(server -> runInBackground("Adding OpenFGA Server", () -> { + servers.add(server); + var newNode = new ServerNode(server, toolWindowNotifier); + root.add(newNode); + var treePath = new TreePath(newNode.getPath()); + selectTreePath(treePath); + })); + } + + private void editAction(AnActionButton anActionButton) { + getSelectedServerNode() + .flatMap(node -> ServerDialog.showEditServerDialog(toolWindow, node.getServer())) + .ifPresent(server -> runInBackground("Updating OpenFGA Server", () -> { + servers.update(server); + SwingUtilities.invokeLater(tree::updateUI); + })); + } + + private void removeAction(AnActionButton anActionButton) { + var selectedNode = getSelectedServerNode(); + selectedNode.ifPresent(node -> { + var server = node.getServer(); + var title = "Confirm OpenFGA Server Deletion"; + var message = "Deleting the OpenFGA server '" + server.getName() + "' is not reversible. Do you confirm the server deletion?"; + if (Messages.showYesNoDialog(this, message, title, null) != Messages.YES) { + return; + } + runInBackground("Deleting OpenFGA Server", () -> { + servers.remove(server); + SwingUtilities.invokeLater(() -> { + root.reloadChildren(treeModel); + tree.updateUI(); + }); + }); + }); + } + + private void moveDownAction(AnActionButton anActionButton) { + getSelectedServerNode().ifPresent(node -> moveServerNode(node, servers::moveDown)); + } + + private void moveUpAction(AnActionButton anActionButton) { + getSelectedServerNode().ifPresent(node -> moveServerNode(node, servers::moveUp)); + } + + private void moveServerNode(ServerNode node, Function moveFunction) { + runInBackground("Moving OpenFGA Server", () -> { + var indexToSelect = moveFunction.apply(node.getServer()); + SwingUtilities.invokeLater(() -> { + root.reloadChildren(treeModel); + var serverNode = (ServerNode) root.getChildAt(indexToSelect); + var pathToSelect = new TreePath(serverNode.getPath()); + tree.setSelectionPath(pathToSelect); + tree.updateUI(); + }); + }); + + } + + private Optional getSelectedNode() { + return getSelectedNode(null); + } + + private Optional getSelectedNode(NodeType nodeType) { + return Arrays + .stream(tree.getSelectedNodes(TreeNode.class, node -> nodeType == null || node.is(nodeType))) + .findFirst(); + } + + private Optional getSelectedServerNode() { + return getSelectedNode(SERVER_NODE) + .map(ServerNode.class::cast); + } + + private boolean isSelectedNode(NodeType nodeType) { + return getSelectedNode(nodeType).isPresent(); + } +} diff --git a/src/main/java/dev/openfga/intellijplugin/servers/ui/tree/CellRenderer.java b/src/main/java/dev/openfga/intellijplugin/servers/ui/tree/CellRenderer.java new file mode 100644 index 0000000..392c1f1 --- /dev/null +++ b/src/main/java/dev/openfga/intellijplugin/servers/ui/tree/CellRenderer.java @@ -0,0 +1,18 @@ +package dev.openfga.intellijplugin.servers.ui.tree; + +import javax.swing.*; +import javax.swing.tree.DefaultTreeCellRenderer; +import java.awt.*; + +public class CellRenderer extends DefaultTreeCellRenderer { + + @Override + public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean expanded, boolean leaf, int row, boolean hasFocus) { + super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus); + var treeNode = (TreeNode) value; + setText(treeNode.getText()); + setIcon(treeNode.getIcon()); + setToolTipText(treeNode.getToolTipText()); + return this; + } +} diff --git a/src/main/java/dev/openfga/intellijplugin/servers/ui/tree/NodeType.java b/src/main/java/dev/openfga/intellijplugin/servers/ui/tree/NodeType.java new file mode 100644 index 0000000..cfa70d1 --- /dev/null +++ b/src/main/java/dev/openfga/intellijplugin/servers/ui/tree/NodeType.java @@ -0,0 +1,7 @@ +package dev.openfga.intellijplugin.servers.ui.tree; + +public enum NodeType { + ROOT_NODE, + SERVER_NODE, + STORE_NODE +} diff --git a/src/main/java/dev/openfga/intellijplugin/servers/ui/tree/OpenFgaTreeModel.java b/src/main/java/dev/openfga/intellijplugin/servers/ui/tree/OpenFgaTreeModel.java new file mode 100644 index 0000000..02e5bdd --- /dev/null +++ b/src/main/java/dev/openfga/intellijplugin/servers/ui/tree/OpenFgaTreeModel.java @@ -0,0 +1,49 @@ +package dev.openfga.intellijplugin.servers.ui.tree; + +import dev.openfga.intellijplugin.util.notifications.ToolWindowNotifier; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.ui.treeStructure.Tree; + +import javax.swing.*; +import javax.swing.event.TreeExpansionEvent; +import javax.swing.event.TreeWillExpandListener; +import javax.swing.tree.DefaultTreeModel; +import javax.swing.tree.TreePath; + + +public class OpenFgaTreeModel extends DefaultTreeModel implements TreeWillExpandListener { + + private static final Logger logger = Logger.getInstance(OpenFgaTreeModel.class); + + private final RootNode rootNode; + + public OpenFgaTreeModel(ToolWindowNotifier toolWindowNotifier) { + this(new RootNode(toolWindowNotifier)); + } + + private OpenFgaTreeModel(RootNode rootNode) { + super(rootNode); + this.rootNode = rootNode; + rootNode.reloadChildren(this); + } + + public RootNode getRootNode() { + return rootNode; + } + + @Override + public void treeWillExpand(TreeExpansionEvent event) { + var node = (TreeNode) event.getPath().getLastPathComponent(); + if (node.getType() == NodeType.SERVER_NODE) { + var serverNode = (ServerNode) node; + serverNode.loadChildren(this).thenAccept(unused -> { + var tree = (Tree) event.getSource(); + SwingUtilities.invokeLater(() -> tree.expandPath(new TreePath(serverNode.getPath()))); + }); + } + } + + @Override + public void treeWillCollapse(TreeExpansionEvent event) { + } +} diff --git a/src/main/java/dev/openfga/intellijplugin/servers/ui/tree/RootContextualPopup.java b/src/main/java/dev/openfga/intellijplugin/servers/ui/tree/RootContextualPopup.java new file mode 100644 index 0000000..48b43e2 --- /dev/null +++ b/src/main/java/dev/openfga/intellijplugin/servers/ui/tree/RootContextualPopup.java @@ -0,0 +1,22 @@ +package dev.openfga.intellijplugin.servers.ui.tree; + +import com.intellij.icons.AllIcons; + +import javax.swing.*; + +public class RootContextualPopup extends JPopupMenu { + + private final Runnable addServerAction; + + public RootContextualPopup(Runnable addServerAction) { + this.addServerAction = addServerAction; + + add(addServerMenuItem()); + } + + private JMenuItem addServerMenuItem() { + var menuItem = new JMenuItem("Add server", AllIcons.General.Add); + menuItem.addActionListener(e -> addServerAction.run()); + return menuItem; + } +} diff --git a/src/main/java/dev/openfga/intellijplugin/servers/ui/tree/RootNode.java b/src/main/java/dev/openfga/intellijplugin/servers/ui/tree/RootNode.java new file mode 100644 index 0000000..9ead4ca --- /dev/null +++ b/src/main/java/dev/openfga/intellijplugin/servers/ui/tree/RootNode.java @@ -0,0 +1,42 @@ +package dev.openfga.intellijplugin.servers.ui.tree; + +import dev.openfga.intellijplugin.servers.model.Server; +import dev.openfga.intellijplugin.servers.service.OpenFGAServers; +import dev.openfga.intellijplugin.util.notifications.ToolWindowNotifier; + +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.MutableTreeNode; +import java.util.List; + +public class RootNode extends DefaultMutableTreeNode implements TreeNode { + + private final ToolWindowNotifier toolWindowNotifier; + + public RootNode(ToolWindowNotifier toolWindowNotifier) { + super("OpenFGA Servers", true); + this.toolWindowNotifier = toolWindowNotifier; + } + + @Override + public NodeType getType() { + return NodeType.ROOT_NODE; + } + + @Override + public String getText() { + return String.valueOf(getUserObject()); + } + + private List getServers() { + return OpenFGAServers.getInstance().getServers(); + } + + public void reloadChildren(OpenFgaTreeModel model) { + removeAllChildren(); + for (Server server : getServers()) { + MutableTreeNode newChild = new ServerNode(server, toolWindowNotifier); + model.insertNodeInto(newChild, this, ((MutableTreeNode) this).getChildCount()); + + } + } +} diff --git a/src/main/java/dev/openfga/intellijplugin/servers/ui/tree/ServerContextualPopup.java b/src/main/java/dev/openfga/intellijplugin/servers/ui/tree/ServerContextualPopup.java new file mode 100644 index 0000000..745a9ea --- /dev/null +++ b/src/main/java/dev/openfga/intellijplugin/servers/ui/tree/ServerContextualPopup.java @@ -0,0 +1,66 @@ +package dev.openfga.intellijplugin.servers.ui.tree; + +import com.intellij.icons.AllIcons; + +import javax.swing.*; +import java.awt.event.ActionEvent; + +import static dev.openfga.intellijplugin.servers.util.UIUtil.copyToClipboard; + +public class ServerContextualPopup extends JPopupMenu { + + private final ServerNode serverNode; + private final OpenFgaTreeModel model; + private final Runnable editServerAction; + private final Runnable removeServerAction; + + public ServerContextualPopup(OpenFgaTreeModel model, ServerNode serverNode, Runnable editServerAction, Runnable removeServerAction) { + this.model = model; + this.serverNode = serverNode; + this.editServerAction = editServerAction; + this.removeServerAction = removeServerAction; + + add(editServerMenuItem()); + add(removeServerMenuItem()); + addSeparator(); + add(refreshMenuItem()); + addSeparator(); + add(copyNameMenuItem()); + add(copyUrlMenuItem()); + } + + private JMenuItem editServerMenuItem() { + var menuItem = new JMenuItem("edit", AllIcons.Actions.Edit); + menuItem.addActionListener(e -> editServerAction.run()); + return menuItem; + } + + private JMenuItem removeServerMenuItem() { + var menuItem = new JMenuItem("delete", AllIcons.General.Remove); + menuItem.addActionListener(e -> removeServerAction.run()); + return menuItem; + } + + private JMenuItem refreshMenuItem() { + var menuItem = new JMenuItem("refresh", AllIcons.Actions.Refresh); + menuItem.addActionListener(this::refresh); + return menuItem; + } + + private void refresh(ActionEvent event) { + serverNode.forceNextReload(); + serverNode.loadChildren(model); + } + + private JMenuItem copyNameMenuItem() { + var menuItem = new JMenuItem("copy name"); + menuItem.addActionListener(e -> copyToClipboard(serverNode.getServer().getName())); + return menuItem; + } + + private JMenuItem copyUrlMenuItem() { + var menuItem = new JMenuItem("copy url"); + menuItem.addActionListener(e -> copyToClipboard(serverNode.getServer().getUrl())); + return menuItem; + } +} diff --git a/src/main/java/dev/openfga/intellijplugin/servers/ui/tree/ServerNode.java b/src/main/java/dev/openfga/intellijplugin/servers/ui/tree/ServerNode.java new file mode 100644 index 0000000..de88ccf --- /dev/null +++ b/src/main/java/dev/openfga/intellijplugin/servers/ui/tree/ServerNode.java @@ -0,0 +1,115 @@ +package dev.openfga.intellijplugin.servers.ui.tree; + +import com.intellij.openapi.diagnostic.Logger; +import dev.openfga.intellijplugin.OpenFGAIcons; +import dev.openfga.intellijplugin.sdk.OpenFgaApiClient; +import dev.openfga.intellijplugin.servers.model.Server; +import dev.openfga.intellijplugin.util.notifications.ToolWindowNotifier; +import dev.openfga.sdk.api.model.Store; + +import javax.swing.*; +import javax.swing.tree.DefaultMutableTreeNode; +import java.net.ConnectException; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; +import java.util.stream.IntStream; + +public class ServerNode extends DefaultMutableTreeNode implements TreeNode { + + private static final Logger logger = Logger.getInstance(ServerNode.class); + + private final Server server; + private final ToolWindowNotifier notifier; + private boolean storesLoaded = false; + + public ServerNode(Server server, ToolWindowNotifier notifier) { + super(server, true); + this.server = server; + this.notifier = notifier; + } + + public Server getServer() { + return server; + } + + public ToolWindowNotifier getNotifier() { + return notifier; + } + + public boolean isStoresLoaded() { + return storesLoaded; + } + + @Override + public boolean isLeaf() { + return false; + } + + @Override + public String getText() { + return server.getName(); + } + + @Override + public String getToolTipText() { + return server.getUrl(); + } + + @Override + public Icon getIcon() { + return OpenFGAIcons.SERVER_NODE; + } + + @Override + public NodeType getType() { + return NodeType.SERVER_NODE; + } + + public synchronized void forceNextReload() { + storesLoaded = false; + } + + public CompletableFuture loadChildren(OpenFgaTreeModel model) { + return loadStores().thenAccept(updateChildren(model)); + } + + private CompletableFuture> loadStores() { + synchronized (this) { + if (storesLoaded) { + return CompletableFuture.completedFuture(null); + } + storesLoaded = true; + } + return OpenFgaApiClient.ForServer(server).listStores() + .exceptionally(exception -> { + var message = exception.getCause() instanceof ConnectException + ? "failed to connect to " + server.getUrl() + : exception.getMessage(); + logger.info(exception); + notifier.notifyError(message); + return null; + }); + } + + private Consumer> updateChildren(OpenFgaTreeModel model) { + return stores -> { + if (stores == null) { + return; + } + var children1 = Collections.list(this.children()); + if (!children1.isEmpty()) { + var indices = IntStream.range(0, children1.size()).toArray(); + this.removeAllChildren(); + SwingUtilities.invokeLater(() -> model.nodesWereRemoved(this, indices, children1.toArray())); + } + + for (var store : stores) { + this.add(new StoreNode(store)); + } + var indices = IntStream.range(0, stores.size()).toArray(); + SwingUtilities.invokeLater(() -> model.nodesWereInserted(this, indices)); + }; + } +} diff --git a/src/main/java/dev/openfga/intellijplugin/servers/ui/tree/StoreContextualPopup.java b/src/main/java/dev/openfga/intellijplugin/servers/ui/tree/StoreContextualPopup.java new file mode 100644 index 0000000..cdb78a2 --- /dev/null +++ b/src/main/java/dev/openfga/intellijplugin/servers/ui/tree/StoreContextualPopup.java @@ -0,0 +1,31 @@ +package dev.openfga.intellijplugin.servers.ui.tree; + +import javax.swing.*; + +import static dev.openfga.intellijplugin.servers.util.UIUtil.copyToClipboard; + +public class StoreContextualPopup extends JPopupMenu { + + private final StoreNode storeNode; + private final OpenFgaTreeModel model; + + public StoreContextualPopup(OpenFgaTreeModel model, StoreNode storeNode) { + this.model = model; + this.storeNode = storeNode; + + add(copyIdMenuItem()); + add(copyNameMenuItem()); + } + + private JMenuItem copyNameMenuItem() { + var menuItem = new JMenuItem("copy name"); + menuItem.addActionListener(e -> copyToClipboard(storeNode.getStore().getName())); + return menuItem; + } + + private JMenuItem copyIdMenuItem() { + var menuItem = new JMenuItem("copy id"); + menuItem.addActionListener(e -> copyToClipboard(storeNode.getStore().getId())); + return menuItem; + } +} diff --git a/src/main/java/dev/openfga/intellijplugin/servers/ui/tree/StoreNode.java b/src/main/java/dev/openfga/intellijplugin/servers/ui/tree/StoreNode.java new file mode 100644 index 0000000..b875ac9 --- /dev/null +++ b/src/main/java/dev/openfga/intellijplugin/servers/ui/tree/StoreNode.java @@ -0,0 +1,43 @@ +package dev.openfga.intellijplugin.servers.ui.tree; + +import dev.openfga.intellijplugin.OpenFGAIcons; +import dev.openfga.sdk.api.model.Store; + +import javax.swing.*; +import javax.swing.tree.DefaultMutableTreeNode; + +import static dev.openfga.intellijplugin.servers.ui.tree.NodeType.STORE_NODE; + +public class StoreNode extends DefaultMutableTreeNode implements TreeNode { + + private final Store store; + + public StoreNode(Store store) { + super(store, false); + this.store = store; + } + + public Store getStore() { + return store; + } + + @Override + public NodeType getType() { + return STORE_NODE; + } + + @Override + public String getText() { + return store.getName(); + } + + @Override + public String getToolTipText() { + return store.getId(); + } + + @Override + public Icon getIcon() { + return OpenFGAIcons.STORE_NODE; + } +} diff --git a/src/main/java/dev/openfga/intellijplugin/servers/ui/tree/TreeNode.java b/src/main/java/dev/openfga/intellijplugin/servers/ui/tree/TreeNode.java new file mode 100644 index 0000000..166bcf8 --- /dev/null +++ b/src/main/java/dev/openfga/intellijplugin/servers/ui/tree/TreeNode.java @@ -0,0 +1,22 @@ +package dev.openfga.intellijplugin.servers.ui.tree; + +import javax.swing.*; + +public interface TreeNode { + + NodeType getType(); + + String getText(); + + default String getToolTipText() { + return null; + } + + default Icon getIcon() { + return null; + } + + default boolean is(NodeType nodeType) { + return nodeType == getType(); + } +} diff --git a/src/main/java/dev/openfga/intellijplugin/servers/util/ServersUtil.java b/src/main/java/dev/openfga/intellijplugin/servers/util/ServersUtil.java index 37a7295..39de0e6 100644 --- a/src/main/java/dev/openfga/intellijplugin/servers/util/ServersUtil.java +++ b/src/main/java/dev/openfga/intellijplugin/servers/util/ServersUtil.java @@ -23,9 +23,9 @@ public static CompletableFuture testConnection(Server server) throws Se } } - private static OpenFgaClient createClient(Server server) throws FgaInvalidParameterException { + public static OpenFgaClient createClient(Server server) throws FgaInvalidParameterException { var clientConfiguration = new ClientConfiguration() - .apiUrl(server.loadUrl()) + .apiUrl(server.getUrl()) .credentials(getCredentials(server)); return new OpenFgaClient(clientConfiguration); @@ -34,9 +34,9 @@ private static OpenFgaClient createClient(Server server) throws FgaInvalidParame private static Credentials getCredentials(Server server) { return switch (server.getAuthenticationMethod()) { case NONE -> new Credentials(); - case API_TOKEN -> new Credentials(new ApiToken(server.loadApiToken())); + case API_TOKEN -> new Credentials(new ApiToken(server.getApiToken())); case OIDC -> { - var oidc = server.loadOidc(); + var oidc = server.getOidc(); yield new Credentials(new ClientCredentials() .apiTokenIssuer(oidc.tokenEndpoint()) .clientId(oidc.clientId()) diff --git a/src/main/java/dev/openfga/intellijplugin/servers/util/UIUtil.java b/src/main/java/dev/openfga/intellijplugin/servers/util/UIUtil.java new file mode 100644 index 0000000..d0b8abd --- /dev/null +++ b/src/main/java/dev/openfga/intellijplugin/servers/util/UIUtil.java @@ -0,0 +1,13 @@ +package dev.openfga.intellijplugin.servers.util; + +import java.awt.*; +import java.awt.datatransfer.StringSelection; + +public class UIUtil { + + public static void copyToClipboard(String text) { + var stringSelection = new StringSelection(text); + var clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + clipboard.setContents(stringSelection, null); + } +} diff --git a/src/main/java/dev/openfga/intellijplugin/util/IterableEnumeration.java b/src/main/java/dev/openfga/intellijplugin/util/IterableEnumeration.java new file mode 100644 index 0000000..49da92e --- /dev/null +++ b/src/main/java/dev/openfga/intellijplugin/util/IterableEnumeration.java @@ -0,0 +1,34 @@ +package dev.openfga.intellijplugin.util; + +import org.jetbrains.annotations.NotNull; + +import java.util.Enumeration; +import java.util.Iterator; + +public class IterableEnumeration implements Iterable { + private final Enumeration enumeration; + + public IterableEnumeration(Enumeration enumeration) { + this.enumeration = enumeration; + } + + public @NotNull Iterator iterator() { + return new Iterator<>() { + public boolean hasNext() { + return enumeration.hasMoreElements(); + } + + public T next() { + return enumeration.nextElement(); + } + + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + + public static Iterable iterable(Enumeration enumeration) { + return new IterableEnumeration<>(enumeration); + } +} \ No newline at end of file diff --git a/src/main/java/dev/openfga/intellijplugin/util/ListUtil.java b/src/main/java/dev/openfga/intellijplugin/util/ListUtil.java new file mode 100644 index 0000000..e244e71 --- /dev/null +++ b/src/main/java/dev/openfga/intellijplugin/util/ListUtil.java @@ -0,0 +1,27 @@ +package dev.openfga.intellijplugin.util; + +import java.util.List; +import java.util.Optional; +import java.util.OptionalInt; +import java.util.function.Predicate; +import java.util.stream.IntStream; + +public class ListUtil { + + public static OptionalInt indexOf(List list, Predicate predicate) { + return IntStream.range(0, list.size()) + .filter(i -> predicate.test(list.get(i))) + .findFirst(); + } + + public static Optional get(List list, Predicate predicate) { + var index = indexOf(list, predicate).orElse(-1); + return index > -1 + ? Optional.ofNullable(list.get(index)) + : Optional.empty(); + } + + public static void remove(List list, Predicate predicate) { + indexOf(list, predicate).ifPresent(list::remove); + } +} diff --git a/src/main/java/dev/openfga/intellijplugin/servers/util/UriUtil.java b/src/main/java/dev/openfga/intellijplugin/util/UriUtil.java similarity index 87% rename from src/main/java/dev/openfga/intellijplugin/servers/util/UriUtil.java rename to src/main/java/dev/openfga/intellijplugin/util/UriUtil.java index e2c66b5..8e2af61 100644 --- a/src/main/java/dev/openfga/intellijplugin/servers/util/UriUtil.java +++ b/src/main/java/dev/openfga/intellijplugin/util/UriUtil.java @@ -1,4 +1,4 @@ -package dev.openfga.intellijplugin.servers.util; +package dev.openfga.intellijplugin.util; import java.net.URI; diff --git a/src/main/java/dev/openfga/intellijplugin/util/notifications/GlobalNotifier.java b/src/main/java/dev/openfga/intellijplugin/util/notifications/GlobalNotifier.java new file mode 100644 index 0000000..dfc01d4 --- /dev/null +++ b/src/main/java/dev/openfga/intellijplugin/util/notifications/GlobalNotifier.java @@ -0,0 +1,20 @@ +package dev.openfga.intellijplugin.util.notifications; + +import com.intellij.notification.NotificationType; + +public enum GlobalNotifier implements Notifier { + + INSTANCE; + + private final Notifier inner = new ProjectNotifier(null); + + @Override + public void notify(NotificationType notificationType, String content) { + inner.notify(notificationType, content); + } + + @Override + public void notify(NotificationType notificationType, String title, String content) { + inner.notify(notificationType, title, content); + } +} diff --git a/src/main/java/dev/openfga/intellijplugin/util/notifications/Notifier.java b/src/main/java/dev/openfga/intellijplugin/util/notifications/Notifier.java new file mode 100644 index 0000000..a9f2072 --- /dev/null +++ b/src/main/java/dev/openfga/intellijplugin/util/notifications/Notifier.java @@ -0,0 +1,26 @@ +package dev.openfga.intellijplugin.util.notifications; + +import com.intellij.notification.NotificationType; + +public interface Notifier { + + void notify(NotificationType notificationType, String content); + + void notify(NotificationType notificationType, String title, String content); + + default void notifyError(String content) { + notify(NotificationType.ERROR, content); + } + + default void notifyError(String title, String content) { + notify(NotificationType.ERROR, title, content); + } + + default void notifyError(Throwable throwable) { + notifyError(throwable.getMessage()); + } + + default void notifyError(String title, Throwable throwable) { + notifyError(title, throwable.getMessage()); + } +} diff --git a/src/main/java/dev/openfga/intellijplugin/util/notifications/ProjectNotifier.java b/src/main/java/dev/openfga/intellijplugin/util/notifications/ProjectNotifier.java new file mode 100644 index 0000000..505a796 --- /dev/null +++ b/src/main/java/dev/openfga/intellijplugin/util/notifications/ProjectNotifier.java @@ -0,0 +1,26 @@ +package dev.openfga.intellijplugin.util.notifications; + +import com.intellij.notification.NotificationGroupManager; +import com.intellij.notification.NotificationType; +import com.intellij.openapi.project.Project; + +public class ProjectNotifier implements Notifier { + + public static final String OPENFGA_NOTIFICATIONS_GROUP = "OpenFGA Notifications"; + private final Project project; + + public ProjectNotifier(Project project) { + this.project = project; + } + + public void notify(NotificationType notificationType, String content) { + notify(notificationType, "", content); + } + + public void notify(NotificationType notificationType, String title, String content) { + NotificationGroupManager.getInstance() + .getNotificationGroup(OPENFGA_NOTIFICATIONS_GROUP) + .createNotification(title, content, notificationType) + .notify(project); + } +} diff --git a/src/main/java/dev/openfga/intellijplugin/util/notifications/ToolWindowNotifier.java b/src/main/java/dev/openfga/intellijplugin/util/notifications/ToolWindowNotifier.java new file mode 100644 index 0000000..a994e7b --- /dev/null +++ b/src/main/java/dev/openfga/intellijplugin/util/notifications/ToolWindowNotifier.java @@ -0,0 +1,30 @@ +package dev.openfga.intellijplugin.util.notifications; + +import com.intellij.openapi.ui.MessageType; +import com.intellij.openapi.wm.ToolWindow; +import com.intellij.openapi.wm.ToolWindowManager; + +import javax.swing.*; + +public class ToolWindowNotifier { + + private final ToolWindow toolWindow; + + public ToolWindowNotifier(ToolWindow toolWindow) { + this.toolWindow = toolWindow; + } + + public void notifyError(Throwable throwable) { + notifyError(throwable.getMessage()); + } + + public void notifyError(String message) { + notify(MessageType.ERROR, message); + } + + public void notify(MessageType messageType, String message) { + SwingUtilities.invokeLater(() -> ToolWindowManager + .getInstance(toolWindow.getProject()) + .notifyByBalloon(toolWindow.getId(), messageType, message)); + } +} diff --git a/src/main/resources/icons/openfga-store.svg b/src/main/resources/icons/openfga-store.svg new file mode 100644 index 0000000..c6963cc --- /dev/null +++ b/src/main/resources/icons/openfga-store.svg @@ -0,0 +1,4 @@ + + + +