diff --git a/build.gradle b/build.gradle index ab6002b..2639925 100644 --- a/build.gradle +++ b/build.gradle @@ -11,7 +11,7 @@ plugins { } group = 'at.esque.kafka' -version = '2.9.0' +version = '2.9.1' repositories { mavenCentral() diff --git a/src/main/java/at/esque/kafka/Controller.java b/src/main/java/at/esque/kafka/Controller.java index 9422962..69f4ad2 100644 --- a/src/main/java/at/esque/kafka/Controller.java +++ b/src/main/java/at/esque/kafka/Controller.java @@ -47,24 +47,35 @@ import io.confluent.kafka.schemaregistry.client.rest.exceptions.RestClientException; import javafx.application.HostServices; import javafx.application.Platform; -import javafx.beans.InvalidationListener; -import javafx.beans.Observable; import javafx.beans.binding.Bindings; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleStringProperty; -import javafx.beans.value.ChangeListener; -import javafx.collections.*; -import javafx.event.*; -import javafx.event.Event; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.event.ActionEvent; +import javafx.event.EventHandler; import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; -import javafx.scene.*; -import javafx.scene.control.*; +import javafx.scene.Parent; +import javafx.scene.Scene; import javafx.scene.control.Button; +import javafx.scene.control.ComboBox; +import javafx.scene.control.ContextMenu; import javafx.scene.control.Label; +import javafx.scene.control.ListCell; import javafx.scene.control.MenuBar; import javafx.scene.control.MenuItem; +import javafx.scene.control.ProgressIndicator; +import javafx.scene.control.SelectionMode; +import javafx.scene.control.SplitPane; +import javafx.scene.control.Tab; +import javafx.scene.control.TabPane; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableRow; +import javafx.scene.control.TableView; import javafx.scene.control.TextField; +import javafx.scene.control.ToggleButton; +import javafx.scene.control.Tooltip; import javafx.scene.image.Image; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyCodeCombination; @@ -76,7 +87,6 @@ import javafx.stage.FileChooser; import javafx.stage.Modality; import javafx.stage.Stage; -import javafx.stage.Window; import javafx.util.Pair; import net.thisptr.jackson.jq.BuiltinFunctionLoader; import net.thisptr.jackson.jq.JsonQuery; @@ -327,18 +337,19 @@ public void setup(Stage controlledStage) { helpIconToolTip.setText(buildToolTip()); versionInfoHandler.showDialogIfUpdateIsAvailable(hostServices); - messageTabPane.addEventHandler(Event.ANY, this::onMessageTabPaneSelectionChange); + messageTabPane.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> { + String text = Optional.ofNullable(newValue) + .map(Tab::getText) + .orElse(null); + showTextInStageTitle(text); + }); } - private void onMessageTabPaneSelectionChange(Event action) { - String windowName = Optional.ofNullable(messageTabPane.getSelectionModel()) - .map(SingleSelectionModel::getSelectedItem) - .map(Tab::getText) - .orElse(null); - if (Strings.isNullOrEmpty(windowName)) { + private void showTextInStageTitle(String text) { + if (Strings.isNullOrEmpty(text)) { controlledStage.setTitle("Kafkaesque"); } else { - controlledStage.setTitle("Kafkaesque - " + windowName); + controlledStage.setTitle("Kafkaesque - " + text); } } @@ -479,50 +490,50 @@ private ListCell topicListCellFactory() { TopicMessageTypeConfig topicMessageTypeConfig = configHandler.getConfigForTopic(selectedCluster().getIdentifier(), selectedTopic()); Map consumerConfig = configHandler.readConsumerConfigs(selectedCluster().getIdentifier()); TraceInputDialog.show(topicMessageTypeConfig.getKeyType() == MessageType.AVRO || topicMessageTypeConfig.getKeyType() == MessageType.PROTOBUF_SR, Settings.isTraceQuickSelectEnabled(configHandler.getSettingsProperties()), Settings.readDurationSetting(configHandler.getSettingsProperties()), Integer.parseInt(configHandler.getSettingsProperties().get(Settings.RECENT_TRACE_MAX_ENTRIES)), partitionCombobox.getItems()) - .ifPresent(traceInput -> { - backGroundTaskHolder.setBackGroundTaskDescription("tracing message"); - Integer partition = null; - if (!traceInput.getConditionMode().equals("value only") && !traceInput.getConditionMode().equals("OR") && topicMessageTypeConfig.getKeyType() != MessageType.AVRO && traceInput.isFastTrace()) { - partition = getPartitionForKey(selectedTopic(), traceInput.getKeySearch()); - } else if (!traceInput.isFastTrace() && traceInput.getPartition() != null && traceInput.getPartition() > -1) { - partition = traceInput.getPartition(); - } - Predicate keyPredicate = null; - Predicate valuePredicate = null; - Predicate actualPredicate = null; + .ifPresent(traceInput -> { + backGroundTaskHolder.setBackGroundTaskDescription("tracing message"); + Integer partition = null; + if (!traceInput.getConditionMode().equals("value only") && !traceInput.getConditionMode().equals("OR") && topicMessageTypeConfig.getKeyType() != MessageType.AVRO && traceInput.isFastTrace()) { + partition = getPartitionForKey(selectedTopic(), traceInput.getKeySearch()); + } else if (!traceInput.isFastTrace() && traceInput.getPartition() != null && traceInput.getPartition() > -1) { + partition = traceInput.getPartition(); + } + Predicate keyPredicate = null; + Predicate valuePredicate = null; + Predicate actualPredicate = null; - if (!traceInput.getConditionMode().equals("value only")) { - keyPredicate = TraceUtils.keyPredicate(traceInput.getKeySearch(), traceInput.getKeyMode()); - } - if (!traceInput.getConditionMode().equals("key only")) { - valuePredicate = TraceUtils.valuePredicate(traceInput.getValueSearch(), traceInput.isSearchNull()); - } + if (!traceInput.getConditionMode().equals("value only")) { + keyPredicate = TraceUtils.keyPredicate(traceInput.getKeySearch(), traceInput.getKeyMode()); + } + if (!traceInput.getConditionMode().equals("key only")) { + valuePredicate = TraceUtils.valuePredicate(traceInput.getValueSearch(), traceInput.isSearchNull()); + } - if (traceInput.getConditionMode().equals("key only")) { - actualPredicate = keyPredicate; - } else if (traceInput.getConditionMode().equals("value only")) { - actualPredicate = valuePredicate; - } else if (traceInput.getConditionMode().equals("AND")) { - actualPredicate = keyPredicate.and(valuePredicate); - } else if (traceInput.getConditionMode().equals("OR")) { - actualPredicate = keyPredicate.or(valuePredicate); - } + if (traceInput.getConditionMode().equals("key only")) { + actualPredicate = keyPredicate; + } else if (traceInput.getConditionMode().equals("value only")) { + actualPredicate = valuePredicate; + } else if (traceInput.getConditionMode().equals("AND")) { + actualPredicate = keyPredicate.and(valuePredicate); + } else if (traceInput.getConditionMode().equals("OR")) { + actualPredicate = keyPredicate.or(valuePredicate); + } - if (traceInput.getKafkaHeaderFilterOptions() != null && !traceInput.getKafkaHeaderFilterOptions().isEmpty()) { - List> predicates = traceInput.getKafkaHeaderFilterOptions() - .stream() - .map(TraceUtils::consumerRecordHeaderPredicate) - .toList(); + if (traceInput.getKafkaHeaderFilterOptions() != null && !traceInput.getKafkaHeaderFilterOptions().isEmpty()) { + List> predicates = traceInput.getKafkaHeaderFilterOptions() + .stream() + .map(TraceUtils::consumerRecordHeaderPredicate) + .toList(); - for (Predicate predicate : predicates) { - actualPredicate = actualPredicate.and(predicate); - } + for (Predicate predicate : predicates) { + actualPredicate = actualPredicate.and(predicate); + } - } + } - trace(topicMessageTypeConfig, consumerConfig, actualPredicate, partition, traceInput.getEpochStart(), traceInput.getEpochEnd() == null ? null : consumerRecord -> consumerRecord.timestamp() >= traceInput.getEpochEnd()); - }); + trace(topicMessageTypeConfig, consumerConfig, actualPredicate, partition, traceInput.getEpochStart(), traceInput.getEpochEnd() == null ? null : consumerRecord -> consumerRecord.timestamp() >= traceInput.getEpochEnd()); + }); } catch (Exception e) { ErrorAlert.show(e, controlledStage); } @@ -934,8 +945,8 @@ private void subscribeOrAssignToSelectedPartition(TopicMessageTypeConfig topic, consumerHandler.getConsumer(consumerId).ifPresent(topicConsumer -> topicConsumer.assign(Collections.singletonList(new TopicPartition(selectedTopic(), selectedPartition)))); } else { List partitions = adminClient.getPatitions(topic.getName()).stream() - .map(integer -> new TopicPartition(topic.getName(), integer)) - .collect(Collectors.toList()); + .map(integer -> new TopicPartition(topic.getName(), integer)) + .collect(Collectors.toList()); consumerHandler.getConsumer(consumerId).ifPresent(topicConsumer -> topicConsumer.assign(partitions)); } } @@ -1020,8 +1031,8 @@ private void trace(TopicMessageTypeConfig topic, Map co } if (stopTraceInPartitionCondition != null && stopTraceInPartitionCondition.test(cr)) { Optional first = topicPatitions.stream() - .filter(topicPartition -> topicPartition.partition() == cr.partition()) - .findFirst(); + .filter(topicPartition -> topicPartition.partition() == cr.partition()) + .findFirst(); first.ifPresent(topicPartition -> { topicPatitions.remove(topicPartition); @@ -1184,14 +1195,14 @@ private void getMessagesStartingFromInstant(TopicMessageTypeConfig topic, Map maxOffsets, Map minOffsets, Map currentOffsets) { + (Map maxOffsets, Map minOffsets, Map currentOffsets) { return maxOffsets.entrySet().stream() - .noneMatch(maxOffset -> (maxOffset.getValue() > -1 && maxOffset.getValue() > minOffsets.get(maxOffset.getKey()) && (currentOffsets.get(maxOffset.getKey()) == null || (maxOffset.getValue() - 1 > (currentOffsets.get(maxOffset.getKey()) == null ? 0L : currentOffsets.get(maxOffset.getKey())))))); + .noneMatch(maxOffset -> (maxOffset.getValue() > -1 && maxOffset.getValue() > minOffsets.get(maxOffset.getKey()) && (currentOffsets.get(maxOffset.getKey()) == null || (maxOffset.getValue() - 1 > (currentOffsets.get(maxOffset.getKey()) == null ? 0L : currentOffsets.get(maxOffset.getKey())))))); } private boolean reachedMaxOffsetForAllPartitionsOrGotEnoughMessages - (Map maxOffsets, Map minOffsets, Map currentOffsets, Map messagesConsumedPerPartition, long numberOfMessagesToConsume) { + (Map maxOffsets, Map minOffsets, Map currentOffsets, Map messagesConsumedPerPartition, long numberOfMessagesToConsume) { return maxOffsets.entrySet().stream().noneMatch(maxOffset -> { AtomicLong atomicLong = messagesConsumedPerPartition.get(maxOffset.getKey().partition()); boolean notEnoughMessagesConsumed = (atomicLong == null ? 0L : atomicLong.get()) < numberOfMessagesToConsume; @@ -1213,14 +1224,14 @@ public void addClusterConfigClick(ActionEvent event) { public void deleteClusterConfigsClick(ActionEvent event) { ObservableList clusterConfigs = clusterComboBox.getItems(); DeleteClustersDialog.show(clusterConfigs) - .ifPresent(deletedClusterConfigs -> { - StringBuilder builder = new StringBuilder(); - deletedClusterConfigs.forEach(config -> builder.append(config.toString()).append(System.lineSeparator())); - if (ConfirmationAlert.show("Deleting cluster configs", "The following configs will be permanently deleted:", builder.toString())) { - clusterConfigs.removeAll(deletedClusterConfigs); - configHandler.saveConfigs(); - } - }); + .ifPresent(deletedClusterConfigs -> { + StringBuilder builder = new StringBuilder(); + deletedClusterConfigs.forEach(config -> builder.append(config.toString()).append(System.lineSeparator())); + if (ConfirmationAlert.show("Deleting cluster configs", "The following configs will be permanently deleted:", builder.toString())) { + clusterConfigs.removeAll(deletedClusterConfigs); + configHandler.saveConfigs(); + } + }); } @FXML @@ -1378,9 +1389,9 @@ public void applyTopicTemplatesClick(ActionEvent event) { String currentTopic = topic.getName(); try { adminClient.createTopic(topic.getName(), - topic.getPartitions() > 0 ? topic.getPartitions() : defaultPartitions, - topic.getReplacationFactor() > 0 ? topic.getReplacationFactor() : defualtReplication, - topic.getConfigs()); + topic.getPartitions() > 0 ? topic.getPartitions() : defaultPartitions, + topic.getReplacationFactor() > 0 ? topic.getReplacationFactor() : defualtReplication, + topic.getConfigs()); createdTopics.add(currentTopic); } catch (Exception e) { if (e.getCause() instanceof TopicExistsException) { @@ -1403,13 +1414,13 @@ public void applyTopicTemplatesClick(ActionEvent event) { public void showSettingsDialog(ActionEvent event) { Map settingsProperties = configHandler.getSettingsProperties(); SettingsDialog.show(settingsProperties).ifPresent( - settingsProperties1 -> { - try { - configHandler.setSettingsProperties(settingsProperties1); - } catch (IOException e) { - ErrorAlert.show(e); + settingsProperties1 -> { + try { + configHandler.setSettingsProperties(settingsProperties1); + } catch (IOException e) { + ErrorAlert.show(e); + } } - } ); } @@ -1443,22 +1454,22 @@ public void playMessageBook(ActionEvent event) { Platform.runLater(() -> backGroundTaskHolder.setBackGroundTaskDescription("Playing Message Book: producing messages")); AtomicInteger counter = new AtomicInteger(0); messagesToSend.stream().sorted(Comparator.comparing(KafkaMessagBookWrapper::getTimestamp, Comparator.nullsLast(Comparator.naturalOrder()))) - .forEach(message -> { - try { - UUID producerId = topicToProducerMap.computeIfAbsent(message.getTargetTopic(), targetTopic -> { - try { - return producerHandler.registerProducer(selectedCluster(), targetTopic); - } catch (IOException e) { - throw new RuntimeException(e); - } - }); - producerHandler.sendMessage(producerId, message.getTargetTopic(), message.getPartition() == -1 ? null : message.getPartition(), message.getKey(), message.getValue(), message.getKeyType(), message.getValueType()); - Platform.runLater(() -> backGroundTaskHolder.setProgressMessage("published " + counter.incrementAndGet() + " messages")); - } catch (InterruptedException | ExecutionException | TimeoutException | IOException | - RestClientException e) { - throw new RuntimeException(e); - } - }); + .forEach(message -> { + try { + UUID producerId = topicToProducerMap.computeIfAbsent(message.getTargetTopic(), targetTopic -> { + try { + return producerHandler.registerProducer(selectedCluster(), targetTopic); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + producerHandler.sendMessage(producerId, message.getTargetTopic(), message.getPartition() == -1 ? null : message.getPartition(), message.getKey(), message.getValue(), message.getKeyType(), message.getValueType()); + Platform.runLater(() -> backGroundTaskHolder.setProgressMessage("published " + counter.incrementAndGet() + " messages")); + } catch (InterruptedException | ExecutionException | TimeoutException | IOException | + RestClientException e) { + throw new RuntimeException(e); + } + }); } catch (Exception e) { Platform.runLater(() -> ErrorAlert.show(e, controlledStage)); } finally { @@ -1481,10 +1492,10 @@ private void applyReplacements(List messagesToSend, Map< private void addMessagesToSend(List messagesToSend, File playFile) { try { List messages = new CsvToBeanBuilder(new FileReader(playFile.getAbsolutePath())) - .withType(KafkaMessage.class) - .build().parse(); + .withType(KafkaMessage.class) + .build().parse(); messagesToSend.addAll(messages.stream().map(message -> new KafkaMessagBookWrapper(playFile.getName(), message)) - .toList()); + .toList()); } catch (FileNotFoundException e) { Platform.runLater(() -> ErrorAlert.show(e, controlledStage)); } @@ -1533,39 +1544,39 @@ private PinTab createTab(ClusterConfig clusterConfig, String name, List { - final TableRow row = new TableRow<>(); - final ContextMenu rowMenu = new ContextMenu(); - MenuItem openinPublisher = new MenuItem("open in publisher"); - openinPublisher.setGraphic(new FontIcon(FontAwesome.SHARE)); - openinPublisher.setOnAction(event -> showPublishMessageDialog(row.getItem())); - MenuItem openAsTxt = new MenuItem("open in text editor"); - openAsTxt.setGraphic(new FontIcon(FontAwesome.EDIT)); - openAsTxt.setOnAction(event -> openInTextEditor(row.getItem(), "txt")); - MenuItem openAsJson = new MenuItem("open in json editor"); - openAsJson.setGraphic(new FontIcon(FontAwesome.EDIT)); - openAsJson.setOnAction(event -> openInTextEditor(row.getItem(), "json")); - MenuItem jsonDiff = new MenuItem("Json Diff"); - jsonDiff.setGraphic(new FontIcon(FontAwesome.EXCHANGE)); - jsonDiff.disableProperty().bind(Bindings.createBooleanBinding(() -> messagesTabContent.getMessageTableView().getSelectionModel().getSelectedItems() != null && messagesTabContent.getMessageTableView().getSelectionModel().getSelectedItems().size() != 2, messagesTabContent.getMessageTableView().getSelectionModel().getSelectedItems())); - jsonDiff.setOnAction(event -> { - final ObservableList selectedItems = messagesTabContent.getMessageTableView().getSelectionModel().getSelectedItems(); - if (selectedItems != null && selectedItems.size() == 2) { - showJsonDiffDialog(selectedItems.get(0), selectedItems.get(1)); - } else { - ErrorAlert.show("Unsupported selection", "Selection has to be exactly 2 message", null, null, controlledStage, false); - } + tableView -> { + final TableRow row = new TableRow<>(); + final ContextMenu rowMenu = new ContextMenu(); + MenuItem openinPublisher = new MenuItem("open in publisher"); + openinPublisher.setGraphic(new FontIcon(FontAwesome.SHARE)); + openinPublisher.setOnAction(event -> showPublishMessageDialog(row.getItem())); + MenuItem openAsTxt = new MenuItem("open in text editor"); + openAsTxt.setGraphic(new FontIcon(FontAwesome.EDIT)); + openAsTxt.setOnAction(event -> openInTextEditor(row.getItem(), "txt")); + MenuItem openAsJson = new MenuItem("open in json editor"); + openAsJson.setGraphic(new FontIcon(FontAwesome.EDIT)); + openAsJson.setOnAction(event -> openInTextEditor(row.getItem(), "json")); + MenuItem jsonDiff = new MenuItem("Json Diff"); + jsonDiff.setGraphic(new FontIcon(FontAwesome.EXCHANGE)); + jsonDiff.disableProperty().bind(Bindings.createBooleanBinding(() -> messagesTabContent.getMessageTableView().getSelectionModel().getSelectedItems() != null && messagesTabContent.getMessageTableView().getSelectionModel().getSelectedItems().size() != 2, messagesTabContent.getMessageTableView().getSelectionModel().getSelectedItems())); + jsonDiff.setOnAction(event -> { + final ObservableList selectedItems = messagesTabContent.getMessageTableView().getSelectionModel().getSelectedItems(); + if (selectedItems != null && selectedItems.size() == 2) { + showJsonDiffDialog(selectedItems.get(0), selectedItems.get(1)); + } else { + ErrorAlert.show("Unsupported selection", "Selection has to be exactly 2 message", null, null, controlledStage, false); + } + }); + rowMenu.getItems().addAll(openinPublisher, openAsTxt, openAsJson, jsonDiff); + + // only display context menu for non-null items: + row.contextMenuProperty().bind( + Bindings.when(Bindings.isNotNull(row.itemProperty())) + .then(rowMenu) + .otherwise((ContextMenu) null)); + return row; }); - rowMenu.getItems().addAll(openinPublisher, openAsTxt, openAsJson, jsonDiff); - - // only display context menu for non-null items: - row.contextMenuProperty().bind( - Bindings.when(Bindings.isNotNull(row.itemProperty())) - .then(rowMenu) - .otherwise((ContextMenu) null)); - return row; - }); messagesTabContent.getMessageTableView().getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); messagesTabContent.getMessageTableView().getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> { selectedMessage = newValue; @@ -1583,13 +1594,18 @@ private PinTab createTab(ClusterConfig clusterConfig, String name, List { + if (pinTab.isSelected()) { + showTextInStageTitle(newValue); + } + }); + return pinTab; } private void updateTabName(PinTab tab, ClusterConfig clusterConfig, String name) { Platform.runLater(() -> { tab.setText(clusterConfig.getIdentifier() + " - " + name); - onMessageTabPaneSelectionChange(null); }); } @@ -1616,8 +1632,8 @@ public void aboutClick(ActionEvent event) { private EventHandler generateHeaderTableEventHandler() { Map> copyCombinations = Map.of( - new KeyCodeCombination(KeyCode.C, KeyCombination.SHORTCUT_DOWN), header -> new String(header.value(), StandardCharsets.UTF_8), - new KeyCodeCombination(KeyCode.K, KeyCombination.SHORTCUT_DOWN), Header::key + new KeyCodeCombination(KeyCode.C, KeyCombination.SHORTCUT_DOWN), header -> new String(header.value(), StandardCharsets.UTF_8), + new KeyCodeCombination(KeyCode.K, KeyCombination.SHORTCUT_DOWN), Header::key ); return SystemUtils.generateTableCopySelectedItemCopyEventHandler(headerTableView, copyCombinations); @@ -1625,8 +1641,8 @@ private EventHandler generateHeaderTableEventHandler() { private EventHandler generateMetadataTableEventHandler() { Map> copyCombinations = Map.of( - new KeyCodeCombination(KeyCode.C, KeyCombination.SHORTCUT_DOWN), metadata -> metadata.valueAsString().getValue(), - new KeyCodeCombination(KeyCode.K, KeyCombination.SHORTCUT_DOWN), metadata -> metadata.nameProperty().getName() + new KeyCodeCombination(KeyCode.C, KeyCombination.SHORTCUT_DOWN), metadata -> metadata.valueAsString().getValue(), + new KeyCodeCombination(KeyCode.K, KeyCombination.SHORTCUT_DOWN), metadata -> metadata.nameProperty().getName() ); return SystemUtils.generateTableCopySelectedItemCopyEventHandler(metdataTableView, copyCombinations); @@ -1639,8 +1655,8 @@ public void setHostServices(HostServices hostServices) { private String buildToolTip() { return String.format("The following KeyCombinations let you copy data from the selected element in the metadata and header table%n" + - new KeyCodeCombination(KeyCode.K, KeyCombination.SHORTCUT_DOWN).getDisplayText() + " - copy Key/Name%n" + - new KeyCodeCombination(KeyCode.C, KeyCombination.SHORTCUT_DOWN).getDisplayText() + " - copy Value%n"); + new KeyCodeCombination(KeyCode.K, KeyCombination.SHORTCUT_DOWN).getDisplayText() + " - copy Key/Name%n" + + new KeyCodeCombination(KeyCode.C, KeyCombination.SHORTCUT_DOWN).getDisplayText() + " - copy Value%n"); } @FXML diff --git a/src/main/java/at/esque/kafka/alerts/UpdateAlert.java b/src/main/java/at/esque/kafka/alerts/UpdateAlert.java index 419c841..8b7f770 100644 --- a/src/main/java/at/esque/kafka/alerts/UpdateAlert.java +++ b/src/main/java/at/esque/kafka/alerts/UpdateAlert.java @@ -3,7 +3,9 @@ import at.esque.kafka.Main; import at.esque.kafka.alerts.model.UpdateDialogResult; import javafx.scene.Node; -import javafx.scene.control.*; +import javafx.scene.control.Alert; +import javafx.scene.control.ButtonBar; +import javafx.scene.control.ButtonType; import javafx.stage.Window; import java.util.Optional; @@ -21,7 +23,7 @@ public static UpdateDialogResult show(String title, String header, String conten Main.applyIcon(alert); ButtonType open = new ButtonType("Open", ButtonBar.ButtonData.FINISH); - ButtonType askAgain = new ButtonType("Ask in 24h again", ButtonBar.ButtonData.OK_DONE); + ButtonType askAgain = new ButtonType("Ask again in 24h", ButtonBar.ButtonData.OK_DONE); ButtonType cancel = new ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE); alert.getButtonTypes().setAll(open, askAgain, cancel);