diff --git a/intellij/src/main/java/software/aws/toolkits/jetbrains/ui/KeyValueEditor.form b/intellij/src/main/java/software/aws/toolkits/jetbrains/ui/KeyValueEditor.form new file mode 100644 index 00000000000..dfe1f97e0cc --- /dev/null +++ b/intellij/src/main/java/software/aws/toolkits/jetbrains/ui/KeyValueEditor.form @@ -0,0 +1,52 @@ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/intellij/src/main/java/software/aws/toolkits/jetbrains/ui/KeyValueEditor.java b/intellij/src/main/java/software/aws/toolkits/jetbrains/ui/KeyValueEditor.java new file mode 100644 index 00000000000..d5b1fd066e2 --- /dev/null +++ b/intellij/src/main/java/software/aws/toolkits/jetbrains/ui/KeyValueEditor.java @@ -0,0 +1,74 @@ +package software.aws.toolkits.jetbrains.ui; + +import com.intellij.openapi.ui.DialogWrapper; +import com.intellij.openapi.ui.ValidationInfo; +import com.intellij.openapi.util.Pair; +import java.awt.Component; +import java.util.function.BiFunction; +import javax.swing.Action; +import javax.swing.JComponent; +import javax.swing.JPanel; +import javax.swing.JTextField; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class KeyValueEditor extends DialogWrapper { + private final BiFunction, Pair, ValidationInfo> validator; + private JTextField keyField; + private JTextField valueField; + private JPanel component; + + public KeyValueEditor(Component parent, + KeyValue initialValue, + BiFunction, Pair, ValidationInfo> validator) { + super(parent, false); + this.validator = validator; + + if (initialValue == null) { + setTitle("Create New Key-Value"); + } else { + setTitle("Edit Key-Value"); + keyField.setText(initialValue.getKey()); + valueField.setText(initialValue.getValue()); + } + init(); + } + + @Nullable + @Override + public JComponent getPreferredFocusedComponent() { + return keyField; + } + + @Nullable + @Override + protected JComponent createCenterPanel() { + return component; + } + + @Nullable + @Override + protected ValidationInfo doValidate() { + if(validator != null) { + return validator.apply(Pair.create(getKey(), keyField), Pair.create(getValue(), valueField)); + } else { + return null; + } + } + + @NotNull + @Override + protected Action[] createActions() { + return new Action[] {getOKAction(), getCancelAction()}; + } + + public String getKey() { + return keyField.getText().trim(); + } + + public String getValue() { + return valueField.getText().trim(); + } +} + + diff --git a/intellij/src/main/java/software/aws/toolkits/jetbrains/ui/KeyValueTableEditor.form b/intellij/src/main/java/software/aws/toolkits/jetbrains/ui/KeyValueTableEditor.form new file mode 100644 index 00000000000..7c8643a4630 --- /dev/null +++ b/intellij/src/main/java/software/aws/toolkits/jetbrains/ui/KeyValueTableEditor.form @@ -0,0 +1,35 @@ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/intellij/src/main/java/software/aws/toolkits/jetbrains/ui/KeyValueTableEditor.java b/intellij/src/main/java/software/aws/toolkits/jetbrains/ui/KeyValueTableEditor.java new file mode 100644 index 00000000000..9b2eaf2cf63 --- /dev/null +++ b/intellij/src/main/java/software/aws/toolkits/jetbrains/ui/KeyValueTableEditor.java @@ -0,0 +1,127 @@ +package software.aws.toolkits.jetbrains.ui; + +import com.intellij.icons.AllIcons; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.ui.ValidationInfo; +import com.intellij.openapi.util.Pair; +import com.intellij.ui.AnActionButton; +import com.intellij.ui.ToolbarDecorator; +import com.intellij.ui.components.JBLabel; +import com.intellij.util.ui.UIUtil; +import java.awt.BorderLayout; +import java.util.ArrayList; +import java.util.List; +import java.util.function.BiFunction; +import java.util.function.Supplier; +import javax.swing.JComponent; +import javax.swing.JPanel; + +public class KeyValueTableEditor { + private final Supplier> refreshLambda; + + private KeyValueTable table; + private JBLabel remainingItems; + private JPanel keyValueTableHolder; + @SuppressWarnings("unused") // Needed in order to embed this into another form + private JPanel content; + + private List initialValues; + private BiFunction, Pair, ValidationInfo> entryValidator; + + public KeyValueTableEditor(Supplier> refreshLambda, Integer itemLimit, + BiFunction, Pair, ValidationInfo> entryValidator, + Runnable changeListener) { + this.refreshLambda = refreshLambda; + this.entryValidator = entryValidator; + + ToolbarDecorator toolbar = ToolbarDecorator.createDecorator(table) + .disableUpDownActions() + .setAddAction(e -> this.createOrEdit(null)) + .setAddActionUpdater(e -> !table.isBusy()) + .setRemoveActionUpdater(e -> !table.isBusy()) + .setEditAction(e -> this.createOrEdit(table.getSelectedObject())) + .setEditActionUpdater(e -> !table.isBusy()) + .addExtraAction(new AnActionButton("Refresh", AllIcons.Actions.Refresh) { + @Override + public void actionPerformed(AnActionEvent e) { + verifyAndRefresh(); + } + + @Override + public boolean isEnabled() { + return !table.isBusy(); + } + }); + + table.getModel().addTableModelListener(e -> { + if (itemLimit != null) { + int remaining = itemLimit - (table.getItems().size()); + remainingItems.setText("Remaining " + remaining + " of " + itemLimit); + remainingItems.setVisible(true); + } else { + remainingItems.setVisible(false); + } + }); + + table.getModel().addTableModelListener(e -> changeListener.run()); + + keyValueTableHolder.add(toolbar.createPanel(), BorderLayout.CENTER); + + remainingItems.setForeground(UIUtil.getLabelDisabledForeground()); + } + + private void verifyAndRefresh() { + if (table.getModel().equals(initialValues)) { + if(!MessageUtils.verifyLossOfChanges(table)) { + return; + } + } + + refresh(); + } + + public void refresh() { + table.setBusy(true); + ApplicationManager.getApplication().executeOnPooledThread(() -> { + if (refreshLambda != null) { + List updatedValues = refreshLambda.get(); + initialValues = updatedValues; + table.getModel().setItems(new ArrayList<>(updatedValues)); + } + table.setBusy(false); + }); + } + + private void createOrEdit(KeyValue selectedObject) { + KeyValueEditor entryEditor = new KeyValueEditor(table, selectedObject, entryValidator); + if (entryEditor.showAndGet()) { + if (selectedObject != null) { + selectedObject.setKey(entryEditor.getKey()); + selectedObject.setValue(entryEditor.getValue()); + } else { + table.getModel().addRow(new KeyValue(entryEditor.getKey(), entryEditor.getValue())); + } + } + } + + public boolean isModified() { + return !table.getItems().equals(initialValues); + } + + public void reset() { + table.getModel().setItems(new ArrayList<>(initialValues)); + } + + public List getItems() { + return table.getItems(); + } + + public void setBusy(boolean busy) { + table.setBusy(busy); + } + + public boolean isBusy() { + return table.isBusy(); + } +} diff --git a/intellij/src/main/java/software/aws/toolkits/jetbrains/ui/s3/BucketDetailsPanel.form b/intellij/src/main/java/software/aws/toolkits/jetbrains/ui/s3/BucketDetailsPanel.form new file mode 100644 index 00000000000..72740ee42d2 --- /dev/null +++ b/intellij/src/main/java/software/aws/toolkits/jetbrains/ui/s3/BucketDetailsPanel.form @@ -0,0 +1,157 @@ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/intellij/src/main/java/software/aws/toolkits/jetbrains/ui/s3/BucketDetailsPanel.java b/intellij/src/main/java/software/aws/toolkits/jetbrains/ui/s3/BucketDetailsPanel.java new file mode 100644 index 00000000000..d0e3bedbed5 --- /dev/null +++ b/intellij/src/main/java/software/aws/toolkits/jetbrains/ui/s3/BucketDetailsPanel.java @@ -0,0 +1,173 @@ +package software.aws.toolkits.jetbrains.ui.s3; + +import static com.intellij.ui.IdeBorderFactory.TITLED_BORDER_LEFT_INSET; +import static com.intellij.ui.IdeBorderFactory.TITLED_BORDER_RIGHT_INSET; +import static com.intellij.ui.IdeBorderFactory.TITLED_BORDER_TOP_INSET; + +import software.aws.toolkits.jetbrains.aws.s3.S3BucketVirtualFile; +import software.aws.toolkits.jetbrains.ui.KeyValue; +import software.aws.toolkits.jetbrains.ui.KeyValueTableEditor; +import software.aws.toolkits.jetbrains.ui.MessageUtils; +import com.amazonaws.intellij.utils.DateUtils; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.model.BucketTaggingConfiguration; +import com.amazonaws.services.s3.model.Region; +import com.amazonaws.services.s3.model.TagSet; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.ide.CopyPasteManager; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.ValidationInfo; +import com.intellij.openapi.util.Pair; +import com.intellij.ui.IdeBorderFactory; +import com.intellij.ui.components.JBLabel; +import com.intellij.util.ui.JBUI; +import com.intellij.util.ui.TextTransferable; +import java.awt.Insets; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import javax.swing.JButton; +import javax.swing.JComponent; +import javax.swing.JPanel; + +public class BucketDetailsPanel { + private static final int BUCKET_TAG_LIMIT = 50; + private static final int MAX_KEY_LENGTH = 128; + private static final int MAX_VALUE_LENGTH = 256; + private static final Pattern KEY_VALUE_VALID_REGEX = Pattern.compile("^([\\p{L}\\p{Z}\\p{N}_.:/=+\\-]*)$"); + private static final String KEY_VALUE_VALIDATION_ERROR = + "The string can contain only the set of Unicode letters, digits, whitespace, '_', '.', '/', '=', '+', '-'"; + + private final S3BucketVirtualFile bucketVirtualFile; + private JPanel contentPanel; + private JPanel tagPanel; + private JBLabel bucketNameLabel; + private JBLabel region; + private JBLabel versioning; + private JBLabel creationDate; + private JButton copyArnButton; + private KeyValueTableEditor tags; + private JButton applyButton; + private JButton cancelButton; + + public BucketDetailsPanel(Project project, S3BucketVirtualFile bucketVirtualFile) { + this.bucketVirtualFile = bucketVirtualFile; + + this.contentPanel.setBorder(IdeBorderFactory.createTitledBorder("Bucket Details", false)); + + this.bucketNameLabel.setText(bucketVirtualFile.getName()); + + this.region.setText(determineRegion()); + this.versioning.setText(bucketVirtualFile.getVersioningStatus()); + this.creationDate.setText(DateUtils.formatDate(bucketVirtualFile.getTimeStamp())); + + Insets insets = JBUI.insets(TITLED_BORDER_TOP_INSET, TITLED_BORDER_LEFT_INSET, 0, TITLED_BORDER_RIGHT_INSET); + this.tagPanel.setBorder(IdeBorderFactory.createTitledBorder("Tags", false, insets)); + + this.copyArnButton.addActionListener(e -> { + String arn = "arn:aws:s3:::" + bucketVirtualFile.getName(); + CopyPasteManager.getInstance().setContents(new TextTransferable(arn)); + }); + + this.applyButton.addActionListener(actionEvent -> applyChanges()); + this.cancelButton.addActionListener(actionEvent -> cancelChanges()); + + this.tags.refresh(); + } + + + private void createUIComponents() { + tags = new KeyValueTableEditor(this::loadTags, BUCKET_TAG_LIMIT, this::validateTag, this::updateButtons); + } + + private List loadTags() { + AmazonS3 s3Client = bucketVirtualFile.getFileSystem().getS3Client(); + BucketTaggingConfiguration bucketTags = s3Client.getBucketTaggingConfiguration( + bucketVirtualFile.getName()); + if (bucketTags == null) { + return Collections.emptyList(); + } + + return bucketTags.getTagSet() + .getAllTags() + .entrySet() + .stream() + .map(entry -> new KeyValue(entry.getKey(), entry.getValue())) + .collect(Collectors.toList()); + } + + private ValidationInfo validateTag(Pair keyPair, Pair valuePair) { + String key = keyPair.getFirst(); + JComponent keyInput = keyPair.getSecond(); + if (key.length() < 1 || key.length() >= MAX_KEY_LENGTH) { + return new ValidationInfo("Key must be between 1 and " + MAX_KEY_LENGTH + " characters", keyInput); + } + + if (!KEY_VALUE_VALID_REGEX.matcher(key).matches()) { + return new ValidationInfo(KEY_VALUE_VALIDATION_ERROR, keyInput); + } + + String value = valuePair.getFirst(); + JComponent valueInput = valuePair.getSecond(); + if (value.length() >= MAX_VALUE_LENGTH) { + return new ValidationInfo("Key must be between 1 and " + MAX_VALUE_LENGTH + " characters", valueInput); + } + + if (!KEY_VALUE_VALID_REGEX.matcher(value).matches()) { + return new ValidationInfo(KEY_VALUE_VALIDATION_ERROR, valueInput); + } + + return null; + } + + private void updateButtons() { + if (tags.isModified() && !tags.isBusy()) { + applyButton.setEnabled(true); + cancelButton.setEnabled(true); + } else { + applyButton.setEnabled(false); + cancelButton.setEnabled(false); + } + } + + private void applyChanges() { + tags.setBusy(true); + updateButtons(); + ApplicationManager.getApplication().executeOnPooledThread(this::updateTags); + } + + private void updateTags() { + List newTags = tags.getItems(); + Map tagMap = newTags.stream().collect(Collectors.toMap(KeyValue::getKey, KeyValue::getValue)); + BucketTaggingConfiguration config = new BucketTaggingConfiguration(Collections.singleton(new TagSet(tagMap))); + bucketVirtualFile.getFileSystem().getS3Client().setBucketTaggingConfiguration(bucketVirtualFile.getName(), config); + tags.setBusy(false); + } + + private void cancelChanges() { + if (!MessageUtils.verifyLossOfChanges(contentPanel)) { + return; + } + + tags.reset(); + updateButtons(); + } + + private String determineRegion() { + Region region = bucketVirtualFile.getRegion(); + if (region != null) { + if (region == Region.US_Standard) { + return "us-east-1"; + } + return region.getFirstRegionId(); + } else { + return "Unknown"; + } + } + + public JComponent getComponent() { + return contentPanel; + } +} diff --git a/intellij/src/main/java/software/aws/toolkits/jetbrains/ui/s3/ObjectDetailsPanel.form b/intellij/src/main/java/software/aws/toolkits/jetbrains/ui/s3/ObjectDetailsPanel.form new file mode 100644 index 00000000000..3e188b608c8 --- /dev/null +++ b/intellij/src/main/java/software/aws/toolkits/jetbrains/ui/s3/ObjectDetailsPanel.form @@ -0,0 +1,183 @@ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/intellij/src/main/java/software/aws/toolkits/jetbrains/ui/s3/ObjectDetailsPanel.java b/intellij/src/main/java/software/aws/toolkits/jetbrains/ui/s3/ObjectDetailsPanel.java new file mode 100644 index 00000000000..c7236b5217c --- /dev/null +++ b/intellij/src/main/java/software/aws/toolkits/jetbrains/ui/s3/ObjectDetailsPanel.java @@ -0,0 +1,187 @@ +package software.aws.toolkits.jetbrains.ui.s3; + + +import com.amazonaws.intellij.utils.DateUtils; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.model.CopyObjectRequest; +import com.amazonaws.services.s3.model.GetObjectTaggingRequest; +import com.amazonaws.services.s3.model.GetObjectTaggingResult; +import com.amazonaws.services.s3.model.ObjectMetadata; +import com.amazonaws.services.s3.model.ObjectTagging; +import com.amazonaws.services.s3.model.SetObjectTaggingRequest; +import com.amazonaws.services.s3.model.Tag; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.ide.CopyPasteManager; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.ui.IdeBorderFactory; +import com.intellij.ui.components.JBLabel; +import com.intellij.ui.components.JBTabbedPane; +import com.intellij.util.ui.JBUI; +import com.intellij.util.ui.TextTransferable; +import java.awt.Insets; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import javax.swing.JButton; +import javax.swing.JComponent; +import javax.swing.JPanel; +import javax.swing.JTabbedPane; +import javax.swing.SwingConstants; +import org.jetbrains.annotations.NotNull; +import software.aws.toolkits.jetbrains.aws.s3.S3VirtualFile; +import software.aws.toolkits.jetbrains.ui.KeyValue; +import software.aws.toolkits.jetbrains.ui.KeyValueTableEditor; +import software.aws.toolkits.jetbrains.ui.MessageUtils; + +public class ObjectDetailsPanel { + private final S3VirtualFile s3File; + private JPanel contentPanel; + private JBLabel objectNameLabel; + private JBLabel size; + private JBLabel modifiedDate; + private JButton copyArnButton; + private JButton applyButton; + private JButton cancelButton; + private JTabbedPane tabbedPanel; + private KeyValueTableEditor tags; + private KeyValueTableEditor metadata; + private JBLabel eTag; + + public ObjectDetailsPanel(S3VirtualFile s3File) { + this.s3File = s3File; + + this.contentPanel.setBorder(IdeBorderFactory.createTitledBorder("Object Details", false)); + + this.objectNameLabel.setText(s3File.getName()); + + this.size.setText(StringUtil.formatFileSize(s3File.getLength())); + this.modifiedDate.setText(DateUtils.formatDate(s3File.getTimeStamp())); + this.eTag.setText(s3File.getETag()); + + this.copyArnButton.addActionListener(e -> { + String arn = "arn:aws:s3:::" + s3File.getPath(); + CopyPasteManager.getInstance().setContents(new TextTransferable(arn)); + }); + + this.applyButton.addActionListener(actionEvent -> applyChanges()); + this.cancelButton.addActionListener(actionEvent -> cancelChanges()); + + this.tags.refresh(); + this.metadata.refresh(); + } + + private void createUIComponents() { + tags = new KeyValueTableEditor(this::loadTags, null, null, this::onValueChanged); + metadata = new KeyValueTableEditor(this::loadMetadata, null, null, this::onValueChanged); + + tabbedPanel = new JBTabbedPane(SwingConstants.TOP) { + @NotNull + @Override + protected Insets getInsetsForTabComponent() { + return JBUI.emptyInsets(); + } + }; + } + + private List loadTags() { + AmazonS3 s3Client = s3File.getFileSystem().getS3Client(); + GetObjectTaggingRequest taggingRequest = new GetObjectTaggingRequest(s3File.getBucketName(), s3File.getKey()); + GetObjectTaggingResult objectTags = s3Client.getObjectTagging(taggingRequest); + if (objectTags == null) { + return Collections.emptyList(); + } + + return objectTags.getTagSet() + .stream() + .map(entry -> new KeyValue(entry.getKey(), entry.getValue())) + .collect(Collectors.toList()); + } + + private void updateTags(List newTags) { + List tags = newTags.stream() + .map(keyValue -> new Tag(keyValue.getKey(), keyValue.getValue())) + .collect(Collectors.toList()); + + AmazonS3 s3Client = s3File.getFileSystem().getS3Client(); + s3Client.setObjectTagging(new SetObjectTaggingRequest(s3File.getBucketName(), s3File.getKey(), new ObjectTagging(tags))); + } + + private List loadMetadata() { + AmazonS3 s3Client = s3File.getFileSystem().getS3Client(); + ObjectMetadata objectMetadata = s3Client.getObjectMetadata(s3File.getBucketName(), s3File.getKey()); + return objectMetadata.getUserMetadata() + .entrySet() + .stream() + .map(entry -> new KeyValue(entry.getKey(), entry.getValue())) + .collect(Collectors.toList()); + } + + private void onValueChanged() { + if (tags.isModified() || metadata.isModified()) { + applyButton.setEnabled(true); + cancelButton.setEnabled(true); + } else { + applyButton.setEnabled(false); + cancelButton.setEnabled(false); + } + } + + private void applyChanges() { + metadata.setBusy(true); + tags.setBusy(true); + applyButton.setEnabled(false); + cancelButton.setEnabled(false); + + // To update metadata, we need to issue a copy + if (metadata.isModified()) { + CopyObjectRequest copyObjectRequest = new CopyObjectRequest(s3File.getBucketName(), s3File.getKey(), + s3File.getBucketName(), s3File.getKey()); + ObjectMetadata newObjectMetadata = new ObjectMetadata(); + metadata.getItems().forEach(keyValue -> newObjectMetadata.addUserMetadata(keyValue.getKey(), keyValue.getValue())); + copyObjectRequest.setNewObjectMetadata(newObjectMetadata); + + if (tags.isModified()) { + copyObjectRequest.setNewObjectTagging(getObjectTagging()); + } + + ApplicationManager.getApplication().executeOnPooledThread(() -> { + s3File.getFileSystem().getS3Client().copyObject(copyObjectRequest); + metadata.refresh(); + tags.refresh(); + }); + } else { + ApplicationManager.getApplication().executeOnPooledThread(() -> { + SetObjectTaggingRequest setObjectTaggingRequest = new SetObjectTaggingRequest(s3File.getBucketName(), + s3File.getKey(), + getObjectTagging()); + s3File.getFileSystem().getS3Client().setObjectTagging(setObjectTaggingRequest); + tags.refresh(); + }); + } + } + + private ObjectTagging getObjectTagging() { + return new ObjectTagging(tags.getItems() + .stream() + .map(keyValue -> new Tag(keyValue.getKey(), keyValue.getValue())) + .collect(Collectors.toList())); + } + + private void cancelChanges() { + if (!MessageUtils.verifyLossOfChanges(contentPanel)) { + return; + } + + if (tags.isModified()) { + tags.reset(); + } + + if (metadata.isModified()) { + metadata.reset(); + } + } + + public JComponent getComponent() { + return contentPanel; + } +} diff --git a/intellij/src/main/kotlin/software/aws/toolkits/jetbrains/aws/s3/S3BucketViewerPanel.kt b/intellij/src/main/kotlin/software/aws/toolkits/jetbrains/aws/s3/S3BucketViewerPanel.kt new file mode 100644 index 00000000000..081bb71ecfc --- /dev/null +++ b/intellij/src/main/kotlin/software/aws/toolkits/jetbrains/aws/s3/S3BucketViewerPanel.kt @@ -0,0 +1,92 @@ +package software.aws.toolkits.jetbrains.aws.s3 + +import com.intellij.ui.JBSplitter + +import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory +import com.intellij.openapi.fileChooser.FileSystemTree +import com.intellij.openapi.fileChooser.FileSystemTreeFactory +import com.intellij.openapi.fileEditor.FileEditorManager +import com.intellij.openapi.project.Project +import com.intellij.ui.ToolbarDecorator +import com.intellij.ui.components.JBLabel +import com.intellij.ui.components.panels.Wrapper +import software.aws.toolkits.jetbrains.ui.s3.BucketDetailsPanel +import software.aws.toolkits.jetbrains.ui.s3.ObjectDetailsPanel +import java.awt.BorderLayout +import java.awt.event.MouseAdapter +import java.awt.event.MouseEvent +import javax.swing.JComponent +import javax.swing.JPanel + +class S3BucketViewerPanel(private val project: Project, private val s3bucket: S3BucketVirtualFile) { + private val splitPanel: JBSplitter + private val s3FileTree: S3FileTree + private val detailPane: Wrapper + + init { + s3FileTree = S3FileTree() + + detailPane = Wrapper(JBLabel()) + + splitPanel = JBSplitter(0.25f) + splitPanel.firstComponent = s3FileTree + splitPanel.secondComponent = detailPane + } + + val component: JComponent + get() = splitPanel + + private var details: JComponent? = null + set(component) { + detailPane.setContent(component ?: JBLabel()) + } + + private inner class S3FileTree() : JPanel(BorderLayout()) { + private val fileSystemTree: FileSystemTree + + init { + val fileDescriptor = FileChooserDescriptorFactory.createSingleFileOrFolderDescriptor() + .withRoots(s3bucket) + .withTreeRootVisible(true) + + fileSystemTree = FileSystemTreeFactory.SERVICE.getInstance() + .createFileSystemTree(project, fileDescriptor) + + val tree = fileSystemTree.tree + tree.addMouseListener(object : MouseAdapter() { + override fun mouseClicked(e: MouseEvent) { + if(e.clickCount >= 2) { + handleDoubleClick() + } + } + }) + tree.addTreeSelectionListener { handleSelectionChange(fileSystemTree) } + + val toolbar = ToolbarDecorator.createDecorator(tree) + + add(toolbar.createPanel(), BorderLayout.CENTER) + } + + private fun handleDoubleClick() { + val selectedFile = fileSystemTree.selectedFile + when(selectedFile) { + is S3VirtualFile -> FileEditorManager.getInstance(project).openFile(selectedFile, true) + } + } + + private fun handleSelectionChange(tree: FileSystemTree) { + val selectedFiles = tree.selectedFiles + if (selectedFiles.size != 1) { + details = null + return + } + + val selectedFile = selectedFiles[0] + details = when (selectedFile) { + is S3BucketVirtualFile -> BucketDetailsPanel(project, selectedFile).component + is S3VirtualFile -> ObjectDetailsPanel(selectedFile).component + else -> null + } + } + } +} diff --git a/intellij/src/main/kotlin/software/aws/toolkits/jetbrains/aws/s3/S3FileTypes.kt b/intellij/src/main/kotlin/software/aws/toolkits/jetbrains/aws/s3/S3FileTypes.kt new file mode 100644 index 00000000000..f8a2599c8db --- /dev/null +++ b/intellij/src/main/kotlin/software/aws/toolkits/jetbrains/aws/s3/S3FileTypes.kt @@ -0,0 +1,52 @@ +package software.aws.toolkits.jetbrains.aws.s3 + +import com.intellij.icons.AllIcons +import com.intellij.openapi.fileTypes.FileTypeConsumer +import com.intellij.openapi.fileTypes.FileTypeFactory +import com.intellij.openapi.fileTypes.ex.FileTypeIdentifiableByVirtualFile +import com.intellij.openapi.vfs.VirtualFile +import software.aws.toolkits.jetbrains.ui.S3_BUCKET_ICON +import javax.swing.Icon + +class BucketFileType : FileTypeIdentifiableByVirtualFile { + override fun getDefaultExtension() = "" + + override fun getIcon(): Icon = S3_BUCKET_ICON + + override fun getCharset(file: VirtualFile, content: ByteArray) = null + + override fun getName() = "S3 Bucket" + + override fun getDescription() = name + + override fun isBinary() = true + + override fun isMyFileType(file: VirtualFile) = file is S3BucketVirtualFile + + override fun isReadOnly() = true +} + +class DirectoryFileType : FileTypeIdentifiableByVirtualFile { + override fun getDefaultExtension() = "" + + override fun getIcon(): Icon = AllIcons.Nodes.Folder + + override fun getCharset(file: VirtualFile, content: ByteArray) = null + + override fun getName() = "S3 Directory" + + override fun getDescription() = name + + override fun isBinary() = true + + override fun isMyFileType(file: VirtualFile) = file is S3VirtualDirectory + + override fun isReadOnly() = true +} + +class S3FileTypeFactory : FileTypeFactory() { + override fun createFileTypes(consumer: FileTypeConsumer) { + consumer.consume(BucketFileType()) + consumer.consume(DirectoryFileType()) + } +} \ No newline at end of file diff --git a/intellij/src/main/kotlin/software/aws/toolkits/jetbrains/aws/s3/S3VirtualFileSystem.kt b/intellij/src/main/kotlin/software/aws/toolkits/jetbrains/aws/s3/S3VirtualFileSystem.kt new file mode 100644 index 00000000000..ac00991f18f --- /dev/null +++ b/intellij/src/main/kotlin/software/aws/toolkits/jetbrains/aws/s3/S3VirtualFileSystem.kt @@ -0,0 +1,302 @@ +package software.aws.toolkits.jetbrains.aws.s3 + +import com.amazonaws.services.s3.AmazonS3 +import com.amazonaws.services.s3.model.* +import com.intellij.openapi.util.io.FileTooBigException +import com.intellij.openapi.util.io.FileUtilRt +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.openapi.vfs.VirtualFileListener +import com.intellij.openapi.vfs.VirtualFileSystem +import com.intellij.util.PathUtil +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream + +class S3VirtualFileSystem(val s3Client: AmazonS3) : VirtualFileSystem() { + override fun deleteFile(requestor: Any?, vFile: VirtualFile) { + TODO("not implemented") + } + + override fun getProtocol(): String { + return "s3" + } + + fun getBuckets(): List { + return s3Client.listBuckets().map { S3BucketVirtualFile(this, it) } + } + + override fun createChildDirectory(requestor: Any?, vDir: VirtualFile, dirName: String): VirtualFile { + TODO("not implemented") + } + + override fun addVirtualFileListener(listener: VirtualFileListener) {} + + override fun isReadOnly(): Boolean = true + + override fun findFileByPath(path: String): VirtualFile? { + TODO("not implemented") + } + + override fun renameFile(requestor: Any?, vFile: VirtualFile, newName: String) { + TODO("not implemented") + } + + override fun createChildFile(requestor: Any?, vDir: VirtualFile, fileName: String): VirtualFile { + TODO("not implemented") + } + + override fun refreshAndFindFileByPath(path: String): VirtualFile? { + TODO("not implemented") + } + + override fun removeVirtualFileListener(listener: VirtualFileListener) {} + + override fun copyFile(requestor: Any?, virtualFile: VirtualFile, newParent: VirtualFile, copyName: String): VirtualFile { + TODO("not implemented") + } + + override fun moveFile(requestor: Any?, vFile: VirtualFile, newParent: VirtualFile) { + TODO("not implemented") + } + + override fun refresh(asynchronous: Boolean) { + TODO("not implemented") + } +} + +class S3BucketVirtualFile(private val fileSystem: S3VirtualFileSystem, private val bucket: Bucket) : VirtualFile() { + override fun getName(): String = bucket.name + + override fun getFileSystem() = fileSystem + + override fun getPath() = name + + override fun isWritable() = false + + override fun isDirectory() = true + + override fun isValid() = true + + override fun getParent() = null + + val versioningEnabled by lazy { versioningStatus != BucketVersioningConfiguration.OFF } + + val versioningStatus by lazy { fileSystem.s3Client.getBucketVersioningConfiguration(bucket.name).status } + + val region: Region? by lazy { + try { + Region.fromValue(fileSystem.s3Client.getBucketLocation(bucket.name)) + } catch (e: IllegalArgumentException) { + null + } + } + + override fun getChildren(): Array { + val s3Client = fileSystem.s3Client + + val request = ListObjectsV2Request() + .withBucketName(bucket.name) + .withDelimiter("/") + val children = arrayListOf() + + do { + val result = s3Client.listObjectsV2(request) + + children.addAll(result.commonPrefixes.map { S3VirtualDirectory(fileSystem, result.bucketName, it, this) }) + children.addAll(result.objectSummaries.map { + if (it.key.endsWith("/")) { + S3VirtualDirectory(fileSystem, bucket.name, it.key, this) + } else { + S3VirtualFile(fileSystem, it, this) + } + }) + + request.continuationToken = result.nextContinuationToken + } while (result.isTruncated) + + return children.toTypedArray() + } + + + @Throws(IOException::class) + override fun getOutputStream(requestor: Any, newModificationStamp: Long, newTimeStamp: Long): OutputStream { + throw IOException("getOutputStream() cannot be called against a bucket") + } + + @Throws(IOException::class) + override fun contentsToByteArray(): ByteArray { + throw IOException("contentsToByteArray() cannot be called against a bucket") + } + + @Throws(IOException::class) + override fun getInputStream(): InputStream { + throw IOException("getInputStream() cannot be called against a bucket") + } + + override fun getTimeStamp() = bucket.creationDate.time + + override fun getLength() = -1L + + override fun getModificationStamp() = -1L + + override fun refresh(asynchronous: Boolean, recursive: Boolean, postRunnable: Runnable?) {} + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + if (other?.javaClass != javaClass) { + return false + } + + other as S3BucketVirtualFile + + if (bucket.name != other.bucket.name) { + return false + } + + return true + } + + override fun hashCode(): Int { + return bucket.name.hashCode() + } +} + +abstract class BaseS3VirtualObject(protected val s3FileSystem: S3VirtualFileSystem, + val bucketName: String, + val key: String, + private val _parent: VirtualFile) : VirtualFile() { + override fun getName() = PathUtil.getFileName(key) + + override fun getFileSystem() = s3FileSystem + + override fun getPath() = bucketName + "/" + key + + override fun getParent(): VirtualFile = _parent + + override fun refresh(asynchronous: Boolean, recursive: Boolean, postRunnable: Runnable?) {} + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + if (other?.javaClass != javaClass) { + return false + } + + other as BaseS3VirtualObject + + if (bucketName != other.bucketName || key != other.key) { + return false + } + + return true + } + + override fun hashCode(): Int { + var result = bucketName.hashCode() + result = 31 * result + key.hashCode() + return result + } +} + +class S3VirtualDirectory(s3FileSystem: S3VirtualFileSystem, bucketName: String, private val directory: String, private val _parent: VirtualFile) + : BaseS3VirtualObject(s3FileSystem, bucketName, directory, _parent) { + override fun getChildren(): Array { + val s3Client = fileSystem.s3Client + val request = ListObjectsV2Request() + .withBucketName(bucketName) + .withPrefix(key) + .withDelimiter("/") + val children = arrayListOf() + + do { + val result = s3Client.listObjectsV2(request) + + children.addAll(result.commonPrefixes.map { S3VirtualDirectory(fileSystem, result.bucketName, it, this) }) + children.addAll(result.objectSummaries + .filterNot { it.key == key } + .map { + if (it.key.endsWith("/")) { + S3VirtualDirectory(fileSystem, bucketName, it.key, this) + } else { + S3VirtualFile(fileSystem, it, this) + } + }) + + request.continuationToken = result.nextContinuationToken + } while (result.isTruncated) + + return children.toTypedArray() + } + + @Throws(IOException::class) + override fun getOutputStream(requestor: Any, newModificationStamp: Long, newTimeStamp: Long): OutputStream { + throw IOException("getOutputStream() cannot be called against a virtual directory") + } + + @Throws(IOException::class) + override fun contentsToByteArray(): ByteArray { + throw IOException("contentsToByteArray() cannot be called against a virtual directory") + } + + @Throws(IOException::class) + override fun getInputStream(): InputStream { + throw IOException("getInputStream() cannot be called against a virtual directory") + } + + override fun isDirectory() = true + + override fun isWritable() = false + + override fun isValid() = true + + override fun getTimeStamp() = -1L + + override fun getLength() = -1L + + override fun getModificationStamp() = -1L +} + +class S3VirtualFile(s3FileSystem: S3VirtualFileSystem, private val obj: S3ObjectSummary, private val _parent: VirtualFile) + : BaseS3VirtualObject(s3FileSystem, obj.bucketName, obj.key, _parent) { + val eTag: String + get() = obj.eTag + + override fun getChildren(): Array = arrayOf() + + @Throws(IOException::class) + override fun getOutputStream(requestor: Any, newModificationStamp: Long, newTimeStamp: Long): OutputStream { + if (isDirectory) { + throw IOException("getOutputStream() cannot be called against a directory") + } + + TODO("not implemented") + } + + @Throws(IOException::class) + override fun contentsToByteArray(): ByteArray { + if (FileUtilRt.isTooLarge(obj.size)) { + throw FileTooBigException(path) + } + return inputStream.readBytes(obj.size.toInt()) + } + + @Throws(IOException::class) + override fun getInputStream(): InputStream { + return fileSystem.s3Client.getObject(obj.bucketName, obj.key).objectContent + } + + override fun isDirectory() = key.endsWith("/") + + override fun isWritable() = false + + override fun isValid() = true + + override fun getTimeStamp() = obj.lastModified.time + + override fun getLength() = obj.size + + override fun getModificationStamp() = -1L +} \ No newline at end of file diff --git a/intellij/src/main/kotlin/software/aws/toolkits/jetbrains/s3/explorer/AwsExplorerS3Node.kt b/intellij/src/main/kotlin/software/aws/toolkits/jetbrains/s3/explorer/AwsExplorerS3Node.kt index 9c20139f01d..b8faa8126f6 100644 --- a/intellij/src/main/kotlin/software/aws/toolkits/jetbrains/s3/explorer/AwsExplorerS3Node.kt +++ b/intellij/src/main/kotlin/software/aws/toolkits/jetbrains/s3/explorer/AwsExplorerS3Node.kt @@ -1,14 +1,20 @@ package software.aws.toolkits.jetbrains.s3.explorer + import com.amazonaws.services.s3.AmazonS3 import com.amazonaws.services.s3.model.Bucket import com.intellij.ide.util.treeView.AbstractTreeNode +import com.intellij.openapi.fileEditor.FileEditorManager import com.intellij.openapi.project.Project +import software.aws.toolkits.jetbrains.aws.s3.S3BucketVirtualFile +import software.aws.toolkits.jetbrains.aws.s3.S3VirtualFileSystem import software.aws.toolkits.jetbrains.core.AwsClientManager import software.aws.toolkits.jetbrains.ui.S3_BUCKET_ICON import software.aws.toolkits.jetbrains.ui.S3_SERVICE_ICON import software.aws.toolkits.jetbrains.ui.explorer.AwsExplorerNode import software.aws.toolkits.jetbrains.ui.explorer.AwsExplorerServiceRootNode +import javax.swing.tree.DefaultMutableTreeNode +import javax.swing.tree.DefaultTreeModel class AwsExplorerS3RootNode(project: Project) : AwsExplorerServiceRootNode(project, "Amazon S3", S3_SERVICE_ICON) { @@ -25,11 +31,15 @@ class AwsExplorerS3RootNode(project: Project) : class AwsExplorerBucketNode(project: Project, private val bucket: Bucket) : AwsExplorerNode(project, bucket, S3_BUCKET_ICON) { - override fun getChildren(): Collection> { - return emptyList() - } + private val editorManager = FileEditorManager.getInstance(project) + private val client: AmazonS3 = AwsClientManager.getInstance(project).getClient() + + override fun getChildren(): Collection> = emptyList() + + override fun toString(): String = bucket.name - override fun toString(): String { - return bucket.name + override fun onDoubleClick(model: DefaultTreeModel, selectedElement: DefaultMutableTreeNode) { + val bucketVirtualFile = S3BucketVirtualFile(S3VirtualFileSystem(client), bucket) + editorManager.openFile(bucketVirtualFile, true) } } \ No newline at end of file diff --git a/intellij/src/main/kotlin/software/aws/toolkits/jetbrains/ui/Icons.kt b/intellij/src/main/kotlin/software/aws/toolkits/jetbrains/ui/Icons.kt index 87d05d09802..4627284bcc2 100644 --- a/intellij/src/main/kotlin/software/aws/toolkits/jetbrains/ui/Icons.kt +++ b/intellij/src/main/kotlin/software/aws/toolkits/jetbrains/ui/Icons.kt @@ -2,26 +2,27 @@ package software.aws.toolkits.jetbrains.ui import com.intellij.openapi.util.IconLoader -val AWS_ICON = IconLoader.getIcon("/icons/aws-box.gif") -val S3_BUCKET_ICON = IconLoader.getIcon("/icons/bucket.png") -val S3_SERVICE_ICON = IconLoader.getIcon("/icons/s3-service.png") -val LAMBDA_SERVICE_ICON = IconLoader.getIcon("/icons/lambda-service.png") -val LAMBDA_SERVICE_ICON_LARGE = IconLoader.getIcon("/icons/lambda-service-large.png") -val LAMBDA_FUNCTION_ICON = IconLoader.getIcon("/icons/function.png") -val INFO_ICON = IconLoader.getIcon("/icons/information.png") -val SQS_SERVICE_ICON = IconLoader.getIcon("/icons/sqs-service.png") -val SQS_QUEUE_ICON = IconLoader.getIcon("/icons/index.png") -val SNS_SERVICE_ICON = IconLoader.getIcon("/icons/sns-service.png") -val SNS_TOPIC_ICON = IconLoader.getIcon("/icons/sns-topic.png") -val EC2_SERVICE_ICON = IconLoader.getIcon("/icons/rds-service.png") -val EC2_INSTANCE_ICON = IconLoader.getIcon("/icons/index.png") -val ADD_ICON = IconLoader.getIcon("/icons/add.png") +@JvmField val AWS_ICON = IconLoader.getIcon("/icons/aws-box.gif") +@JvmField val S3_BUCKET_ICON = IconLoader.getIcon("/icons/bucket.png") +@JvmField val S3_SERVICE_ICON = IconLoader.getIcon("/icons/s3-service.png") +@JvmField val LAMBDA_SERVICE_ICON = IconLoader.getIcon("/icons/lambda-service.png") +@JvmField val LAMBDA_SERVICE_ICON_LARGE = IconLoader.getIcon("/icons/lambda-service-large.png") +@JvmField val LAMBDA_FUNCTION_ICON = IconLoader.getIcon("/icons/function.png") +@JvmField val INFO_ICON = IconLoader.getIcon("/icons/information.png") +@JvmField val SQS_SERVICE_ICON = IconLoader.getIcon("/icons/sqs-service.png") +@JvmField val SQS_QUEUE_ICON = IconLoader.getIcon("/icons/index.png") +@JvmField val SNS_SERVICE_ICON = IconLoader.getIcon("/icons/sns-service.png") +@JvmField val SNS_TOPIC_ICON = IconLoader.getIcon("/icons/sns-topic.png") +@JvmField val EC2_SERVICE_ICON = IconLoader.getIcon("/icons/rds-service.png") +@JvmField val EC2_INSTANCE_ICON = IconLoader.getIcon("/icons/index.png") -val EU_ICON = IconLoader.getIcon("/icons/flags/eu.png") -val USA_ICON = IconLoader.getIcon("/icons/flags/us.png") -val SINGAPORE_ICON = IconLoader.getIcon("/icons/flags/singapore.png") -val JAPAN_ICON = IconLoader.getIcon("/icons/flags/japan.png") -val AUSTRALIA_ICON = IconLoader.getIcon("/icons/flags/australia.png") -val BRAZIL_ICON = IconLoader.getIcon("/icons/flags/brazil.png") -val IRELAND_ICON = IconLoader.getIcon("/icons/flags/ireland.png") \ No newline at end of file +@JvmField val ADD_ICON = IconLoader.getIcon("/icons/add.png") + +@JvmField val EU_ICON = IconLoader.getIcon("/icons/flags/eu.png") +@JvmField val USA_ICON = IconLoader.getIcon("/icons/flags/us.png") +@JvmField val SINGAPORE_ICON = IconLoader.getIcon("/icons/flags/singapore.png") +@JvmField val JAPAN_ICON = IconLoader.getIcon("/icons/flags/japan.png") +@JvmField val AUSTRALIA_ICON = IconLoader.getIcon("/icons/flags/australia.png") +@JvmField val BRAZIL_ICON = IconLoader.getIcon("/icons/flags/brazil.png") +@JvmField val IRELAND_ICON = IconLoader.getIcon("/icons/flags/ireland.png") \ No newline at end of file diff --git a/intellij/src/main/kotlin/software/aws/toolkits/jetbrains/ui/KeyValueTable.kt b/intellij/src/main/kotlin/software/aws/toolkits/jetbrains/ui/KeyValueTable.kt new file mode 100644 index 00000000000..39a2c0443c8 --- /dev/null +++ b/intellij/src/main/kotlin/software/aws/toolkits/jetbrains/ui/KeyValueTable.kt @@ -0,0 +1,64 @@ +package software.aws.toolkits.jetbrains.ui + +import com.intellij.ui.table.TableView +import com.intellij.util.ui.ColumnInfo +import com.intellij.util.ui.ListTableModel +import javax.swing.JTable +import javax.swing.RowSorter +import javax.swing.SortOrder +import javax.swing.table.TableRowSorter + +class KeyValueTable(initialItems: List = mutableListOf()) : TableView(createModel()) { + init { + autoResizeMode = (JTable.AUTO_RESIZE_LAST_COLUMN) + isStriped = true + emptyText.text = "No entries" + tableHeader.reorderingAllowed = false + + val sorter = TableRowSorter>(model) + sorter.setSortable(0, true) + sorter.setSortable(1, true) + sorter.sortsOnUpdates = true + sorter.sortKeys = listOf(RowSorter.SortKey(0, SortOrder.ASCENDING)) + + rowSorter = sorter + + model.items = initialItems + } + + var isBusy: Boolean = false + set(busy) { + setPaintBusy(busy) + field = busy + } + + @Suppress("UNCHECKED_CAST") + override fun getModel(): ListTableModel { + return super.getModel() as ListTableModel + } + + companion object { + private fun createModel(): ListTableModel { + val tableModel = ListTableModel(*createColumns()) + tableModel.isSortable = true + + return tableModel + } + + private fun createColumns(): Array { + return arrayOf( + StringColumn("Key", { it.key }), + StringColumn("Value", { it.value }) + ) + } + } +} + +private class StringColumn(name: String, private val extractor: (KeyValue) -> String) + : ColumnInfo(name) { + override fun valueOf(keyValue: KeyValue): String { + return extractor.invoke(keyValue) + } +} + +data class KeyValue(var key: String, var value: String) diff --git a/intellij/src/main/kotlin/software/aws/toolkits/jetbrains/ui/MessageUtils.kt b/intellij/src/main/kotlin/software/aws/toolkits/jetbrains/ui/MessageUtils.kt new file mode 100644 index 00000000000..675fe45a301 --- /dev/null +++ b/intellij/src/main/kotlin/software/aws/toolkits/jetbrains/ui/MessageUtils.kt @@ -0,0 +1,20 @@ +package software.aws.toolkits.jetbrains.ui + +import com.intellij.openapi.ui.Messages +import javax.swing.JComponent + +class MessageUtils { + companion object { + @JvmStatic + fun verifyLossOfChanges(parent: JComponent): Boolean { + val result = Messages.showOkCancelDialog(parent, + "You have uncommitted changes, this will erase those changes. Continue?", + "Confirm?", + Messages.YES_BUTTON, + Messages.CANCEL_BUTTON, + Messages.getWarningIcon()) + + return result == Messages.OK + } + } +} diff --git a/intellij/src/main/kotlin/software/aws/toolkits/jetbrains/ui/s3/S3BucketViewer.kt b/intellij/src/main/kotlin/software/aws/toolkits/jetbrains/ui/s3/S3BucketViewer.kt new file mode 100644 index 00000000000..fd98e269e07 --- /dev/null +++ b/intellij/src/main/kotlin/software/aws/toolkits/jetbrains/ui/s3/S3BucketViewer.kt @@ -0,0 +1,66 @@ +package software.aws.toolkits.jetbrains.ui.s3 + + +import com.intellij.codeHighlighting.BackgroundEditorHighlighter +import com.intellij.openapi.fileEditor.FileEditor +import com.intellij.openapi.fileEditor.FileEditorLocation +import com.intellij.openapi.fileEditor.FileEditorPolicy +import com.intellij.openapi.fileEditor.FileEditorProvider +import com.intellij.openapi.fileEditor.FileEditorState +import com.intellij.openapi.fileEditor.FileEditorStateLevel +import com.intellij.openapi.project.DumbAware +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.UserDataHolderBase +import com.intellij.openapi.vfs.VirtualFile +import software.aws.toolkits.jetbrains.aws.s3.S3BucketViewerPanel +import software.aws.toolkits.jetbrains.aws.s3.S3BucketVirtualFile +import java.beans.PropertyChangeListener +import javax.swing.JComponent + +class S3BucketViewer(private val project: Project, private val s3Bucket: S3BucketVirtualFile) + : UserDataHolderBase(), FileEditor { + + private val bucketViewer: S3BucketViewerPanel = S3BucketViewerPanel(project, s3Bucket) + + override fun getName() = "S3 Bucket Viewer" + + override fun getComponent(): JComponent = bucketViewer.component + + override fun dispose() {} + + override fun isModified(): Boolean = false + + override fun getState(level: FileEditorStateLevel): FileEditorState = FileEditorState.INSTANCE + + override fun setState(state: FileEditorState) {} + + override fun getPreferredFocusedComponent(): JComponent? = null + + override fun getCurrentLocation(): FileEditorLocation? = null + + override fun selectNotify() {} + + override fun deselectNotify() {} + + override fun getBackgroundHighlighter(): BackgroundEditorHighlighter? = null + + override fun isValid(): Boolean = true + + override fun addPropertyChangeListener(listener: PropertyChangeListener) {} + + override fun removePropertyChangeListener(listener: PropertyChangeListener) {} +} + +class S3BucketViewerProvider : FileEditorProvider, DumbAware { + override fun getEditorTypeId() = EDITOR_TYPE_ID + + override fun accept(project: Project, file: VirtualFile) = file is S3BucketVirtualFile + + override fun createEditor(project: Project, file: VirtualFile) = S3BucketViewer(project, file as S3BucketVirtualFile) + + override fun getPolicy() = FileEditorPolicy.HIDE_DEFAULT_EDITOR + + companion object { + const val EDITOR_TYPE_ID = "s3Bucket" + } +} \ No newline at end of file diff --git a/intellij/src/main/kotlin/software/aws/toolkits/jetbrains/utils/DateUtils.kt b/intellij/src/main/kotlin/software/aws/toolkits/jetbrains/utils/DateUtils.kt new file mode 100644 index 00000000000..e51b6caf052 --- /dev/null +++ b/intellij/src/main/kotlin/software/aws/toolkits/jetbrains/utils/DateUtils.kt @@ -0,0 +1,17 @@ +package com.amazonaws.intellij.utils + +import java.time.Instant +import java.time.ZoneOffset +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter + +class DateUtils { + companion object { + @JvmStatic + fun formatDate(epochMills: Long): String { + val instant = Instant.ofEpochMilli(epochMills) + + return DateTimeFormatter.RFC_1123_DATE_TIME.format(ZonedDateTime.ofInstant(instant, ZoneOffset.UTC)) + } + } +} \ No newline at end of file diff --git a/intellij/src/main/resources/META-INF/plugin.xml b/intellij/src/main/resources/META-INF/plugin.xml index d97d7a68641..d6a71c22074 100644 --- a/intellij/src/main/resources/META-INF/plugin.xml +++ b/intellij/src/main/resources/META-INF/plugin.xml @@ -28,6 +28,9 @@ + + +