Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import java.util.prefs.BackingStoreException;

import static dev.ikm.komet.preferences.KLEditorPreferences.KL_EDITOR_WINDOWS;
import static dev.ikm.komet.preferences.KLEditorPreferences.KL_MAIN_SECTION;

/**
* Seeds the standard (application-provided) KL Windows into the standard-windows folder of the
Expand Down Expand Up @@ -82,15 +83,24 @@ public static void ensureStandardWindows(KometPreferences standardWindowsPrefere
standardWindowsPreferences.putInt(STANDARD_WINDOWS_VERSION_KEY, CURRENT_STANDARD_WINDOWS_VERSION);
}

List<String> standardWindows = standardWindowsPreferences.getList(KL_EDITOR_WINDOWS);
if (!standardWindows.contains(CONCEPT_WINDOW_2)) {
if (!isSaved(standardWindowsPreferences, CONCEPT_WINDOW_2)) {
saveConceptWindow2(standardWindowsPreferences, viewCalculator);
}
if (!standardWindows.contains(PATTERN_WINDOW_2)) {
if (!isSaved(standardWindowsPreferences, PATTERN_WINDOW_2)) {
savePatternWindow2(standardWindowsPreferences, viewCalculator);
}
}

/**
* Whether the window is both listed and actually written. The list alone is not enough: a
* seeding that failed part way can leave the title listed with no window definition behind it,
* which then fails every load with "Can't load section (main section) from preferences".
*/
private static boolean isSaved(KometPreferences standardWindowsPreferences, String windowTitle) {
return standardWindowsPreferences.getList(KL_EDITOR_WINDOWS).contains(windowTitle)
&& standardWindowsPreferences.node(windowTitle).get(KL_MAIN_SECTION).isPresent();
}

/**
* Removes every seeded standard window definition (their window nodes and the window list),
* so the seeding in {@link #ensureStandardWindows} re-creates them from the current code.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,9 @@ public T fromString(String string) {
// TODO: might be another way to fix by using CSS to fix this issue or examine height of popup (ListView).
Platform.runLater(() -> {
final Node scrollBar = comboBox.lookup(".scroll-bar:vertical");
if (scrollBar == null) {
return; // no popup scroll bar to hide (yet) — nothing to work around
}
scrollBar.setStyle("-fx-pref-width: 0; -fx-padding: 0;");
scrollBar.applyCss();
scrollBar.setVisible(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import dev.ikm.komet.kview.mvvm.viewmodel.ExportViewModel;
import dev.ikm.tinkar.common.alert.AlertStreams;
import dev.ikm.tinkar.coordinate.view.calculator.ViewCalculator;
import dev.ikm.tinkar.common.service.TrackingCallable;
import dev.ikm.tinkar.entity.export.ExportEntitiesToProtobufFile;
import dev.ikm.tinkar.terms.EntityFacade;
import javafx.beans.InvalidationListener;
Expand Down Expand Up @@ -209,7 +210,12 @@ private void setUpPathOptions() {
@Override
public String toString(EntityFacade conceptEntity) {
ViewCalculator viewCalculator = getViewProperties().calculator();
return (conceptEntity != null) ? viewCalculator.getRegularDescriptionText(conceptEntity).get() : "";
// orElseGet, not get(): over gRPC a path's descriptions may not be loaded locally
// yet, and an empty Optional here threw while the dialog was being built.
return (conceptEntity != null)
? viewCalculator.getRegularDescriptionText(conceptEntity)
.orElseGet(() -> viewCalculator.getPreferredDescriptionTextWithFallbackOrNid(conceptEntity))
: "";
}

@Override
Expand Down Expand Up @@ -347,7 +353,14 @@ private void performChangeSetExport(final FileSavePicker fileSavePicker, final l
return CompletableFuture.failedFuture(new IllegalArgumentException("Export file cannot be null"));
}

ExportEntitiesToProtobufFile exportEntities = new ExportEntitiesToProtobufFile(exportFile, fromDate, toDate);
// Connected to a server: export from there, where all the data is, rather than from
// this Komet's local view, which holds only what it has loaded.
TrackingCallable<dev.ikm.tinkar.common.service.EntityCountSummary> exportEntities = RemoteChangesetTask.remote()
.<TrackingCallable<dev.ikm.tinkar.common.service.EntityCountSummary>>map(remote ->
new RemoteChangesetTask(remote, "Export change set from the server",
(service, listener, tracker) ->
service.exportChangeSet(exportFile, fromDate, toDate, listener, tracker)))
.orElseGet(() -> new ExportEntitiesToProtobufFile(exportFile, fromDate, toDate));
CompletableFuture<dev.ikm.tinkar.common.service.EntityCountSummary> exportFuture = ProgressHelper.progress(exportEntities, "Cancel Export");

exportFuture.handle((result, throwable) -> {
Expand Down Expand Up @@ -381,7 +394,12 @@ private void performMembershipSetExport(final FileSavePicker fileSavePicker) {
// map TagsDataModel to a publicId
EntityHandle.get(Integer.parseInt(tagsDataModel.tagNid)).expectEntity().publicId()
).toList();
ExportEntitiesToProtobufFile exportEntities = new ExportEntitiesToProtobufFile(exportFile, membershipPublicIds);
TrackingCallable<dev.ikm.tinkar.common.service.EntityCountSummary> exportEntities = RemoteChangesetTask.remote()
.<TrackingCallable<dev.ikm.tinkar.common.service.EntityCountSummary>>map(remote ->
new RemoteChangesetTask(remote, "Export tagged modules from the server",
(service, listener, tracker) ->
service.exportMembership(exportFile, membershipPublicIds, listener, tracker)))
.orElseGet(() -> new ExportEntitiesToProtobufFile(exportFile, membershipPublicIds));
CompletableFuture<dev.ikm.tinkar.common.service.EntityCountSummary> exportFuture = ProgressHelper.progress(exportEntities, "Cancel Export");

exportFuture.handle((result, throwable) -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import dev.ikm.komet.kview.mvvm.viewmodel.ImportViewModel;
import dev.ikm.tinkar.common.alert.AlertStreams;
import dev.ikm.tinkar.common.service.EntityCountSummary;
import dev.ikm.tinkar.common.service.TrackingCallable;
import dev.ikm.tinkar.entity.load.LoadEntitiesFromProtobufFile;
import javafx.application.Platform;
import javafx.css.PseudoClass;
Expand Down Expand Up @@ -216,7 +217,13 @@ void handleImportButtonEvent(ActionEvent event) {

if (importViewModel.validProperty().get()) {
File selectedFile = importViewModel.getPropertyValue(SELECTED_FILE);
LoadEntitiesFromProtobufFile importTask = new LoadEntitiesFromProtobufFile(selectedFile);
// Connected to a server: import there, where the data lives, rather than into this
// Komet's local view of it.
TrackingCallable<EntityCountSummary> importTask = RemoteChangesetTask.remote()
.<TrackingCallable<EntityCountSummary>>map(remote -> new RemoteChangesetTask(remote,
"Import " + selectedFile.getName() + " on the server",
(service, listener, tracker) -> service.importChangeset(selectedFile, listener, tracker)))
.orElseGet(() -> new LoadEntitiesFromProtobufFile(selectedFile));
FrameworkTopics destTopic = importViewModel.getPropertyValue(DESTINATION_TOPIC);
CompletableFuture<EntityCountSummary> future = ProgressHelper.progress(destTopic, importTask, "Cancel Import");
future.whenComplete((entityCountSummary, throwable) -> {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* Copyright © 2015 Integrated Knowledge Management (support@ikm.dev)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.ikm.komet.kview.mvvm.view.changeset;

import dev.ikm.tinkar.common.service.EntityCountSummary;
import dev.ikm.tinkar.common.service.RemoteChangesetService;
import dev.ikm.tinkar.common.service.ServiceLifecycleManager;
import dev.ikm.tinkar.common.service.TrackingCallable;

import java.util.Optional;

/**
* Runs a changeset import or export on the remote datastore, for Komet connected over gRPC.
*
* <p>In that mode Komet's own store is only a local view of the server's: importing into it would
* be lost on close and seen by nobody else, and exporting from it would cover only what Komet had
* loaded. So the import and export dialogs hand the work to the server whenever a
* {@link RemoteChangesetService} is running, and to the local loader and exporter otherwise.
*/
final class RemoteChangesetTask extends TrackingCallable<EntityCountSummary> {

/** The remote call to make, given where to report progress and the handle that cancels it. */
@FunctionalInterface
interface Work {
EntityCountSummary run(RemoteChangesetService service, RemoteChangesetService.ProgressListener listener,
TrackingCallable<?> tracker);
}

private final RemoteChangesetService service;
private final Work work;

RemoteChangesetTask(RemoteChangesetService service, String title, Work work) {
super(true, true);
this.service = service;
this.work = work;
updateTitle(title);
}

/** The remote service, when Komet is connected to one. */
static Optional<RemoteChangesetService> remote() {
return ServiceLifecycleManager.get().getRunningService(RemoteChangesetService.class);
}

@Override
protected EntityCountSummary compute() {
updateProgress(-1, 1);
EntityCountSummary result = work.run(service, (done, total, message) -> {
if (message != null && !message.isBlank()) {
updateMessage(message);
}
if (total > 1 && done >= 0) {
updateProgress(done, total);
}
}, this);
updateProgress(1, 1);
updateMessage("Done on the server in " + durationString());
return result;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -137,9 +137,15 @@ public void initialize() {
patternEntity, StandardEditorWindows.PATTERN_WINDOW_2));
}
} else if (remotePublicIds != null && !remotePublicIds.isEmpty()) {
// Remote-backed result: fetch full entity graph from the server, load into
// ephemeral store, then open the concept window as normal.
openRemoteConcept();
// Remote-backed result: fetch the concept from the server first, then open
// the same windows a local result does — Concept (2), or classic with shift.
boolean classic = mouseEvent.isShiftDown();
withConcept(conceptEntity -> eventBus.publish(
searchEntryViewModel.getPropertyValue(CURRENT_JOURNAL_WINDOW_TOPIC),
classic
? new MakeConceptWindowEvent(this, MakeConceptWindowEvent.OPEN_CONCEPT_FROM_CONCEPT, conceptEntity)
: new MakeKLWindowEvent(this, MakeKLWindowEvent.OPEN_STANDARD_WINDOW,
conceptEntity, StandardEditorWindows.CONCEPT_WINDOW_2)));
}
}
}
Expand All @@ -153,24 +159,65 @@ public void initialize() {
}

public void populateConcept(ActionEvent actionEvent) {
if (entity instanceof ConceptEntity conceptEntity) {
eventBus.publish(searchEntryViewModel.getPropertyValue(CURRENT_JOURNAL_WINDOW_TOPIC), new MakeConceptWindowEvent(this,
MakeConceptWindowEvent.OPEN_CONCEPT_FROM_CONCEPT, conceptEntity));
}
withConcept(conceptEntity -> eventBus.publish(searchEntryViewModel.getPropertyValue(CURRENT_JOURNAL_WINDOW_TOPIC),
new MakeConceptWindowEvent(this, MakeConceptWindowEvent.OPEN_CONCEPT_FROM_CONCEPT, conceptEntity)));
}

public void openInConceptNavigator(ActionEvent actionEvent) {
if (entity instanceof ConceptEntity conceptEntity) {
eventBus.publish(searchEntryViewModel.getPropertyValue(CURRENT_JOURNAL_WINDOW_TOPIC), new ShowNavigationalPanelEvent(this, ShowNavigationalPanelEvent.SHOW_CONCEPT_NAVIGATIONAL_FROM_CONCEPT, conceptEntity));
}
withConcept(conceptEntity -> eventBus.publish(searchEntryViewModel.getPropertyValue(CURRENT_JOURNAL_WINDOW_TOPIC),
new ShowNavigationalPanelEvent(this, ShowNavigationalPanelEvent.SHOW_CONCEPT_NAVIGATIONAL_FROM_CONCEPT, conceptEntity)));
}

public void openAsKLWindow(ActionEvent actionEvent, String windowTitle) {
UUID journalTopic = searchEntryViewModel.getPropertyValue(CURRENT_JOURNAL_WINDOW_TOPIC);
if (entity == null) {
withConcept(conceptEntity -> eventBus.publish(journalTopic,
new MakeKLWindowEvent(this, MakeKLWindowEvent.OPEN_ENTITY_FROM_ENTITY, conceptEntity, windowTitle)));
return;
}
eventBus.publish(journalTopic,
new MakeKLWindowEvent(this, MakeKLWindowEvent.OPEN_ENTITY_FROM_ENTITY, entity, windowTitle));
}

/**
* Runs {@code action} with this result's concept on the UI thread.
*
* <p>A local result has its entity already. A result from a remote search (Komet connected over
* gRPC) carries only the concept's UUIDs, so the concept is fetched from the server into the
* local store first — every action on the row works the same either way, where before only a
* double-click fetched it and the menu actions silently did nothing. The fetched concept is kept,
* so later actions on the same row do not fetch again.
*/
private void withConcept(java.util.function.Consumer<ConceptEntity> action) {
if (entity instanceof ConceptEntity conceptEntity) {
action.accept(conceptEntity);
return;
}
if (remotePublicIds == null || remotePublicIds.isEmpty()) {
return;
}
List<UUID> ids = List.copyOf(remotePublicIds);
Thread.ofVirtual().start(() -> {
try {
RemoteConceptSearchService remote = ServiceLifecycleManager.get()
.getRunningService(RemoteConceptSearchService.class)
.orElseThrow(() -> new IllegalStateException("RemoteConceptSearchService not available"));
long nid = remote.loadConceptWithSemantics(ids);
Entity<?> loaded = EntityHandle.get(nid).orNull();
if (loaded instanceof ConceptEntity loadedConcept) {
Platform.runLater(() -> {
entity = loadedConcept;
action.accept(loadedConcept);
});
} else {
LOG.warn("Loaded entity for {} is not a ConceptEntity: {}", ids, loaded);
}
} catch (Exception ex) {
LOG.warn("Failed to load concept details from remote backend for {}: {}", ids, ex.getMessage());
}
});
}

public boolean isRetired() {
return retired;
}
Expand Down Expand Up @@ -249,36 +296,6 @@ public void setRemotePublicIds(List<UUID> publicIds) {
this.remotePublicIds = publicIds;
}

/**
* Background-fetches the concept entity graph from the active {@link RemoteConceptSearchService},
* loads it into the local ephemeral entity store, then fires {@link MakeConceptWindowEvent}
* on the UI thread.
*/
private void openRemoteConcept() {
List<UUID> ids = List.copyOf(remotePublicIds);
UUID journalTopic = searchEntryViewModel.getPropertyValue(CURRENT_JOURNAL_WINDOW_TOPIC);
Thread.ofVirtual().start(() -> {
try {
RemoteConceptSearchService remote = ServiceLifecycleManager.get()
.getRunningService(RemoteConceptSearchService.class)
.orElseThrow(() -> new IllegalStateException("RemoteConceptSearchService not available"));
long nid = remote.loadConceptWithSemantics(ids);
Entity<?> loaded = EntityHandle.get(nid).orNull();
if (loaded instanceof ConceptEntity loadedConcept) {
Platform.runLater(() ->
eventBus.publish(journalTopic,
new MakeConceptWindowEvent(this,
MakeConceptWindowEvent.OPEN_CONCEPT_FROM_CONCEPT,
loadedConcept)));
} else {
LOG.warn("Loaded entity for {} is not a ConceptEntity: {}", ids, loaded);
}
} catch (Exception ex) {
LOG.warn("Failed to load concept details from remote backend for {}: {}", ids, ex.getMessage());
}
});
}

public void setWindowView(ObservableViewNoOverride windowView) {
this.windowView = windowView;
}
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<groupId>dev.ikm.komet</groupId>
<artifactId>komet-parent</artifactId>
<name>KOMET</name>
<version>1.59.5-SNAPSHOT</version>
<version>1.59.5-changeset_import_export-SNAPSHOT</version>
<packaging>pom</packaging>
<inceptionYear>2015</inceptionYear>
<licenses>
Expand Down
Loading