written = new HashSet<>();
+ for (Plugin p : plugins) {
+ Path file = outDir.resolve(p.id() + ".adoc");
+ Files.writeString(file, render(p), StandardCharsets.UTF_8);
+ written.add(file);
+ System.out.printf(
+ "%-34s %2d options -> %s%n", p.name(), p.options().size(), root.relativize(file));
+ }
+
+ // One page wants the lot: a section per plugin, in the order the perspective's tree shows them.
+ StringBuilder all = new StringBuilder(LICENSE);
+ all.append(GENERATED_MARKER).append("\n// Edit those, not this file.\n");
+ for (Plugin p : plugins) {
+ all.append("\n== ").append(p.name()).append("\n\n").append(body(p));
+ }
+ Path index = outDir.resolve(AGGREGATE);
+ Files.writeString(index, all.toString(), StandardCharsets.UTF_8);
+ written.add(index);
+ System.out.println("aggregate -> " + root.relativize(index));
+
+ // A plugin that was renamed or removed leaves its page behind, and the pages that include one
+ // by name would go on rendering it for good. Nothing reads a generated page that this run did
+ // not write, so it goes.
+ for (Path stale : generatedBefore) {
+ if (!written.contains(stale)) {
+ Files.delete(stale);
+ System.out.println("removed (no longer generated) -> " + root.relativize(stale));
+ }
+ }
+ System.out.println(
+ "\n"
+ + plugins.size()
+ + " configuration plugins, "
+ + plugins.stream().mapToInt(p -> p.options().size()).sum()
+ + " options");
+ }
+
+ /**
+ * Which modules a documented plugin can come from is decided by this module's dependencies, and
+ * that list is written by hand. A plugin added to a module that is not on it would be absent from
+ * the manual with nothing to say so: the page set would not change, so the drift check in CI
+ * would pass, and the option would simply never be written about.
+ *
+ * So: sweep what the build has actually compiled and see whether anything turns up that the
+ * classpath did not. This is deliberately only a report - it never adds to the pages, because the
+ * set of modules on disk depends on what this particular build chose to compile, and the pages
+ * have to come out the same everywhere. An incremental build that has not compiled the new module
+ * simply finds nothing, which is why this is best-effort. A full build - every push to main is
+ * one - does find it.
+ */
+ static void reportPluginsOutsideTheClasspath(Path root, List documented)
+ throws IOException {
+ Set known = new HashSet<>();
+ for (Plugin p : documented) {
+ known.add(p.id());
+ }
+
+ Map missing = new TreeMap<>();
+ try (Stream walk = Files.walk(root)) {
+ for (Path index : walk.filter(ConfigDocGenerator::isModuleIndex).toList()) {
+ Path classes = index.getParent().getParent();
+ Source source = Source.of(classes);
+ if (source == null) continue;
+ Index jandex;
+ try (InputStream in = source.open(JANDEX_INDEX)) {
+ if (in == null) continue;
+ jandex = new IndexReader(in).read();
+ } catch (Exception e) {
+ continue;
+ }
+ for (AnnotationInstance ai : jandex.getAnnotations(CONFIG_PLUGIN)) {
+ if (ai.target().kind() != AnnotationTarget.Kind.CLASS) continue;
+ String id = str(ai, "id", "");
+ if (known.contains(id) || !hasPerspectiveWidgets(ai.target().asClass())) continue;
+ // target/classes/META-INF/jandex.idx -> the module directory
+ missing.put(id, root.relativize(classes.getParent().getParent()).toString());
+ }
+ }
+ }
+ if (missing.isEmpty()) return;
+
+ StringBuilder message =
+ new StringBuilder(
+ "These configuration plugins add options to the configuration perspective but are in"
+ + " modules this generator cannot see, so the manual would not describe them:\n");
+ missing.forEach(
+ (id, module) ->
+ message.append(" ").append(id).append(" in ").append(module).append("\n"));
+ message.append(
+ "Add the module as a dependency of config-doc/pom.xml, next to the others, and they will"
+ + " be documented on the next build.");
+ throw new IllegalStateException(message.toString());
+ }
+
+ /** target/classes/META-INF/jandex.idx under some module of this tree. */
+ static boolean isModuleIndex(Path path) {
+ return path.endsWith(Paths.get("target", "classes", "META-INF", "jandex.idx"));
+ }
+
+ /** Whether the class puts any widget on the configuration perspective. */
+ static boolean hasPerspectiveWidgets(ClassInfo ci) {
+ for (FieldInfo f : ci.fields()) {
+ AnnotationInstance w = f.annotation(GUI_WIDGET);
+ if (w != null && PARENT_ID.equals(str(w, "parentId", ""))) return true;
+ }
+ return false;
+ }
+
+ /**
+ * The .adoc files in the output directory that carry {@link #GENERATED_MARKER}. A file without it
+ * was written by hand - a README, a page someone dropped in - and this generator neither counts
+ * it nor deletes it.
+ */
+ static List generatedPages(Path outDir) throws IOException {
+ if (!Files.isDirectory(outDir)) return List.of();
+ List pages = new ArrayList<>();
+ try (Stream files = Files.list(outDir)) {
+ for (Path file : files.toList()) {
+ if (!file.getFileName().toString().endsWith(".adoc")) continue;
+ try {
+ if (Files.readString(file, StandardCharsets.UTF_8).contains(GENERATED_MARKER)) {
+ pages.add(file);
+ }
+ } catch (IOException e) {
+ // Unreadable, so not something to count on or delete.
+ }
+ }
+ }
+ return pages;
+ }
+
+ static Plugin readPlugin(ClassInfo ci, AnnotationInstance ai, Source source) {
+ List fields = new ArrayList<>();
+ for (FieldInfo f : ci.fields()) {
+ AnnotationInstance w = f.annotation(GUI_WIDGET);
+ // No default: parentId() itself defaults to "", so falling back to the config parent
+ // here would adopt every widget that simply did not name one.
+ if (w != null && PARENT_ID.equals(str(w, "parentId", ""))) fields.add(f);
+ }
+ if (fields.isEmpty()) return null;
+
+ Properties msgs = messages(source, ci.name().packagePrefix());
+ Map defaults = configDefaults(ai);
+
+ List options = new ArrayList<>();
+ for (FieldInfo f : fields) {
+ AnnotationInstance w = f.annotation(GUI_WIDGET);
+ // The configuration object is the truth where it has this setting; the annotation is the
+ // fallback for plugins that keep no such object.
+ Object fromConfig = defaults.get(f.name());
+ String dflt = fromConfig != null ? display(fromConfig, source) : str(w, "defaultValue", "");
+ options.add(
+ new Option(
+ str(w, "order", "") + str(w, "id", ""),
+ translate(str(w, "label", ""), msgs),
+ translate(str(w, "toolTip", ""), msgs),
+ w.value("type") == null ? "" : w.value("type").asEnum(),
+ w.value("variables") == null || w.value("variables").asBoolean(),
+ dflt));
+ }
+ options.sort(Comparator.comparing(Option::order));
+ // The configuration perspective labels the tree with the @GuiPlugin description, so that is
+ // the name a reader is looking for on the page.
+ AnnotationInstance gui = ci.declaredAnnotation(GUI_PLUGIN);
+ String name = gui == null ? "" : translate(str(gui, "description", ""), msgs);
+ if (name.isEmpty()) {
+ name = translate(str(ai, "description", ""), msgs);
+ }
+ return new Plugin(
+ str(ai, "id", ""),
+ translate(str(ai, "description", ""), msgs),
+ name,
+ str(ai, "configKey", ""),
+ options);
+ }
+
+ /** Instantiates the plugin's configClass and reads its fields: the real, current defaults. */
+ static Map configDefaults(AnnotationInstance ai) {
+ AnnotationValue cc = ai.value("configClass");
+ if (cc == null) return Map.of();
+ String name = cc.asClass().name().toString();
+ if (name.equals("java.lang.Void")) return Map.of();
+ try {
+ Class> c = Class.forName(name);
+ Constructor> ctor = c.getDeclaredConstructor();
+ ctor.setAccessible(true);
+ Object o = ctor.newInstance();
+ Map values = new HashMap<>();
+ for (Field f : c.getDeclaredFields()) {
+ if (Modifier.isStatic(f.getModifiers())) continue;
+ f.setAccessible(true);
+ Object v = f.get(o);
+ if (v != null
+ && !(v instanceof Collection> c2 && c2.isEmpty())
+ && !(v instanceof Map, ?> m && m.isEmpty())) {
+ values.put(f.getName(), v);
+ }
+ }
+ return values;
+ } catch (Throwable t) {
+ // Blank defaults would be committed and then stay green in the drift check, so stop here.
+ throw new IllegalStateException(
+ "Could not read the defaults from configClass "
+ + name
+ + " of @ConfigPlugin "
+ + str(ai, "id", ""),
+ t);
+ }
+ }
+
+ /**
+ * The value as the configuration perspective shows it. An enum's combo lists its getLabel(),
+ * which by convention is "{@code .}" in the enum's own bundle. That is read here
+ * rather than by calling getLabel(): BaseMessages would pull in HopConfig and the locale of
+ * whoever builds.
+ */
+ static String display(Object value, Source source) {
+ if (value instanceof Enum> e) {
+ Class> type = e.getDeclaringClass();
+ String label =
+ messages(source, type.getPackageName())
+ .getProperty(type.getSimpleName() + "." + e.name());
+ return label != null ? label : e.name();
+ }
+ return String.valueOf(value);
+ }
+
+ /** BaseMessages.getString(Class, key) resolves to .messages.messages. */
+ static Properties messages(Source source, String pkg) {
+ Properties props = new Properties();
+ if (pkg == null) return props;
+ String bundle = pkg.replace('.', '/') + "/messages/messages_en_US.properties";
+ try (InputStream in = source.open(bundle)) {
+ if (in != null) {
+ props.load(new InputStreamReader(in, StandardCharsets.UTF_8));
+ }
+ } catch (Exception ignored) {
+ // an unreadable bundle simply leaves the i18n key visible, which is the loudest signal
+ }
+ return props;
+ }
+
+ /**
+ * One place classes and resources are read from: a module's {@code target/classes} while the
+ * reactor is building it, or its jar once it has been installed. Both turn up on the classpath
+ * and both have to be readable the same way.
+ */
+ record Source(Path path, boolean jar) {
+
+ static Source of(Path path) {
+ if (Files.isDirectory(path)) return new Source(path, false);
+ if (Files.isRegularFile(path) && path.toString().endsWith(".jar")) {
+ return new Source(path, true);
+ }
+ return null;
+ }
+
+ /** The entry's contents, or null when this source does not hold it. */
+ InputStream open(String entry) throws IOException {
+ if (!jar) {
+ Path file = path.resolve(entry);
+ return Files.exists(file) ? Files.newInputStream(file) : null;
+ }
+ // Read the one entry out into memory: the caller closes the stream, not the archive.
+ try (ZipFile zip = new ZipFile(path.toFile())) {
+ ZipEntry zipEntry = zip.getEntry(entry);
+ if (zipEntry == null) return null;
+ try (InputStream in = zip.getInputStream(zipEntry)) {
+ return new ByteArrayInputStream(in.readAllBytes());
+ }
+ } catch (IOException e) {
+ return null;
+ }
+ }
+ }
+
+ static String translate(String value, Properties msgs) {
+ if (value == null || !value.startsWith("i18n:")) return value == null ? "" : value;
+ String[] parts = value.split(":");
+ if (parts.length != 3) return value;
+ return format(msgs.getProperty(parts[2], value));
+ }
+
+ /**
+ * Bundle values reach the user through {@code MessageFormat}, which Hop applies whether or not
+ * there are parameters. Running it here too is what makes a doubled apostrophe and a quoted
+ * '${VARIABLE}' read the same on the page as in the dialog.
+ */
+ static String format(String value) {
+ try {
+ return MessageFormat.format(value, new Object[0]);
+ } catch (IllegalArgumentException e) {
+ // Not a format pattern after all - the raw value is the best answer available.
+ return value;
+ }
+ }
+
+ static String str(AnnotationInstance ai, String name, String dflt) {
+ AnnotationValue v = ai.value(name);
+ return v == null ? dflt : v.asString();
+ }
+
+ static String render(Plugin p) {
+ return LICENSE + GENERATED_MARKER + "\n// Edit those, not this file.\n\n" + body(p);
+ }
+
+ static String body(Plugin p) {
+ StringBuilder sb = new StringBuilder();
+ if (!p.configKey().isEmpty()) {
+ sb.append("These options are stored under ")
+ .append(literal(p.configKey()))
+ .append(" in `hop-config.json`.\n\n");
+ }
+ sb.append("[%header, cols=\"2,5,1\"]\n|===\n|Option|Description|Default\n\n");
+ for (Option o : p.options()) {
+ sb.append("|").append(text(o.label())).append("\n");
+ String description = flatten(o.toolTip());
+ if (o.variables() && acceptsVariables(o.type())) {
+ description =
+ sentence(description) + (description.isEmpty() ? "" : " ") + "Accepts variables.";
+ }
+ if (isLongDefault(o.dflt())) {
+ // An AsciiDoc cell, so the default can be a block under the description where there is
+ // room for it. The Default column is a narrow one and a long value wraps to a word a line.
+ sb.append("a|")
+ .append(text(description))
+ .append("\n\n.Default\n[listing]\n----\n")
+ .append(o.dflt().strip())
+ .append("\n----\n");
+ sb.append("|_(with the description)_\n\n");
+ } else {
+ sb.append("|").append(text(description)).append("\n");
+ sb.append("|").append(o.dflt().isEmpty() ? "" : literal(o.dflt())).append("\n\n");
+ }
+ }
+ sb.append("|===\n");
+ return sb.toString();
+ }
+
+ /**
+ * A checkbox is built as a plain SWT {@code Button}, which has nowhere to put a variable, so its
+ * {@code variables()} flag never reaches the user however it is set. Only the widgets backed by a
+ * text field really take one.
+ */
+ static boolean acceptsVariables(String type) {
+ return switch (type) {
+ case "TEXT", "MULTI_LINE_TEXT", "FILENAME", "FOLDER", "COMBO", "METADATA" -> true;
+ default -> false;
+ };
+ }
+
+ /** Rounds off a tooltip so a sentence appended after it does not run on. */
+ static String sentence(String s) {
+ if (s.isEmpty() || s.endsWith(".") || s.endsWith("!") || s.endsWith("?")) return s;
+ return s + ".";
+ }
+
+ /** Collapses a label or tooltip onto the single line a table cell is written on. */
+ static String flatten(String s) {
+ return s == null ? "" : s.replace("\n", " ").replace("\r", " ").replaceAll(" +", " ").trim();
+ }
+
+ /**
+ * A label or tooltip as a table cell. These are prose written for a dialog, not AsciiDoc, and
+ * they contain things AsciiDoc would otherwise read as markup: {@code **} in a glob, {@code
+ * ${VARIABLE}} and {@code {NAME}} in an example, a {@code |} anywhere at all. {@code pass:c[]}
+ * applies character substitution - so {@code <}, {@code >} and {@code &} still reach the page
+ * safely - and no other substitution, which leaves every one of those alone. Escaping them one at
+ * a time is what produced visible backslashes in the first version of this file.
+ */
+ static String text(String s) {
+ String flat = flatten(s);
+ return flat.isEmpty() ? "" : "pass:c[" + flat.replace("]", "\\]") + "]";
+ }
+
+ /**
+ * Whether a default is too big for the Default column. Most are a word or a number; a few - the
+ * AI Assistant's standing prompt, for one - are paragraphs, and those belong in a block rather
+ * than in a column an eighth of the table wide.
+ */
+ static boolean isLongDefault(String s) {
+ return s.length() > 80 || s.contains("\n");
+ }
+
+ /**
+ * A default value as monospaced text. The {@code `+...+`} form looks like the one to reach for
+ * but it is a passthrough, so nothing inside it is substituted - including the {@code {plus}} a
+ * literal {@code +} would have to be written as. {@code pass:c[]} inside the backticks keeps the
+ * monospace and leaves the value alone.
+ */
+ static String literal(String s) {
+ return "`pass:c[" + flatten(s).replace("]", "\\]") + "]`";
+ }
+}
diff --git a/core/src/main/java/org/apache/hop/core/config/plugin/ConfigPlugin.java b/core/src/main/java/org/apache/hop/core/config/plugin/ConfigPlugin.java
index 509e0648ba8..9143f538022 100644
--- a/core/src/main/java/org/apache/hop/core/config/plugin/ConfigPlugin.java
+++ b/core/src/main/java/org/apache/hop/core/config/plugin/ConfigPlugin.java
@@ -51,4 +51,28 @@
* plugin folder that also uses {@code classLoaderGroup} on metadata or GUI types.
*/
String classLoaderGroup() default "";
+
+ /**
+ * The key this plugin's options are stored under in {@code hop-config.json}, for example {@code
+ * googleCloud}. Empty when the plugin writes its options as individual top-level options rather
+ * than as one block. Documentation generators use this to name the JSON block they describe.
+ *
+ * @return The hop-config.json key, or an empty String
+ */
+ String configKey() default "";
+
+ /**
+ * The plain configuration object holding this plugin's settings and their default values, for
+ * example {@code GoogleCloudConfig.class}. Its no-argument constructor must set the defaults and
+ * must not need a running Hop: documentation generators instantiate it at build time and read the
+ * fields to report what each option defaults to.
+ *
+ * A field is matched to this class by name, so a widget field and the setting it edits have to
+ * be called the same thing. Where there is no matching field - the plugin reads the option
+ * straight from {@link org.apache.hop.core.config.HopConfig}, say - the default belongs on {@code
+ * GuiWidgetElement#defaultValue()} instead.
+ *
+ * @return The configuration class, or {@link Void} when the plugin has none
+ */
+ Class> configClass() default Void.class;
}
diff --git a/core/src/main/java/org/apache/hop/core/gui/plugin/GuiWidgetElement.java b/core/src/main/java/org/apache/hop/core/gui/plugin/GuiWidgetElement.java
index dae7253be99..e170ea7441a 100644
--- a/core/src/main/java/org/apache/hop/core/gui/plugin/GuiWidgetElement.java
+++ b/core/src/main/java/org/apache/hop/core/gui/plugin/GuiWidgetElement.java
@@ -178,4 +178,17 @@
* GuiWidgetGroupType#NONE} value wins. Mixed types log a warning and fall back to tabs.
*/
GuiWidgetGroupType groupType() default GuiWidgetGroupType.NONE;
+
+ /**
+ * What this option falls back to when it has never been set, as it would be written in the
+ * documentation ({@code "true"}, {@code "1000"}, ...). Only for widgets whose value has no field
+ * of the same name on the {@code configClass()} of their {@code @ConfigPlugin} - a plugin reading
+ * the option straight from {@link org.apache.hop.core.config.HopConfig} with an inline fallback,
+ * for instance. Where that field does exist, leave this empty: a documentation generator reads
+ * the real default off the configuration object, which cannot fall out of step with the code the
+ * way a copy here can.
+ *
+ * @return The default value as text, or an empty String
+ */
+ String defaultValue() default "";
}
diff --git a/docs/hop-user-manual/modules/ROOT/pages/hop-gui/perspective-configuration.adoc b/docs/hop-user-manual/modules/ROOT/pages/hop-gui/perspective-configuration.adoc
index 85d04e14a29..0a0ff4323a6 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/hop-gui/perspective-configuration.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/hop-gui/perspective-configuration.adoc
@@ -17,6 +17,9 @@ under the License.
:imagesdir: ../assets/images
= Configuration Perspective
+// One level deeper than the default, so the contents list each plugin under
+// Plugin options reference rather than stopping at the heading above them.
+:page-toclevels: 3
Icon: image:icons/gear.svg[width="24px"]
@@ -28,125 +31,275 @@ The configuration perspective offers a central location for all Apache Hop confi
== Usage
-The configuration perspective groups configuration options in the following tabs:
+The perspective lists its sections in a tree on the left. Selecting one shows its options on the right; the search box above the tree finds an option by name across all of them.
-* **General** options control the behavior of Apache Hop and Hop Gui.
-* **Look & Feel** control how Hop Gui looks like on your desktop or in your browser.
-* **Plugin** options provide configuration options to control the behavior of your available Apache Hop plugins (including Explorer, xref:hop-gui/perspective-search.adoc[Search] limits, and xref:hop-gui/perspective-ai-advisor.adoc[AI Assistant] enablement).
-* **System variables** allows you to set and configure Apache Hop global variables.
+* **General**: how Apache Hop and Hop GUI behave - confirmations, tooltips and dialog positions.
+* **Look & Feel**: how Hop GUI is drawn on your desktop or in your browser.
+* **Plugins**: one entry per plugin that contributes options, including xref:hop-gui/perspective-search.adoc[Search], the Explorer perspective and xref:hop-gui/perspective-ai-advisor.adoc[AI Assistant]. Every option is listed in <<_plugin_options_reference,Plugin options reference>> below.
+* **System variables**: set and inspect the Apache Hop global variables.
+* **Security**: how a session is authenticated, split over General, Basic, External and OAuth sub-tabs. Until authentication is turned on the session is unrestricted and anyone can change these settings, which the tab says at the top.
+* **Keyboard shortcuts**: the shortcuts currently in effect, with the action each one triggers.
-== Detailed configuration tabs overview
+== Detailed options overview
=== General
-The General configuration tab allows you to configure the core Apache Hop options.
+The General tab holds the options that control how Hop GUI behaves rather than how it looks. The tables below follow the sections of the tab.
[%header, width=90%, cols="2,5,1"]
|===
|Option|Description|Default
+
|Hop configuration filename
-a|the location where Apache Hop will store the configuration file. This configuration file will contain your project and environment information and global variables.
+a|Where Apache Hop stores `hop-config.json`, the file holding your projects, environments and global variables. Read-only here.
-TIP: set the environment variable `HOP_CONFIG_FOLDER` to set this location dynamically to allow easy switching between different Apache Hop versions and installations. xref:protips/index.adoc[read more]
+TIP: set the environment variable `HOP_CONFIG_FOLDER` to change this location, which makes switching between Hop versions and installations easy. xref:protips/index.adoc[read more]
|`/config/hop-config.json`
|Preview data batch size
-a|number of rows that will be shown when previewing through the Pipeline run toolbar or from transform preview dialogs.
+a|How many rows are fetched when previewing from the pipeline run toolbar or a transform preview dialog.
-INFO: this setting complements the preview option that is used in the xref:pipeline/pipeline-run-configurations/native-local-pipeline-engine.adoc[native local pipeline run configuration], which can be accessed through the image:transforms/icons/datagrid.svg[data grid, width="16px"] `preview data` icon in the bottom right corner of transforms after a successful run.
+INFO: this complements the preview setting of the xref:pipeline/pipeline-run-configurations/native-local-pipeline-engine.adoc[native local pipeline run configuration], reachable through the image:transforms/icons/datagrid.svg[data grid, width="16px"] `preview data` icon in the bottom right corner of a transform after a run.
|1000
-|Use database cache|enable caching for relational databases|yes
+|Use database cache|Cache relational database metadata so repeated lookups of tables, fields and types do not go back to the server.|yes
-|Open last file at startup|reopen the last used files and select the last used file when Hop GUI starts|yes
+|Reopen tabs on startup|Reopen the files that were open when Hop GUI last closed, and select the one that was active.|yes
|Enable embedded terminal
-a|Show the embedded terminal in the bottom panel. Clear this on desktops where that terminal misbehaves. Search, the database workbench, and the VFS file explorer stay available. Hop Web has no embedded terminal. Excluding `40010-menu-tools-terminal` in xref:hop-gui/disable-ui-elements.adoc[disabledGuiElements.xml] forces it off.
+a|Show the embedded terminal in the bottom panel. Clear this on desktops where that terminal misbehaves; search, the database workbench and the VFS file explorer stay available either way. Hop Web has no embedded terminal. Excluding `40010-menu-tools-terminal` in xref:hop-gui/disable-ui-elements.adoc[disabledGuiElements.xml] forces it off.
|yes
-|Autosave changed files|Automatically save changed workflow and pipelines files|yes
+|Reload file if changed on filesystem|Reload a pipeline or workflow when it changes outside Hop GUI, for example after a `git pull` or an edit in another editor.|no
-|Automatically split hops
-a|Automatically split hops when a new transform or action is dropped on a hop.
+|Sort field names alphabetically in dropdowns|Sort field names in dropdown lists alphabetically. When off, fields keep the order they arrive in from the incoming stream.|yes
-TIP: Hops will be shown in bold when a transform or action can be dropped on it
-|yes
+|Use global bookmarks in the file dialog|Share the file dialog's bookmarks across every project instead of keeping a separate set per project.|yes
+
+|Maximum execution logging text size|The largest amount of log text held for the execution information perspective, in characters. Caps memory use on long-running or very chatty executions.|2000000
+|===
-|Show Copy or Distribute Dialog|Show a pop-up dialog to ask the user to confirm whether data should be copied or distributed (round-robin) to subsequent transforms in a pipeline|yes
+==== Confirmation dialogs
-|Ask user when exiting|Show a pop-up dialog to let the user confirm before exiting Hop GUI|yes
+Each option here controls one confirmation pop-up. Turning one off does not change what the action does, only whether Hop asks first. Every one of these dialogs can also be switched off from the dialog itself, with its *Do not ask this again* option.
+
+[%header, width=90%, cols="2,5,1"]
+|===
+|Option|Description|Default
-|Clear customer parameters (transforms/plugins|Deletes all custom flags and parameters in the dialogs of the transforms and plugins|on click
+|Show confirmation when exiting Hop|Ask for confirmation before closing Hop GUI.|yes
-|Auto collapse palette tree|Collapse or expand the categories in the context dialog|no (expanded)
+|Show copy or distribute dialog when drawing a hop|Ask whether rows should be copied to, or distributed round-robin over, the following transforms when a second hop is drawn out of a transform.|yes
-|Display tooltips|Show tooltips where available in Hop GUI|yes
+|Show confirmation when splitting hops|Ask before splitting a hop when a transform or action is dropped onto it. With this off, the hop is split without asking. Hops that a dropped item would split are drawn in bold.|yes
-|Show help tooltips|Show help tooltips where available in Hop GUI|yes
+|Show confirmation to save file when starting pipeline or workflow|Ask whether to save a changed file before running it. With this off, the file is saved automatically on every run.|yes
+
+|Reset all to enabled (Includes Action/Transform warnings)|Button. Switches every confirmation dialog back on, including the per-transform and per-action warnings that were dismissed with *Do not ask this again*.|on click
+|===
+
+==== Tooltips
+
+[%header, width=90%, cols="2,5,1"]
+|===
+|Option|Description|Default
-|Use double click on canvas?
-|double-click instead of single click on e.g. transform and action dialogs.
+|Show tooltip when hovering over a hop or Action/Transform|The master switch for the canvas tooltips. With this off, none of them appear, whatever is ticked under *Tooltips* in the xref:hop-gui/perspective-configuration.adoc#_look_feel[Look & Feel] tab.|yes
+
+|Show resolved variables in tooltips|Show what a variable currently resolves to in the tooltip, rather than the `+'${VARIABLE}'+` expression as written.|yes
+
+|Reset all to enabled|Button. Switches every tooltip option back on.|on click
+|===
+
+==== Dialog positioning
+
+[%header, width=90%, cols="2,5,1"]
+|===
+|Option|Description|Default
+
+|Remember dialog positions|Reopen each dialog at the position and size it was last given. With this off, every dialog opens at its default position and size.|yes
+
+|Reset dialog positions after restart|Keep remembered positions for the current session only, clearing `shells-state.json` on restart. With this off, positions are also written to that file and restored the next time Hop GUI starts. Only has an effect while *Remember dialog positions* is on.|no
+
+|Clear dialog positions|Button. Forgets every remembered dialog position and resets the perspective tree-panel widths to their defaults. Useful after moving between monitors of different sizes, when a dialog reopens off-screen.|on click
+|===
+
+=== Look & Feel
-enabled: double-click on a transform or action icon to open its properties. A single click on the icon opens the context dialog.
-disabled (default): (single) click on a transform or action icon to open the context dialog. (single) click on a transform or action name to open its properties.
+The Look & Feel tab holds everything about how Hop GUI is drawn: fonts, colours, the canvas, and which columns and toolbars are shown. The tables below follow the sections of the tab.
+
+Some of these settings only take effect after Hop GUI is restarted; the dialog says so where that is the case.
+
+==== General appearance
+
+[%header, width=90%, cols="2,5,1"]
+|===
+|Option|Description|Default
+
+|Preferred language
+a|The language Hop GUI is shown in.
+
+TIP: Check the https://hop.apache.org/community/contribution-guides/translation-contribution-guide/[Translation Contribution Guide] to translate Apache Hop into your own language.
+|English (US)
+
+|Hide the menu bar|Hide the menu bar. With it hidden, the menu is reachable from the Apache Hop icon in the upper left corner of Hop GUI.|yes
+
+|Dark mode|Use the dark theme. On Hop Web and Windows this is a straight toggle; on macOS and other desktop platforms the theme follows the operating system. Changing it in Hop Web reloads the page.|off on Windows; follows the OS on macOS and other desktops
+
+|Follow system dark mode (Hop Web)|Take the light or dark setting from the system or browser. While this is on, the *Dark mode* option above is disabled.|yes
+
+|UI zoom level|The zoom level pipelines and workflows open at.|100%
+
+|Icon size in workspace|The size transform and action icons are drawn at on the canvas, in pixels.|32
+
+|Line width on workspace|The width hops are drawn with on the canvas, in pixels.|1
+
+|Dialog middle percentage|Where the divider between labels and input fields sits in dialogs, as a percentage of the dialog width. Raise it where a translation makes labels too wide for their column.|35
+|===
+
+==== macOS
+
+This section only appears on macOS.
+
+[%header, width=90%, cols="2,5,1"]
+|===
+|Option|Description|Default
+
+|Allow dialogs to open on any screen|Off: transform, action and metadata dialogs follow the Hop window and stay above it. On: they open as windows of their own, which can be moved to another screen.|no
+|===
+
+==== Fonts
+
+[%header, width=90%, cols="2,5,1"]
+|===
+|Option|Description|Default
+
+|Fixed width font|The fixed-width font for SQL, scripts, logs and similar editors.|platform default (Consolas / Monaco / Monospace)
+
+|Font on workspace|The font for transform and action names on the canvas. Notes that do not set a font of their own use it too.|system font
+|===
+
+==== Pipeline and Workflow canvas
+
+[%header, width=90%, cols="2,5,1"]
+|===
+|Option|Description|Default
+
+|Show canvas grid|Draw the dotted alignment grid behind pipelines and workflows.|no
+
+|Canvas grid size|The spacing of that grid, in pixels. Transforms and actions snap to it.|16
+
+|Show viewport|Show the viewport, the small overview of the whole pipeline or workflow in the bottom right corner of the canvas.|yes
+
+|Use double click on canvas
+a|Enabled: double-click a transform or action icon to open its properties; a single click opens the context dialog.
+
+Disabled (the default): a single click on the icon opens the context dialog, and a click on the name opens the properties.
See xref:hop-gui/canvas-mouse.adoc[Canvas mouse gestures] for every click in both modes.
|no
|Use right click for the context dialog
-|A right click (Ctrl-click or two-finger click on macOS) opens the context dialog of whatever is under the pointer, and a left click never does: it selects, clears the selection, or presses a badge. Names and badges keep opening their editors on a left click.
+a|A right click (Ctrl-click or a two-finger click on macOS) opens the context dialog of whatever is under the pointer, and a left click never does: it selects, clears the selection, or presses a badge. Names and badges still open their editors on a left click.
See xref:hop-gui/canvas-mouse.adoc[Canvas mouse gestures] for every click in both modes.
|no
|Use menus instead of the context dialog
-|Transforms, actions, hops and notes show their actions in a pop-up menu with a submenu per category instead of the xref:hop-gui/hop-gui-popup-dialog.adoc[context dialog]. A click on the empty canvas keeps the context dialog while the xref:hop-gui/hop-gui-popup-dialog.adoc#palette-tree[palette tree] is hidden, since that is where new transforms and actions are searched for; with the palette shown the empty canvas gets a menu of the pipeline/workflow actions too. The click that opens the menu is the same one that opens the context dialog (left click, or right click with the option above).
+a|Transforms, actions, hops and notes show their actions in a pop-up menu with a submenu per category rather than in the xref:hop-gui/hop-gui-popup-dialog.adoc[context dialog]. A click on the empty canvas keeps the context dialog while the xref:hop-gui/hop-gui-popup-dialog.adoc#palette-tree[palette tree] is hidden, since that is where new transforms and actions are searched for; with the palette shown, the empty canvas gets a menu of the pipeline and workflow actions instead. The click that opens the menu is the same one that opens the context dialog.
|no
-|Draw border around names on canvas?
-|If this option is enabled, a border will be drawn around the names of transforms and actions on the canvas.
-|no
+|Draw border around Action/Transform names on canvas|Draw a border around the names of transforms and actions, which helps where names overlap the icons behind them.|no
+
+|Enable infinite move|Pan the canvas in every direction without being stopped at the origin, so a diagram can be built out to the left of or above where it started.|no
+
+|Disable zoom scrolling|Stop the mouse scroll wheel zooming the canvas in and out. Useful on trackpads where scrolling and zooming are easy to confuse.|no
+
+|Metrics above selected running transforms?|While a pipeline runs, draw its metrics above the transforms that are selected.|yes
|Tooltips
-a|A group of its own in the *Pipeline and Workflow canvas* section: one checkbox per kind of tooltip the canvas shows when the mouse rests on something. Untick a kind to stop only that one; *Show tooltip when hovering over a hop or Action/Transform* in the *Tooltips* section of the General tab still switches all of them off at once.
+a|One checkbox per kind of tooltip the canvas shows when the mouse rests on something. Untick one to stop only that kind. *Show tooltip when hovering over a hop or Action/Transform*, in the *Tooltips* section of the General tab, switches all of them off at once.
* *Edit hint on transform and action names*: the "Click to edit" hint on a name.
* *Transform and action descriptions*: the description on an icon and its info badge, along with the transform partitioning and whether a transform can start a pipeline.
* *Deprecation warnings*: the warning on the icon of a deprecated transform or action, and its replacement.
-* *Hop details*: the source, target and status of a hop when the mouse rests on its line, and the badges on it: info, error, copies, row distribution, target streams and the conditions of workflow hops.
-* *Execution results*: after a run, the failure logs, the output rows a transform or hop holds, the result of an action and where a workflow restarted from a checkpoint.
+* *Hop details*: the source, target and status of a hop, and the badges on it: info, error, copies, row distribution, target streams and the conditions of workflow hops.
+* *Execution results*: after a run, the failure logs, the rows a transform or hop holds, the result of an action and where a workflow restarted from a checkpoint.
* *Note links*: where a hyperlink in a note leads.
* *Plugin tooltips*: tooltips that plugins add to the canvas, such as the custom debug levels.
* *Selection cleared notice*: the short "Selection cleared" notice after a click on the empty canvas while something was selected.
|all on
+|===
+
+==== Auto-layout
-|Use global bookmarks in the file dialog|Bookmarks in the file dialog are global (across projects) by default. Enable to make them project-specific|yes
+These settings control the *Arrange* action, which lays a pipeline or workflow out automatically.
+[%header, width=90%, cols="2,5,1"]
|===
+|Option|Description|Default
-=== Look & Feel
+|Layout direction|The direction the graph flows from start to end: left to right, right to left, top to bottom or bottom to top.|Left to right
+
+|Layer spacing|The distance between consecutive layers, along the flow direction, in pixels.|150
+
+|Node spacing|The distance between nodes within one layer, across the flow direction, in pixels.|120
+
+|Crossing-reduction iterations|How many barycenter sweeps are used to pull crossing hops apart. Higher is tidier but slower on large diagrams.|4
+
+|Move notes with nearest node|Move each note along with the transform or action it sits closest to. Notes that are not near any of them stay where they are.|yes
+|===
-The `Look & Feel` configuration options allow you to configure various aspects of Hop GUI's behavior.
+==== Tables and grids
[%header, width=90%, cols="2,5,1"]
|===
|Option|Description|Default
-|Fixed width font|the fixed-width font for SQL, scripts, logs, and similar editors|platform default (Consolas / Monaco / Monospace)
-|Font on workspace|the font for transform and action names on the canvas. Notes that do not set their own font also use this.|system font
-|Icon size in workspace|the default size for action and transform icons in workflows and pipelines|32
-|Line width on workspace|line width to use for hops in workflows and actions|1
-|Dialog middle percentage|percentage to use to determine a dialog's position|35
-|UI zoom level|default zoom level to use for workflows and pipelines in Hop GUI|100%
-|Canvas Grid Size|default grid size to use for workflows and pipelines in the Hop GUI editor |16
-|Show Canvas Grid|show the grid (dotted) for workflows and pipelines in Hop GUI |no
-|Hide the menu bar|Do not show the menu bar. If enabled (default), the menu options are available from the Apache Hop icon in Hop GUI's upper left corner. |yes
-|Show a toolbar above tables|Show a toolbar with options to cut/copy/paste, move lines up/down, navigate to a column, manage xref:pipeline/run-preview-debug-pipeline.adoc#table-views[table views] etc in table grids (e.g. in preview dialogs, transform configuration options)|yes
-|Allow dialogs to open on any screen (macOS only)|Off: transform, action and metadata dialogs follow the Hop window and stay above it. On: they open as windows of their own that you can move to another screen. The macOS section only appears on macOS.|no
-|Dark mode|use dark mode. On Hop Web and Windows you can toggle this; on macOS and other desktop platforms it follows the operating system theme. Changing it in Hop Web reloads the page to apply the theme. |N/A
-|Preferred Language
-a|the default language to use in Hop GUI.
-
-TIP: Check the https://hop.apache.org/community/contribution-guides/translation-contribution-guide/[Translation Contribution Guide] to translate Apache Hop to your native language.|English (US)
+
+|Show toolbar|Show a toolbar above table grids with cut/copy/paste, move row up/down, go to column and xref:pipeline/run-preview-debug-pipeline.adoc#table-views[table views]. Applies to preview dialogs, transform settings and similar grids.|yes
+
+|Show text editor toolbar|Show a toolbar above multi-line text editors (SQL, scripts and similar fields) with cut/copy/paste, undo/redo and find/replace.|yes
+
+|Maximum data grid cell length
+a|The most characters drawn in one cell of a data grid: the row preview, the data grid transform and the rows viewers. Longer values are only shortened on screen; click a cell to see the whole value in place, or use the expand icon to open it in the multi-line editor. Copying, exporting and saving always use the full value. Set it to 0 to stop shortening.
+
+Configuration grids are never shortened, and this has no effect in Hop Web.
+|50
+
+|Show line breaks as symbols in data grid cells
+a|Draw line breaks and tabs inside a data grid cell as symbols on a single line. With this off, a value containing a line break is cut at the first break and marked with an ellipsis. Either way this only changes how the cell is drawn: clicking it shows the whole value, and copying or exporting keeps every line break.
+|no
+|===
+
+==== Metrics panel
+
+Which columns the pipeline metrics grid shows while a pipeline runs. Hiding a column does not stop the metric being collected.
+
+[%header, width=90%, cols="2,5,1"]
+|===
+|Option|Description|Default
+
+|Show units in cells|Show the unit alongside each value in the metrics grid (`r`, `h:m:s`, `rows/s`). With this off, only the numbers are shown.|no
+
+|Dynamic column resizing|Widen metrics columns during a run as the values in them get wider. Turn this off to keep the widths you set by hand; *View > Fit columns to content* still packs them once, on demand.|yes
+
+|Show Input|Show the *Input* column: rows read from an external source, such as a file or a database.|yes
+
+|Show Read|Show the *Read* column: rows read from incoming hops.|yes
+
+|Show Output|Show the *Output* column: rows written to an external target.|yes
+
+|Show Updated|Show the *Updated* column: rows updated in an external target.|yes
+
+|Show Rejected|Show the *Rejected* column: rows sent down an error handling hop.|yes
+
+|Show Buffers Input|Show the *Buffers Input* column: how full each transform's input row buffer is, which is where a bottleneck shows up first.|yes
+
+|Show Data volume|Show the *Data volume* column, the row-based byte count. Only collected when the `HOP_METRIC_DATA_VOLUME` variable is on, and disabled here when it is set to `N`.|yes
+
+|Show Data volume in|Show the *Data volume in* column, bytes read from the input stream. Needs `HOP_METRIC_DATA_VOLUME` as above.|yes
+
+|Show Data volume out|Show the *Data volume out* column, bytes written to the output stream. Needs `HOP_METRIC_DATA_VOLUME` as above.|yes
|===
=== Plugins
@@ -176,6 +329,14 @@ The warning lists the missing connections. *Yes* saves anyway, *No* cancels the
+
image::hop-gui/validate-database-connections-on-save-warning-dialog.png[Database connections save warning,width="90%"]
+=== Plugin options reference
+
+Every option each plugin adds to the *Plugins* section, with what it does and what it falls back to when it has never been set. Generated from the plugins themselves, so it covers the plugins shipped with Apache Hop; a plugin you install yourself adds its own section to the perspective that is not listed here.
+
+Anything set here can also be set with `hop-conf`, and ends up in `hop-config.json`.
+
+include::partial$config/all-plugins.adoc[leveloffset=+2]
+
=== System Variables
The System Variables tab contains the available system variables in Hop GUI.
diff --git a/docs/hop-user-manual/modules/ROOT/pages/vfs/azure-blob-storage-vfs.adoc b/docs/hop-user-manual/modules/ROOT/pages/vfs/azure-blob-storage-vfs.adoc
index 8b5519837e1..773155d44ef 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/vfs/azure-blob-storage-vfs.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/vfs/azure-blob-storage-vfs.adoc
@@ -68,6 +68,12 @@ Once done you will see an `azure` entry in the central `hop-config.json` file:
}
----
+== Options
+
+The options below are the ones in the *Plugins* section of the xref:hop-gui/perspective-configuration.adoc[configuration perspective]. Each is also settable with `hop-conf`.
+
+include::partial$config/AzureConfigPlugin.adoc[]
+
== Authentication types
An Azure connection in the metadata perspective can authenticate in three ways, selected with the
diff --git a/docs/hop-user-manual/modules/ROOT/pages/vfs/dropbox-vfs.adoc b/docs/hop-user-manual/modules/ROOT/pages/vfs/dropbox-vfs.adoc
index bd9544846ec..3e62a4254b6 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/vfs/dropbox-vfs.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/vfs/dropbox-vfs.adoc
@@ -56,6 +56,12 @@ Dropbox displays the App Settings panel for the app that you created
Once done you will see a `dropbox` entry in the central `hop-config.json` file:
+== Options
+
+The options below are the ones in the *Plugins* section of the xref:hop-gui/perspective-configuration.adoc[configuration perspective]. Each is also settable with `hop-conf`.
+
+include::partial$config/DropboxConfigPlugin.adoc[]
+
== Usage and testing
To test if the configuration works you can simply put a small CSV file in Dropbox and then use File/Open in Hop GUI.
diff --git a/docs/hop-user-manual/modules/ROOT/pages/vfs/google-cloud-storage-vfs.adoc b/docs/hop-user-manual/modules/ROOT/pages/vfs/google-cloud-storage-vfs.adoc
index 8e91533923b..ea6d59b32cf 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/vfs/google-cloud-storage-vfs.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/vfs/google-cloud-storage-vfs.adoc
@@ -51,6 +51,12 @@ Once done you will see a `googleCloud` entry in the central `hop-config.json` fi
}
----
+== Options
+
+The options below are the ones in the *Plugins* section of the xref:hop-gui/perspective-configuration.adoc[configuration perspective]. Each is also settable with `hop-conf`.
+
+include::partial$config/GoogleCloudStorageConfigPlugin.adoc[]
+
== Usage and testing
To test if the configuration works type use File/Open in the GUI and type in gs:// as a file location.
diff --git a/docs/hop-user-manual/modules/ROOT/pages/vfs/google-drive-vfs.adoc b/docs/hop-user-manual/modules/ROOT/pages/vfs/google-drive-vfs.adoc
index 6a778bed0e4..bdaab12d7d1 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/vfs/google-drive-vfs.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/vfs/google-drive-vfs.adoc
@@ -68,6 +68,12 @@ Please open the following address in your browser:
Open that URL in a browser and authenticate the given client ID.
You'll then get a token in the configured `tokens folder` with which you can work.
+== Options
+
+The options below are the ones in the *Plugins* section of the xref:hop-gui/perspective-configuration.adoc[configuration perspective]. Each is also settable with `hop-conf`.
+
+include::partial$config/GoogleDriveConfigPlugin.adoc[]
+
== Usage and testing
To test if the configuration works you can simply put a small CSV file in Google Drive and then use File/Open in Hop GUI.
diff --git a/docs/hop-user-manual/modules/ROOT/partials/config/AzureConfigPlugin.adoc b/docs/hop-user-manual/modules/ROOT/partials/config/AzureConfigPlugin.adoc
new file mode 100644
index 00000000000..f90b4323f05
--- /dev/null
+++ b/docs/hop-user-manual/modules/ROOT/partials/config/AzureConfigPlugin.adoc
@@ -0,0 +1,38 @@
+////
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+////
+// Generated from the @ConfigPlugin and @GuiWidgetElement annotations.
+// Edit those, not this file.
+
+These options are stored under `pass:c[azure]` in `hop-config.json`.
+
+[%header, cols="2,5,1"]
+|===
+|Option|Description|Default
+
+|pass:c[Account]
+|pass:c[The account to use for the Azure VFS. Accepts variables.]
+|
+
+|pass:c[Account key]
+|pass:c[The key to use for the Azure VFS. Accepts variables.]
+|
+
+|pass:c[Alternative Local Storage]
+|pass:c[URL of Azure emulator/local storage. Accepts variables.]
+|
+
+|===
diff --git a/docs/hop-user-manual/modules/ROOT/partials/config/CalciteSqlFormatConfigPlugin.adoc b/docs/hop-user-manual/modules/ROOT/partials/config/CalciteSqlFormatConfigPlugin.adoc
new file mode 100644
index 00000000000..fc5d51e2eff
--- /dev/null
+++ b/docs/hop-user-manual/modules/ROOT/partials/config/CalciteSqlFormatConfigPlugin.adoc
@@ -0,0 +1,90 @@
+////
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+////
+// Generated from the @ConfigPlugin and @GuiWidgetElement annotations.
+// Edit those, not this file.
+
+These options are stored under `pass:c[calciteSqlFormat]` in `hop-config.json`.
+
+[%header, cols="2,5,1"]
+|===
+|Option|Description|Default
+
+|pass:c[Indentation (spaces)]
+|pass:c[Number of spaces to indent nested SQL. Calcite default is 4; Hop default is 2.]
+|`pass:c[2]`
+
+|pass:c[Line length]
+|pass:c[Preferred maximum line length. 0 means no maximum.]
+|`pass:c[0]`
+
+|pass:c[Clause starts a new line]
+|pass:c[Start FROM, WHERE, GROUP BY, HAVING, WINDOW and ORDER BY on a new line.]
+|`pass:c[true]`
+
+|pass:c[Clause is followed by a new line]
+|pass:c[Clause (FROM, WHERE, GROUP BY, HAVING, WINDOW, ORDER BY) is followed by a new line]
+|`pass:c[false]`
+
+|pass:c[Always use parentheses]
+|pass:c[Wrap every expression in parentheses even when operator precedence does not require it.]
+|`pass:c[false]`
+
+|pass:c[Leading comma]
+|pass:c[Commas in SELECT, GROUP BY and ORDER clauses should appear at the start of the line.]
+|`pass:c[false]`
+
+|pass:c[SELECT items on separate lines]
+|pass:c[Put each item in the SELECT list on its own line.]
+|`pass:c[true]`
+
+|pass:c[FROM items on separate lines]
+|pass:c[Put each item in the FROM list on its own line.]
+|`pass:c[true]`
+
+|pass:c[WHERE items on separate lines]
+|pass:c[Put each item in the WHERE list on its own line.]
+|`pass:c[true]`
+
+|pass:c[GROUP BY items on separate lines]
+|pass:c[Put each item in a GROUP BY list on its own line.]
+|`pass:c[true]`
+
+|pass:c[ORDER BY items on separate lines]
+|pass:c[Put each item in an ORDER BY list on its own line.]
+|`pass:c[true]`
+
+|pass:c[CASE clauses on new lines]
+|pass:c[Put WHEN, THEN and ELSE of a CASE expression at the start of a new line.]
+|`pass:c[true]`
+
+|pass:c[WINDOW declaration starts a new line]
+|pass:c[Start a WINDOW declaration on a new line.]
+|`pass:c[true]`
+
+|pass:c[WINDOW items on separate lines]
+|pass:c[Put each item in a WINDOW list on its own line.]
+|`pass:c[true]`
+
+|pass:c[Keywords in lower case]
+|pass:c[Print SQL keywords such as SELECT and FROM in lower case.]
+|`pass:c[false]`
+
+|pass:c[Quote all identifiers]
+|pass:c[Quote every identifier even when the dialect would accept it unquoted.]
+|`pass:c[false]`
+
+|===
diff --git a/docs/hop-user-manual/modules/ROOT/partials/config/DatabasePerspectiveConfigPlugin.adoc b/docs/hop-user-manual/modules/ROOT/partials/config/DatabasePerspectiveConfigPlugin.adoc
new file mode 100644
index 00000000000..b26340d1279
--- /dev/null
+++ b/docs/hop-user-manual/modules/ROOT/partials/config/DatabasePerspectiveConfigPlugin.adoc
@@ -0,0 +1,38 @@
+////
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+////
+// Generated from the @ConfigPlugin and @GuiWidgetElement annotations.
+// Edit those, not this file.
+
+These options are stored under `pass:c[database-perspective]` in `hop-config.json`.
+
+[%header, cols="2,5,1"]
+|===
+|Option|Description|Default
+
+|pass:c[Automatically connect when executing SQL]
+|pass:c[Connect the SQL editors database without asking if it is not already connected. You can also enable this from the dialog that appears when the option is off.]
+|`pass:c[false]`
+
+|pass:c[Select the executed SQL in the editor]
+|pass:c[After Run or Run all, highlight only the statement(s) that were sent to the database. A leftover clause after a semicolon is left unselected.]
+|`pass:c[true]`
+
+|pass:c[Query row limit]
+|pass:c[Maximum number of rows shown for a SELECT in the SQL editor and for table preview. A SQL LIMIT above this value is still capped here so a SELECT * does not pull the whole table.]
+|`pass:c[1000]`
+
+|===
diff --git a/docs/hop-user-manual/modules/ROOT/partials/config/DropboxConfigPlugin.adoc b/docs/hop-user-manual/modules/ROOT/partials/config/DropboxConfigPlugin.adoc
new file mode 100644
index 00000000000..a5811da2d6c
--- /dev/null
+++ b/docs/hop-user-manual/modules/ROOT/partials/config/DropboxConfigPlugin.adoc
@@ -0,0 +1,30 @@
+////
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+////
+// Generated from the @ConfigPlugin and @GuiWidgetElement annotations.
+// Edit those, not this file.
+
+These options are stored under `pass:c[dropbox]` in `hop-config.json`.
+
+[%header, cols="2,5,1"]
+|===
+|Option|Description|Default
+
+|pass:c[Access token]
+|pass:c[The Dropbox access token used to authenticate. Generate one for an app in the Dropbox App Console; it grants whatever access that app was given. Accepts variables.]
+|
+
+|===
diff --git a/docs/hop-user-manual/modules/ROOT/partials/config/ExplorerPerspectiveConfigPlugin.adoc b/docs/hop-user-manual/modules/ROOT/partials/config/ExplorerPerspectiveConfigPlugin.adoc
new file mode 100644
index 00000000000..cd811b90709
--- /dev/null
+++ b/docs/hop-user-manual/modules/ROOT/partials/config/ExplorerPerspectiveConfigPlugin.adoc
@@ -0,0 +1,50 @@
+////
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+////
+// Generated from the @ConfigPlugin and @GuiWidgetElement annotations.
+// Edit those, not this file.
+
+These options are stored under `pass:c[explorer-perspective]` in `hop-config.json`.
+
+[%header, cols="2,5,1"]
+|===
+|Option|Description|Default
+
+|pass:c[The initial depth to load not lazily]
+|pass:c[The initial depth to load not lazily. Accepts variables.]
+|`pass:c[0]`
+
+|pass:c[The maximum file size to load]
+|pass:c[The maximum file size to load. Accepts variables.]
+|`pass:c[16]`
+
+|pass:c[Show file explorer panel by default]
+|pass:c[When enabled, the file explorer panel (project tree) is shown by default when opening the explorer perspective]
+|`pass:c[true]`
+
+|pass:c[Open help pages in]
+|pass:c[Choose where documentation opens: the system browser, a tab in the File Explorer perspective, or a dialog on top of the current window.]
+|`pass:c[External browser]`
+
+|pass:c[Select active file in tree automatically]
+|pass:c[Automatically select the active tab file in the file explorer tree on the left hand side when it is shown in a tab.]
+|`pass:c[true]`
+
+|pass:c[Maximum undo operations]
+|pass:c[The maximum number of undo/redo snapshots kept for pipelines, workflows and table editors. Default is 100. Accepts variables.]
+|`pass:c[100]`
+
+|===
diff --git a/docs/hop-user-manual/modules/ROOT/partials/config/GitConfigOptionPlugin.adoc b/docs/hop-user-manual/modules/ROOT/partials/config/GitConfigOptionPlugin.adoc
new file mode 100644
index 00000000000..cec99c7fb97
--- /dev/null
+++ b/docs/hop-user-manual/modules/ROOT/partials/config/GitConfigOptionPlugin.adoc
@@ -0,0 +1,42 @@
+////
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+////
+// Generated from the @ConfigPlugin and @GuiWidgetElement annotations.
+// Edit those, not this file.
+
+These options are stored under `pass:c[gitConfig]` in `hop-config.json`.
+
+[%header, cols="2,5,1"]
+|===
+|Option|Description|Default
+
+|pass:c[Enable the git GUI plugin]
+|pass:c[Switches the git plugin on or off. With it off the git perspective and the git decorations in the file explorer are hidden, and no repository is opened.]
+|`pass:c[true]`
+
+|pass:c[Search parent folders for git repository]
+|pass:c[Look in parent folders for the git repository rather than only in the project folder. Needed where the project sits in a subfolder of a larger repository.]
+|`pass:c[false]`
+
+|pass:c[Fetch remote changes automatically]
+|pass:c[Enables background fetching of remote updates every 20 minutes.]
+|`pass:c[false]`
+
+|pass:c[Ignore position in the visual diff]
+|pass:c[Do not mark a transform or action as changed when it was only moved.]
+|`pass:c[true]`
+
+|===
diff --git a/docs/hop-user-manual/modules/ROOT/partials/config/GoogleCloudStorageConfigPlugin.adoc b/docs/hop-user-manual/modules/ROOT/partials/config/GoogleCloudStorageConfigPlugin.adoc
new file mode 100644
index 00000000000..1f9706758d4
--- /dev/null
+++ b/docs/hop-user-manual/modules/ROOT/partials/config/GoogleCloudStorageConfigPlugin.adoc
@@ -0,0 +1,82 @@
+////
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+////
+// Generated from the @ConfigPlugin and @GuiWidgetElement annotations.
+// Edit those, not this file.
+
+These options are stored under `pass:c[googleCloud]` in `hop-config.json`.
+
+[%header, cols="2,5,1"]
+|===
+|Option|Description|Default
+
+|pass:c[Account key file]
+|pass:c[The path to a Google Cloud service account JSON key file. Accepts variables.]
+|
+
+|pass:c[Scan folders to find last modified data]
+|pass:c[Scans all files in a folder and uses the most recent modification data as the folder modification data. This may have a performance impact in buckets with lots of files.]
+|`pass:c[false]`
+
+|pass:c[Max number of attempts]
+|pass:c[Maximum number of attempts for a single request, including the first one. Accepts variables.]
+|`pass:c[6]`
+
+|pass:c[Initial retry delay (seconds)]
+|pass:c[Delay before the first retry, in seconds. Accepts variables.]
+|`pass:c[1]`
+
+|pass:c[Retry delay multiplier]
+|pass:c[Retry delay multiplier. Accepts variables.]
+|`pass:c[2.0]`
+
+|pass:c[Maximum retry delay (seconds)]
+|pass:c[Ceiling for the retry delay once the multiplier has been applied, in seconds. Accepts variables.]
+|`pass:c[32]`
+
+|pass:c[Total timeout (minutes)]
+|pass:c[Deadline for a request including all of its retries, in minutes. Accepts variables.]
+|`pass:c[50]`
+
+|pass:c[Initial RPC timeout (seconds)]
+|pass:c[Timeout for the first attempt, in seconds. Raised to the max RPC timeout if that is lower. Accepts variables.]
+|`pass:c[50]`
+
+|pass:c[RPC Timeout Multiplier]
+|pass:c[RPC Timeout Multiplier. Accepts variables.]
+|`pass:c[1.0]`
+
+|pass:c[Max RPC timeout (seconds)]
+|pass:c[Ceiling for a single attempt timeout, in seconds. Never lower than the initial RPC timeout. Accepts variables.]
+|`pass:c[50]`
+
+|pass:c[Connect timeout (seconds)]
+|pass:c[Socket connect timeout, in seconds. Accepts variables.]
+|`pass:c[20]`
+
+|pass:c[Read timeout (seconds)]
+|pass:c[Socket read timeout, in seconds. Accepts variables.]
+|`pass:c[20]`
+
+|pass:c[Retry non-idempotent operations]
+|pass:c[Also retry writes (create, delete, starting an upload). Google Cloud Storage only retries reads by default, so without this the retry settings above do not apply to writing files. Note that a retried delete can report "not found" when the first attempt already succeeded, and a retried create overwrites.]
+|`pass:c[false]`
+
+|pass:c[Cache TTL (seconds)]
+|pass:c[How long to cache folder listing results (in seconds, default 5). If not set, falls back to 10 seconds. Accepts variables.]
+|`pass:c[5]`
+
+|===
diff --git a/docs/hop-user-manual/modules/ROOT/partials/config/GoogleDriveConfigPlugin.adoc b/docs/hop-user-manual/modules/ROOT/partials/config/GoogleDriveConfigPlugin.adoc
new file mode 100644
index 00000000000..1b71dab8c7b
--- /dev/null
+++ b/docs/hop-user-manual/modules/ROOT/partials/config/GoogleDriveConfigPlugin.adoc
@@ -0,0 +1,34 @@
+////
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+////
+// Generated from the @ConfigPlugin and @GuiWidgetElement annotations.
+// Edit those, not this file.
+
+These options are stored under `pass:c[googleDrive]` in `hop-config.json`.
+
+[%header, cols="2,5,1"]
+|===
+|Option|Description|Default
+
+|pass:c[Path to a Google Drive credentials JSON file]
+|pass:c[Please note that this is a global configuration option and while you can specify variables that these will be not include project specific settings. Accepts variables.]
+|
+
+|pass:c[Path to a tokens folder for credential caching]
+|pass:c[Pick a safe folder to store these tokens. Accepts variables.]
+|
+
+|===
diff --git a/docs/hop-user-manual/modules/ROOT/partials/config/HopAiConfigOptionPlugin.adoc b/docs/hop-user-manual/modules/ROOT/partials/config/HopAiConfigOptionPlugin.adoc
new file mode 100644
index 00000000000..6a329943a44
--- /dev/null
+++ b/docs/hop-user-manual/modules/ROOT/partials/config/HopAiConfigOptionPlugin.adoc
@@ -0,0 +1,56 @@
+////
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+////
+// Generated from the @ConfigPlugin and @GuiWidgetElement annotations.
+// Edit those, not this file.
+
+These options are stored under `pass:c[hopAiConfig]` in `hop-config.json`.
+
+[%header, cols="2,5,1"]
+|===
+|Option|Description|Default
+
+|pass:c[Enable AI advisory]
+|pass:c[When disabled, AI Help is hidden and no prompts are sent. Default is off. Before applying AI changes, work on a Git feature branch so you can revert the project if needed.]
+|`pass:c[false]`
+
+|pass:c[Default AI provider]
+|pass:c[Named AI Provider metadata used by GUI advisors. Pipelines can still pick a different provider. Accepts variables.]
+|
+
+|pass:c[Allow sending full XML]
+|pass:c[Second gate for the session "Include full XML" checkbox. Off by default. Full XML is large and can contain SQL, filenames and notes. Leave this off unless you need it, and only after secrets live in environment variables or a keystore.]
+|`pass:c[false]`
+
+|pass:c[Extra context]
+a|pass:c[Standing notes sent with every AI Assistant question. The default reminds the model that metadata names are case-sensitive (`local` is not `Local`), that pipeline and workflow run configurations are different types, and that Hop is not Kettle. Add project conventions here. Accepts variables.]
+
+.Default
+[listing]
+----
+Hop metadata names are case-sensitive.
+Pipeline run configurations and workflow run configurations are different metadata types.
+Do not guess names of existing metadata, transforms, or actions that are not in the prompt context. New objects the user asked to create should use the names they gave.
+Variables use ${NAME}. Prefer variables or a resolver for secrets.
+Hop uses pipelines (.hpl) and workflows (.hwf) with transforms and actions, not Kettle transformations, jobs, steps, or job entries.
+----
+|_(with the description)_
+
+|pass:c[Context files]
+|pass:c[One VFS path per line. Files that exist are appended to the system prompt; missing files are skipped. Default is ${PROJECT_HOME}/AGENTS.md. Variables are resolved. Accepts variables.]
+|`pass:c[${PROJECT_HOME}/AGENTS.md]`
+
+|===
diff --git a/docs/hop-user-manual/modules/ROOT/partials/config/ProjectsConfigOptionPlugin.adoc b/docs/hop-user-manual/modules/ROOT/partials/config/ProjectsConfigOptionPlugin.adoc
new file mode 100644
index 00000000000..f2d29cde145
--- /dev/null
+++ b/docs/hop-user-manual/modules/ROOT/partials/config/ProjectsConfigOptionPlugin.adoc
@@ -0,0 +1,66 @@
+////
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+////
+// Generated from the @ConfigPlugin and @GuiWidgetElement annotations.
+// Edit those, not this file.
+
+These options are stored under `pass:c[projectsConfig]` in `hop-config.json`.
+
+[%header, cols="2,5,1"]
+|===
+|Option|Description|Default
+
+|pass:c[Enable the projects plugin]
+|pass:c[Switches the projects plugin on or off. With it off, Hop GUI drops the project and environment selectors and files are addressed by path alone.]
+|`pass:c[true]`
+
+|pass:c[Use of a project is mandatory]
+|pass:c[Refuse to run a pipeline or workflow unless a project is selected. Catches runs against the wrong configuration on a shared installation.]
+|`pass:c[false]`
+
+|pass:c[Use of an environment is mandatory]
+|pass:c[Refuse to run a pipeline or workflow unless a lifecycle environment is selected, so every run has explicit environment variables behind it.]
+|`pass:c[false]`
+
+|pass:c[The default project to use when none is specified]
+|pass:c[The default project to use when none is specified. Accepts variables.]
+|
+
+|pass:c[The default environment to use when none is specified]
+|pass:c[The name of the default environment to use when none is specified. Accepts variables.]
+|
+
+|pass:c[The parent project to propose when creating projects]
+|pass:c[The project proposed as the parent when a new project is created. A child project inherits its parent's variables and configuration. Accepts variables.]
+|
+
+|pass:c[The standard projects folder for new projects]
+|pass:c[The standard projects folder for new projects. Accepts variables.]
+|
+
+|pass:c[The project configuration filename for new projects]
+|pass:c[The name given to the configuration file of a newly created project, relative to the project folder. Accepts variables.]
+|`pass:c[project-config.json]`
+
+|pass:c[Clear DB cache when switching project/environment]
+|pass:c[Empty the database cache when switching project or environment. Keep this on where environments point at different databases that share connection names, so metadata cached for the previous one is not reused.]
+|`pass:c[true]`
+
+|pass:c[Sort the last used projects alphabetically]
+|pass:c[List the last used projects alphabetically rather than most recently used first.]
+|`pass:c[false]`
+
+|===
diff --git a/docs/hop-user-manual/modules/ROOT/partials/config/SearchConfigPlugin.adoc b/docs/hop-user-manual/modules/ROOT/partials/config/SearchConfigPlugin.adoc
new file mode 100644
index 00000000000..d7d7b532e17
--- /dev/null
+++ b/docs/hop-user-manual/modules/ROOT/partials/config/SearchConfigPlugin.adoc
@@ -0,0 +1,54 @@
+////
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+////
+// Generated from the @ConfigPlugin and @GuiWidgetElement annotations.
+// Edit those, not this file.
+
+These options are stored under `pass:c[search]` in `hop-config.json`.
+
+[%header, cols="2,5,1"]
+|===
+|Option|Description|Default
+
+|pass:c[Minimum characters for content search]
+|pass:c[Object names are always searched. File and field content is only searched when the query has at least this many characters (default 3). Prevents floods of hits while typing. Accepts variables.]
+|`pass:c[3]`
+
+|pass:c[Maximum search results]
+|pass:c[Hard limit on the number of results built for Hop GUI search (default 500). Further matches are not collected so the UI stays responsive. Accepts variables.]
+|`pass:c[500]`
+
+|pass:c[Maximum matches per text file]
+|pass:c[Stop scanning a single text file after this many matching lines (default 20). Accepts variables.]
+|`pass:c[20]`
+
+|pass:c[Maximum text file size for search (MB)]
+|pass:c[Text files larger than this are skipped for content search (default 1 MB). Use dedicated tools for million-row CSV/JSON files. Accepts variables.]
+|`pass:c[1]`
+
+|pass:c[Include project text files in content search]
+|pass:c[When enabled, CSV, JSON, SQL and other project text files are content-searched (subject to size and match limits). Open text files are always eligible. Disable to search only pipelines, workflows, metadata and open files.]
+|`pass:c[true]`
+
+|pass:c[Search as you type]
+|pass:c[When enabled, search runs automatically after a short pause while typing. Press Enter to search immediately either way.]
+|`pass:c[true]`
+
+|pass:c[Search-as-you-type delay (ms)]
+|pass:c[Milliseconds to wait after the last keystroke before running a live search (default 300). Accepts variables.]
+|`pass:c[300]`
+
+|===
diff --git a/docs/hop-user-manual/modules/ROOT/partials/config/WelcomeDialogOption.adoc b/docs/hop-user-manual/modules/ROOT/partials/config/WelcomeDialogOption.adoc
new file mode 100644
index 00000000000..c69a723b414
--- /dev/null
+++ b/docs/hop-user-manual/modules/ROOT/partials/config/WelcomeDialogOption.adoc
@@ -0,0 +1,28 @@
+////
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+////
+// Generated from the @ConfigPlugin and @GuiWidgetElement annotations.
+// Edit those, not this file.
+
+[%header, cols="2,5,1"]
+|===
+|Option|Description|Default
+
+|pass:c[Show welcome dialog at the startup of the Hop GUI]
+|pass:c[Show the welcome dialog when Hop GUI starts. The dialog has a Don't show this at startup option of its own that sets the same thing, and the ${HOP_NO_WELCOME_DIALOG} variable suppresses it whatever is set here.]
+|`pass:c[true]`
+
+|===
diff --git a/docs/hop-user-manual/modules/ROOT/partials/config/all-plugins.adoc b/docs/hop-user-manual/modules/ROOT/partials/config/all-plugins.adoc
new file mode 100644
index 00000000000..ef8952e073b
--- /dev/null
+++ b/docs/hop-user-manual/modules/ROOT/partials/config/all-plugins.adoc
@@ -0,0 +1,498 @@
+////
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+////
+// Generated from the @ConfigPlugin and @GuiWidgetElement annotations.
+// Edit those, not this file.
+
+== AI Assistant
+
+These options are stored under `pass:c[hopAiConfig]` in `hop-config.json`.
+
+[%header, cols="2,5,1"]
+|===
+|Option|Description|Default
+
+|pass:c[Enable AI advisory]
+|pass:c[When disabled, AI Help is hidden and no prompts are sent. Default is off. Before applying AI changes, work on a Git feature branch so you can revert the project if needed.]
+|`pass:c[false]`
+
+|pass:c[Default AI provider]
+|pass:c[Named AI Provider metadata used by GUI advisors. Pipelines can still pick a different provider. Accepts variables.]
+|
+
+|pass:c[Allow sending full XML]
+|pass:c[Second gate for the session "Include full XML" checkbox. Off by default. Full XML is large and can contain SQL, filenames and notes. Leave this off unless you need it, and only after secrets live in environment variables or a keystore.]
+|`pass:c[false]`
+
+|pass:c[Extra context]
+a|pass:c[Standing notes sent with every AI Assistant question. The default reminds the model that metadata names are case-sensitive (`local` is not `Local`), that pipeline and workflow run configurations are different types, and that Hop is not Kettle. Add project conventions here. Accepts variables.]
+
+.Default
+[listing]
+----
+Hop metadata names are case-sensitive.
+Pipeline run configurations and workflow run configurations are different metadata types.
+Do not guess names of existing metadata, transforms, or actions that are not in the prompt context. New objects the user asked to create should use the names they gave.
+Variables use ${NAME}. Prefer variables or a resolver for secrets.
+Hop uses pipelines (.hpl) and workflows (.hwf) with transforms and actions, not Kettle transformations, jobs, steps, or job entries.
+----
+|_(with the description)_
+
+|pass:c[Context files]
+|pass:c[One VFS path per line. Files that exist are appended to the system prompt; missing files are skipped. Default is ${PROJECT_HOME}/AGENTS.md. Variables are resolved. Accepts variables.]
+|`pass:c[${PROJECT_HOME}/AGENTS.md]`
+
+|===
+
+== Apache Calcite SQL formatter
+
+These options are stored under `pass:c[calciteSqlFormat]` in `hop-config.json`.
+
+[%header, cols="2,5,1"]
+|===
+|Option|Description|Default
+
+|pass:c[Indentation (spaces)]
+|pass:c[Number of spaces to indent nested SQL. Calcite default is 4; Hop default is 2.]
+|`pass:c[2]`
+
+|pass:c[Line length]
+|pass:c[Preferred maximum line length. 0 means no maximum.]
+|`pass:c[0]`
+
+|pass:c[Clause starts a new line]
+|pass:c[Start FROM, WHERE, GROUP BY, HAVING, WINDOW and ORDER BY on a new line.]
+|`pass:c[true]`
+
+|pass:c[Clause is followed by a new line]
+|pass:c[Clause (FROM, WHERE, GROUP BY, HAVING, WINDOW, ORDER BY) is followed by a new line]
+|`pass:c[false]`
+
+|pass:c[Always use parentheses]
+|pass:c[Wrap every expression in parentheses even when operator precedence does not require it.]
+|`pass:c[false]`
+
+|pass:c[Leading comma]
+|pass:c[Commas in SELECT, GROUP BY and ORDER clauses should appear at the start of the line.]
+|`pass:c[false]`
+
+|pass:c[SELECT items on separate lines]
+|pass:c[Put each item in the SELECT list on its own line.]
+|`pass:c[true]`
+
+|pass:c[FROM items on separate lines]
+|pass:c[Put each item in the FROM list on its own line.]
+|`pass:c[true]`
+
+|pass:c[WHERE items on separate lines]
+|pass:c[Put each item in the WHERE list on its own line.]
+|`pass:c[true]`
+
+|pass:c[GROUP BY items on separate lines]
+|pass:c[Put each item in a GROUP BY list on its own line.]
+|`pass:c[true]`
+
+|pass:c[ORDER BY items on separate lines]
+|pass:c[Put each item in an ORDER BY list on its own line.]
+|`pass:c[true]`
+
+|pass:c[CASE clauses on new lines]
+|pass:c[Put WHEN, THEN and ELSE of a CASE expression at the start of a new line.]
+|`pass:c[true]`
+
+|pass:c[WINDOW declaration starts a new line]
+|pass:c[Start a WINDOW declaration on a new line.]
+|`pass:c[true]`
+
+|pass:c[WINDOW items on separate lines]
+|pass:c[Put each item in a WINDOW list on its own line.]
+|`pass:c[true]`
+
+|pass:c[Keywords in lower case]
+|pass:c[Print SQL keywords such as SELECT and FROM in lower case.]
+|`pass:c[false]`
+
+|pass:c[Quote all identifiers]
+|pass:c[Quote every identifier even when the dialect would accept it unquoted.]
+|`pass:c[false]`
+
+|===
+
+== Database Perspective
+
+These options are stored under `pass:c[database-perspective]` in `hop-config.json`.
+
+[%header, cols="2,5,1"]
+|===
+|Option|Description|Default
+
+|pass:c[Automatically connect when executing SQL]
+|pass:c[Connect the SQL editors database without asking if it is not already connected. You can also enable this from the dialog that appears when the option is off.]
+|`pass:c[false]`
+
+|pass:c[Select the executed SQL in the editor]
+|pass:c[After Run or Run all, highlight only the statement(s) that were sent to the database. A leftover clause after a semicolon is left unselected.]
+|`pass:c[true]`
+
+|pass:c[Query row limit]
+|pass:c[Maximum number of rows shown for a SELECT in the SQL editor and for table preview. A SQL LIMIT above this value is still capped here so a SELECT * does not pull the whole table.]
+|`pass:c[1000]`
+
+|===
+
+== Dropbox
+
+These options are stored under `pass:c[dropbox]` in `hop-config.json`.
+
+[%header, cols="2,5,1"]
+|===
+|Option|Description|Default
+
+|pass:c[Access token]
+|pass:c[The Dropbox access token used to authenticate. Generate one for an app in the Dropbox App Console; it grants whatever access that app was given. Accepts variables.]
+|
+
+|===
+
+== Explorer Perspective
+
+These options are stored under `pass:c[explorer-perspective]` in `hop-config.json`.
+
+[%header, cols="2,5,1"]
+|===
+|Option|Description|Default
+
+|pass:c[The initial depth to load not lazily]
+|pass:c[The initial depth to load not lazily. Accepts variables.]
+|`pass:c[0]`
+
+|pass:c[The maximum file size to load]
+|pass:c[The maximum file size to load. Accepts variables.]
+|`pass:c[16]`
+
+|pass:c[Show file explorer panel by default]
+|pass:c[When enabled, the file explorer panel (project tree) is shown by default when opening the explorer perspective]
+|`pass:c[true]`
+
+|pass:c[Open help pages in]
+|pass:c[Choose where documentation opens: the system browser, a tab in the File Explorer perspective, or a dialog on top of the current window.]
+|`pass:c[External browser]`
+
+|pass:c[Select active file in tree automatically]
+|pass:c[Automatically select the active tab file in the file explorer tree on the left hand side when it is shown in a tab.]
+|`pass:c[true]`
+
+|pass:c[Maximum undo operations]
+|pass:c[The maximum number of undo/redo snapshots kept for pipelines, workflows and table editors. Default is 100. Accepts variables.]
+|`pass:c[100]`
+
+|===
+
+== File validation
+
+[%header, cols="2,5,1"]
+|===
+|Option|Description|Default
+
+|pass:c[Validate database connections when saving]
+|pass:c[When saving a pipeline or workflow, warn if a transform or action references a database connection that is not in the project metadata. Names that still contain a variable such as ${CONNECTION} after resolving the current environment are skipped. Saving is never blocked.]
+|`pass:c[true]`
+
+|===
+
+== Git
+
+These options are stored under `pass:c[gitConfig]` in `hop-config.json`.
+
+[%header, cols="2,5,1"]
+|===
+|Option|Description|Default
+
+|pass:c[Enable the git GUI plugin]
+|pass:c[Switches the git plugin on or off. With it off the git perspective and the git decorations in the file explorer are hidden, and no repository is opened.]
+|`pass:c[true]`
+
+|pass:c[Search parent folders for git repository]
+|pass:c[Look in parent folders for the git repository rather than only in the project folder. Needed where the project sits in a subfolder of a larger repository.]
+|`pass:c[false]`
+
+|pass:c[Fetch remote changes automatically]
+|pass:c[Enables background fetching of remote updates every 20 minutes.]
+|`pass:c[false]`
+
+|pass:c[Ignore position in the visual diff]
+|pass:c[Do not mark a transform or action as changed when it was only moved.]
+|`pass:c[true]`
+
+|===
+
+== Google Cloud
+
+These options are stored under `pass:c[googleCloud]` in `hop-config.json`.
+
+[%header, cols="2,5,1"]
+|===
+|Option|Description|Default
+
+|pass:c[Account key file]
+|pass:c[The path to a Google Cloud service account JSON key file. Accepts variables.]
+|
+
+|pass:c[Scan folders to find last modified data]
+|pass:c[Scans all files in a folder and uses the most recent modification data as the folder modification data. This may have a performance impact in buckets with lots of files.]
+|`pass:c[false]`
+
+|pass:c[Max number of attempts]
+|pass:c[Maximum number of attempts for a single request, including the first one. Accepts variables.]
+|`pass:c[6]`
+
+|pass:c[Initial retry delay (seconds)]
+|pass:c[Delay before the first retry, in seconds. Accepts variables.]
+|`pass:c[1]`
+
+|pass:c[Retry delay multiplier]
+|pass:c[Retry delay multiplier. Accepts variables.]
+|`pass:c[2.0]`
+
+|pass:c[Maximum retry delay (seconds)]
+|pass:c[Ceiling for the retry delay once the multiplier has been applied, in seconds. Accepts variables.]
+|`pass:c[32]`
+
+|pass:c[Total timeout (minutes)]
+|pass:c[Deadline for a request including all of its retries, in minutes. Accepts variables.]
+|`pass:c[50]`
+
+|pass:c[Initial RPC timeout (seconds)]
+|pass:c[Timeout for the first attempt, in seconds. Raised to the max RPC timeout if that is lower. Accepts variables.]
+|`pass:c[50]`
+
+|pass:c[RPC Timeout Multiplier]
+|pass:c[RPC Timeout Multiplier. Accepts variables.]
+|`pass:c[1.0]`
+
+|pass:c[Max RPC timeout (seconds)]
+|pass:c[Ceiling for a single attempt timeout, in seconds. Never lower than the initial RPC timeout. Accepts variables.]
+|`pass:c[50]`
+
+|pass:c[Connect timeout (seconds)]
+|pass:c[Socket connect timeout, in seconds. Accepts variables.]
+|`pass:c[20]`
+
+|pass:c[Read timeout (seconds)]
+|pass:c[Socket read timeout, in seconds. Accepts variables.]
+|`pass:c[20]`
+
+|pass:c[Retry non-idempotent operations]
+|pass:c[Also retry writes (create, delete, starting an upload). Google Cloud Storage only retries reads by default, so without this the retry settings above do not apply to writing files. Note that a retried delete can report "not found" when the first attempt already succeeded, and a retried create overwrites.]
+|`pass:c[false]`
+
+|pass:c[Cache TTL (seconds)]
+|pass:c[How long to cache folder listing results (in seconds, default 5). If not set, falls back to 10 seconds. Accepts variables.]
+|`pass:c[5]`
+
+|===
+
+== Google Drive VFS
+
+These options are stored under `pass:c[googleDrive]` in `hop-config.json`.
+
+[%header, cols="2,5,1"]
+|===
+|Option|Description|Default
+
+|pass:c[Path to a Google Drive credentials JSON file]
+|pass:c[Please note that this is a global configuration option and while you can specify variables that these will be not include project specific settings. Accepts variables.]
+|
+
+|pass:c[Path to a tokens folder for credential caching]
+|pass:c[Pick a safe folder to store these tokens. Accepts variables.]
+|
+
+|===
+
+== Linter
+
+[%header, cols="2,5,1"]
+|===
+|Option|Description|Default
+
+|pass:c[Configuration File]
+|pass:c[Path to hop-lint.yml configuration file (leave empty for project-specific). Accepts variables.]
+|
+
+|pass:c[Enable Linter]
+|pass:c[Enable or disable the Hop Linter globally]
+|`pass:c[true]`
+
+|pass:c[Include Lint Rules in Pipeline Verify]
+|pass:c[Add YAML/custom lint findings to the native pipeline verify Problems tab]
+|`pass:c[true]`
+
+|pass:c[Include Lint Rules in Workflow Verify]
+|pass:c[Add YAML/custom lint findings to the native workflow verify Problems tab]
+|`pass:c[true]`
+
+|pass:c[Include Hop Native Checks in Lint Runs]
+|pass:c[Also run Hops built-in pipeline/workflow verify checks during lint project/file scans]
+|`pass:c[true]`
+
+|pass:c[Lint Automatically While Editing]
+|pass:c[Re-run lint checks shortly after you change an open pipeline or workflow. When disabled, files are only linted on open, save, or on demand.]
+|`pass:c[true]`
+
+|pass:c[Block Commits on Warnings]
+|pass:c[When enabled, warnings also block commits. When disabled, only errors block commits]
+|`pass:c[false]`
+
+|pass:c[Block Git Commits on Lint Failures]
+|pass:c[When enabled, lint checks run before Explorer git commits and block the commit if issues are found]
+|`pass:c[false]`
+
+|pass:c[Include Metadata in Pre-Commit Checks]
+|pass:c[Also lint staged metadata files (e.g. metadata/**/*.json) during pre-commit checks]
+|`pass:c[true]`
+
+|pass:c[Mark Ignored Transforms and Actions]
+|pass:c[Draw a muted outline on a transform or action whose lint findings have been marked as expected, so the absence of a warning reads as a decision rather than as a check that never ran]
+|`pass:c[true]`
+
+|pass:c[Show Lint Indicators on Canvas]
+|pass:c[Show the lint totals overlay (top-left of the canvas) and the severity badges on transforms/actions in open pipeline and workflow editors]
+|`pass:c[true]`
+
+|===
+
+== Microsoft Azure
+
+These options are stored under `pass:c[azure]` in `hop-config.json`.
+
+[%header, cols="2,5,1"]
+|===
+|Option|Description|Default
+
+|pass:c[Account]
+|pass:c[The account to use for the Azure VFS. Accepts variables.]
+|
+
+|pass:c[Account key]
+|pass:c[The key to use for the Azure VFS. Accepts variables.]
+|
+
+|pass:c[Alternative Local Storage]
+|pass:c[URL of Azure emulator/local storage. Accepts variables.]
+|
+
+|===
+
+== Projects
+
+These options are stored under `pass:c[projectsConfig]` in `hop-config.json`.
+
+[%header, cols="2,5,1"]
+|===
+|Option|Description|Default
+
+|pass:c[Enable the projects plugin]
+|pass:c[Switches the projects plugin on or off. With it off, Hop GUI drops the project and environment selectors and files are addressed by path alone.]
+|`pass:c[true]`
+
+|pass:c[Use of a project is mandatory]
+|pass:c[Refuse to run a pipeline or workflow unless a project is selected. Catches runs against the wrong configuration on a shared installation.]
+|`pass:c[false]`
+
+|pass:c[Use of an environment is mandatory]
+|pass:c[Refuse to run a pipeline or workflow unless a lifecycle environment is selected, so every run has explicit environment variables behind it.]
+|`pass:c[false]`
+
+|pass:c[The default project to use when none is specified]
+|pass:c[The default project to use when none is specified. Accepts variables.]
+|
+
+|pass:c[The default environment to use when none is specified]
+|pass:c[The name of the default environment to use when none is specified. Accepts variables.]
+|
+
+|pass:c[The parent project to propose when creating projects]
+|pass:c[The project proposed as the parent when a new project is created. A child project inherits its parent's variables and configuration. Accepts variables.]
+|
+
+|pass:c[The standard projects folder for new projects]
+|pass:c[The standard projects folder for new projects. Accepts variables.]
+|
+
+|pass:c[The project configuration filename for new projects]
+|pass:c[The name given to the configuration file of a newly created project, relative to the project folder. Accepts variables.]
+|`pass:c[project-config.json]`
+
+|pass:c[Clear DB cache when switching project/environment]
+|pass:c[Empty the database cache when switching project or environment. Keep this on where environments point at different databases that share connection names, so metadata cached for the previous one is not reused.]
+|`pass:c[true]`
+
+|pass:c[Sort the last used projects alphabetically]
+|pass:c[List the last used projects alphabetically rather than most recently used first.]
+|`pass:c[false]`
+
+|===
+
+== Search
+
+These options are stored under `pass:c[search]` in `hop-config.json`.
+
+[%header, cols="2,5,1"]
+|===
+|Option|Description|Default
+
+|pass:c[Minimum characters for content search]
+|pass:c[Object names are always searched. File and field content is only searched when the query has at least this many characters (default 3). Prevents floods of hits while typing. Accepts variables.]
+|`pass:c[3]`
+
+|pass:c[Maximum search results]
+|pass:c[Hard limit on the number of results built for Hop GUI search (default 500). Further matches are not collected so the UI stays responsive. Accepts variables.]
+|`pass:c[500]`
+
+|pass:c[Maximum matches per text file]
+|pass:c[Stop scanning a single text file after this many matching lines (default 20). Accepts variables.]
+|`pass:c[20]`
+
+|pass:c[Maximum text file size for search (MB)]
+|pass:c[Text files larger than this are skipped for content search (default 1 MB). Use dedicated tools for million-row CSV/JSON files. Accepts variables.]
+|`pass:c[1]`
+
+|pass:c[Include project text files in content search]
+|pass:c[When enabled, CSV, JSON, SQL and other project text files are content-searched (subject to size and match limits). Open text files are always eligible. Disable to search only pipelines, workflows, metadata and open files.]
+|`pass:c[true]`
+
+|pass:c[Search as you type]
+|pass:c[When enabled, search runs automatically after a short pause while typing. Press Enter to search immediately either way.]
+|`pass:c[true]`
+
+|pass:c[Search-as-you-type delay (ms)]
+|pass:c[Milliseconds to wait after the last keystroke before running a live search (default 300). Accepts variables.]
+|`pass:c[300]`
+
+|===
+
+== Welcome Dialog
+
+[%header, cols="2,5,1"]
+|===
+|Option|Description|Default
+
+|pass:c[Show welcome dialog at the startup of the Hop GUI]
+|pass:c[Show the welcome dialog when Hop GUI starts. The dialog has a Don't show this at startup option of its own that sets the same thing, and the ${HOP_NO_WELCOME_DIALOG} variable suppresses it whatever is set here.]
+|`pass:c[true]`
+
+|===
diff --git a/docs/hop-user-manual/modules/ROOT/partials/config/file-validation-config.adoc b/docs/hop-user-manual/modules/ROOT/partials/config/file-validation-config.adoc
new file mode 100644
index 00000000000..046733f0daf
--- /dev/null
+++ b/docs/hop-user-manual/modules/ROOT/partials/config/file-validation-config.adoc
@@ -0,0 +1,28 @@
+////
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+////
+// Generated from the @ConfigPlugin and @GuiWidgetElement annotations.
+// Edit those, not this file.
+
+[%header, cols="2,5,1"]
+|===
+|Option|Description|Default
+
+|pass:c[Validate database connections when saving]
+|pass:c[When saving a pipeline or workflow, warn if a transform or action references a database connection that is not in the project metadata. Names that still contain a variable such as ${CONNECTION} after resolving the current environment are skipped. Saving is never blocked.]
+|`pass:c[true]`
+
+|===
diff --git a/docs/hop-user-manual/modules/ROOT/partials/config/linter-config.adoc b/docs/hop-user-manual/modules/ROOT/partials/config/linter-config.adoc
new file mode 100644
index 00000000000..52f6ad8d69b
--- /dev/null
+++ b/docs/hop-user-manual/modules/ROOT/partials/config/linter-config.adoc
@@ -0,0 +1,68 @@
+////
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+////
+// Generated from the @ConfigPlugin and @GuiWidgetElement annotations.
+// Edit those, not this file.
+
+[%header, cols="2,5,1"]
+|===
+|Option|Description|Default
+
+|pass:c[Configuration File]
+|pass:c[Path to hop-lint.yml configuration file (leave empty for project-specific). Accepts variables.]
+|
+
+|pass:c[Enable Linter]
+|pass:c[Enable or disable the Hop Linter globally]
+|`pass:c[true]`
+
+|pass:c[Include Lint Rules in Pipeline Verify]
+|pass:c[Add YAML/custom lint findings to the native pipeline verify Problems tab]
+|`pass:c[true]`
+
+|pass:c[Include Lint Rules in Workflow Verify]
+|pass:c[Add YAML/custom lint findings to the native workflow verify Problems tab]
+|`pass:c[true]`
+
+|pass:c[Include Hop Native Checks in Lint Runs]
+|pass:c[Also run Hops built-in pipeline/workflow verify checks during lint project/file scans]
+|`pass:c[true]`
+
+|pass:c[Lint Automatically While Editing]
+|pass:c[Re-run lint checks shortly after you change an open pipeline or workflow. When disabled, files are only linted on open, save, or on demand.]
+|`pass:c[true]`
+
+|pass:c[Block Commits on Warnings]
+|pass:c[When enabled, warnings also block commits. When disabled, only errors block commits]
+|`pass:c[false]`
+
+|pass:c[Block Git Commits on Lint Failures]
+|pass:c[When enabled, lint checks run before Explorer git commits and block the commit if issues are found]
+|`pass:c[false]`
+
+|pass:c[Include Metadata in Pre-Commit Checks]
+|pass:c[Also lint staged metadata files (e.g. metadata/**/*.json) during pre-commit checks]
+|`pass:c[true]`
+
+|pass:c[Mark Ignored Transforms and Actions]
+|pass:c[Draw a muted outline on a transform or action whose lint findings have been marked as expected, so the absence of a warning reads as a decision rather than as a check that never ran]
+|`pass:c[true]`
+
+|pass:c[Show Lint Indicators on Canvas]
+|pass:c[Show the lint totals overlay (top-left of the canvas) and the severity badges on transforms/actions in open pipeline and workflow editors]
+|`pass:c[true]`
+
+|===
diff --git a/plugins/misc/git/src/main/java/org/apache/hop/git/config/GitConfigOptionPlugin.java b/plugins/misc/git/src/main/java/org/apache/hop/git/config/GitConfigOptionPlugin.java
index f9c10361203..228d145a79f 100644
--- a/plugins/misc/git/src/main/java/org/apache/hop/git/config/GitConfigOptionPlugin.java
+++ b/plugins/misc/git/src/main/java/org/apache/hop/git/config/GitConfigOptionPlugin.java
@@ -41,7 +41,9 @@
@ConfigPlugin(
id = "GitConfigOptionPlugin",
- description = "Configuration options for the git GUI plugin")
+ description = "Configuration options for the git GUI plugin",
+ configKey = GitConfig.HOP_CONFIG_GIT_CONFIG_KEY,
+ configClass = GitConfig.class)
@GuiPlugin(
description = "i18n::GitConfig.Tab.Name" // label in options dialog
)
@@ -61,17 +63,19 @@ public class GitConfigOptionPlugin implements IConfigOptions, IGuiPluginComposit
id = WIDGET_ID_GIT_ENABLE,
parentId = ConfigPluginOptionsTab.GUI_WIDGETS_PARENT_ID,
type = GuiElementType.CHECKBOX,
- label = "i18n::GitConfig.EnableGitPlugin.Message")
+ label = "i18n::GitConfig.EnableGitPlugin.Message",
+ toolTip = "i18n::GitConfig.EnableGitPlugin.Tooltip")
@CommandLine.Option(
names = {"--git-gui-enabled"},
description = "Enable or disable the git GUI plugin")
- private Boolean gitEnabled;
+ private Boolean enabled;
@GuiWidgetElement(
id = WIDGET_ID_GIT_SEARCH_PARENT_FOLDERS,
parentId = ConfigPluginOptionsTab.GUI_WIDGETS_PARENT_ID,
type = GuiElementType.CHECKBOX,
- label = "i18n::GitConfig.SearchParentFolders.Message")
+ label = "i18n::GitConfig.SearchParentFolders.Message",
+ toolTip = "i18n::GitConfig.SearchParentFolders.Tooltip")
@CommandLine.Option(
names = {"--git-gui-search-parent-folders"},
description = "The git GUI searches the parent folders for a repository")
@@ -99,7 +103,7 @@ public class GitConfigOptionPlugin implements IConfigOptions, IGuiPluginComposit
public static GitConfigOptionPlugin getInstance() {
GitConfigOptionPlugin instance = new GitConfigOptionPlugin();
GitConfig config = GitConfigSingleton.getConfig();
- instance.gitEnabled = config.isEnabled();
+ instance.enabled = config.isEnabled();
instance.searchingParentFolders = config.isSearchingParentFolders();
instance.fetchAutomatic = config.isFetchAutomatic();
instance.ignoringPositionInDiff = config.isIgnoringPositionInDiff();
@@ -113,9 +117,9 @@ public boolean handleOption(
GitConfig config = GitConfigSingleton.getConfig();
try {
boolean changed = false;
- if (gitEnabled != null) {
- config.setEnabled(gitEnabled);
- if (gitEnabled) {
+ if (enabled != null) {
+ config.setEnabled(enabled);
+ if (enabled) {
log.logBasic("Enabled the git GUI plugin");
} else {
log.logBasic("Disabled the git GUI plugin");
@@ -179,8 +183,8 @@ public void persistContents(GuiCompositeWidgets compositeWidgets) {
Control control = compositeWidgets.getWidgetsMap().get(widgetId);
switch (widgetId) {
case WIDGET_ID_GIT_ENABLE:
- gitEnabled = ((Button) control).getSelection();
- GitConfigSingleton.getConfig().setEnabled(gitEnabled);
+ enabled = ((Button) control).getSelection();
+ GitConfigSingleton.getConfig().setEnabled(enabled);
break;
case WIDGET_ID_GIT_SEARCH_PARENT_FOLDERS:
searchingParentFolders = ((Button) control).getSelection();
diff --git a/plugins/misc/git/src/main/resources/org/apache/hop/git/config/messages/messages_en_US.properties b/plugins/misc/git/src/main/resources/org/apache/hop/git/config/messages/messages_en_US.properties
index de3dc9da3a8..5f29d882dfb 100644
--- a/plugins/misc/git/src/main/resources/org/apache/hop/git/config/messages/messages_en_US.properties
+++ b/plugins/misc/git/src/main/resources/org/apache/hop/git/config/messages/messages_en_US.properties
@@ -23,4 +23,6 @@ GitConfig.SavingOption.ErrorDialog.Message=Error saving option
GitConfig.FetchAutomatic.Message=Fetch remote changes automatically
GitConfig.FetchAutomatic.Tooltip=Enables background fetching of remote updates every 20 minutes.
GitConfig.IgnorePositionInDiff.Message=Ignore position in the visual diff
-GitConfig.IgnorePositionInDiff.Tooltip=Do not mark a transform or action as changed when it was only moved.
\ No newline at end of file
+GitConfig.IgnorePositionInDiff.Tooltip=Do not mark a transform or action as changed when it was only moved.
+GitConfig.EnableGitPlugin.Tooltip=Switches the git plugin on or off. With it off the git perspective and the git decorations in the file explorer are hidden, and no repository is opened.
+GitConfig.SearchParentFolders.Tooltip=Look in parent folders for the git repository rather than only in the project folder. Needed where the project sits in a subfolder of a larger repository.
diff --git a/plugins/misc/lint/src/main/java/org/apache/hop/lint/LinterConfigPlugin.java b/plugins/misc/lint/src/main/java/org/apache/hop/lint/LinterConfigPlugin.java
index 9f5585efdea..7c41c310eef 100644
--- a/plugins/misc/lint/src/main/java/org/apache/hop/lint/LinterConfigPlugin.java
+++ b/plugins/misc/lint/src/main/java/org/apache/hop/lint/LinterConfigPlugin.java
@@ -53,7 +53,7 @@
* Rules.
*/
@ConfigPlugin(id = "linter-config", description = "Configure linter rules and settings")
-@GuiPlugin(description = "Linter Configuration GUI")
+@GuiPlugin(description = "Linter")
public class LinterConfigPlugin implements IConfigOptions, IGuiPluginCompositeWidgetsListener {
private static final ILogChannel log = LogChannel.GENERAL;
@@ -256,7 +256,8 @@ private static void putIfSet(Map options, String key, Object val
parentId = ConfigPluginOptionsTab.GUI_WIDGETS_PARENT_ID,
type = GuiElementType.CHECKBOX,
label = "i18n::LinterConfigPlugin.Option.Enabled.Label",
- toolTip = "i18n::LinterConfigPlugin.Option.Enabled.ToolTip")
+ toolTip = "i18n::LinterConfigPlugin.Option.Enabled.ToolTip",
+ defaultValue = "true")
@CommandLine.Option(
names = {"--lint-enabled"},
description = "Enable or disable the linter (default: true)")
@@ -267,7 +268,8 @@ private static void putIfSet(Map options, String key, Object val
parentId = ConfigPluginOptionsTab.GUI_WIDGETS_PARENT_ID,
type = GuiElementType.CHECKBOX,
label = "i18n::LinterConfigPlugin.Option.LintOnEdit.Label",
- toolTip = "i18n::LinterConfigPlugin.Option.LintOnEdit.ToolTip")
+ toolTip = "i18n::LinterConfigPlugin.Option.LintOnEdit.ToolTip",
+ defaultValue = "true")
@CommandLine.Option(
names = {"--lint-on-edit"},
description = "Lint files while they are being edited (default: true)")
@@ -278,7 +280,8 @@ private static void putIfSet(Map options, String key, Object val
parentId = ConfigPluginOptionsTab.GUI_WIDGETS_PARENT_ID,
type = GuiElementType.CHECKBOX,
label = "i18n::LinterConfigPlugin.Option.ShowIndicators.Label",
- toolTip = "i18n::LinterConfigPlugin.Option.ShowIndicators.ToolTip")
+ toolTip = "i18n::LinterConfigPlugin.Option.ShowIndicators.ToolTip",
+ defaultValue = "true")
@CommandLine.Option(
names = {"--lint-problems-bar"},
description = "Show the lint problems bar (default: true)")
@@ -289,7 +292,8 @@ private static void putIfSet(Map options, String key, Object val
parentId = ConfigPluginOptionsTab.GUI_WIDGETS_PARENT_ID,
type = GuiElementType.CHECKBOX,
label = "i18n::LinterConfigPlugin.Option.ShowIgnoredMarkers.Label",
- toolTip = "i18n::LinterConfigPlugin.Option.ShowIgnoredMarkers.ToolTip")
+ toolTip = "i18n::LinterConfigPlugin.Option.ShowIgnoredMarkers.ToolTip",
+ defaultValue = "true")
@CommandLine.Option(
names = {"--lint-show-ignored-markers"},
description = "Mark transforms and actions whose findings are ignored (default: true)")
@@ -312,7 +316,8 @@ private static void putIfSet(Map options, String key, Object val
parentId = ConfigPluginOptionsTab.GUI_WIDGETS_PARENT_ID,
type = GuiElementType.CHECKBOX,
label = "i18n::LinterConfigPlugin.Option.PreCommit.Label",
- toolTip = "i18n::LinterConfigPlugin.Option.PreCommit.ToolTip")
+ toolTip = "i18n::LinterConfigPlugin.Option.PreCommit.ToolTip",
+ defaultValue = "false")
@CommandLine.Option(
names = {"--lint-block-commits"},
description = "Block git commits from Hop Gui on lint failures (default: false)")
@@ -323,7 +328,8 @@ private static void putIfSet(Map options, String key, Object val
parentId = ConfigPluginOptionsTab.GUI_WIDGETS_PARENT_ID,
type = GuiElementType.CHECKBOX,
label = "i18n::LinterConfigPlugin.Option.PreCommitWarnings.Label",
- toolTip = "i18n::LinterConfigPlugin.Option.PreCommitWarnings.ToolTip")
+ toolTip = "i18n::LinterConfigPlugin.Option.PreCommitWarnings.ToolTip",
+ defaultValue = "false")
@CommandLine.Option(
names = {"--lint-block-on-warnings"},
description = "Block commits on warnings, not only errors (default: false)")
@@ -334,7 +340,8 @@ private static void putIfSet(Map options, String key, Object val
parentId = ConfigPluginOptionsTab.GUI_WIDGETS_PARENT_ID,
type = GuiElementType.CHECKBOX,
label = "i18n::LinterConfigPlugin.Option.PreCommitMetadata.Label",
- toolTip = "i18n::LinterConfigPlugin.Option.PreCommitMetadata.ToolTip")
+ toolTip = "i18n::LinterConfigPlugin.Option.PreCommitMetadata.ToolTip",
+ defaultValue = "true")
@CommandLine.Option(
names = {"--lint-commit-metadata"},
description = "Lint metadata files when checking a commit (default: true)")
@@ -345,7 +352,8 @@ private static void putIfSet(Map options, String key, Object val
parentId = ConfigPluginOptionsTab.GUI_WIDGETS_PARENT_ID,
type = GuiElementType.CHECKBOX,
label = "i18n::LinterConfigPlugin.Option.PipelineVerify.Label",
- toolTip = "i18n::LinterConfigPlugin.Option.PipelineVerify.ToolTip")
+ toolTip = "i18n::LinterConfigPlugin.Option.PipelineVerify.ToolTip",
+ defaultValue = "true")
@CommandLine.Option(
names = {"--lint-in-pipeline-verify"},
description = "Add lint findings to pipeline Verify (default: true)")
@@ -356,7 +364,8 @@ private static void putIfSet(Map options, String key, Object val
parentId = ConfigPluginOptionsTab.GUI_WIDGETS_PARENT_ID,
type = GuiElementType.CHECKBOX,
label = "i18n::LinterConfigPlugin.Option.WorkflowVerify.Label",
- toolTip = "i18n::LinterConfigPlugin.Option.WorkflowVerify.ToolTip")
+ toolTip = "i18n::LinterConfigPlugin.Option.WorkflowVerify.ToolTip",
+ defaultValue = "true")
@CommandLine.Option(
names = {"--lint-in-workflow-verify"},
description = "Add lint findings to workflow Verify (default: true)")
@@ -367,7 +376,8 @@ private static void putIfSet(Map options, String key, Object val
parentId = ConfigPluginOptionsTab.GUI_WIDGETS_PARENT_ID,
type = GuiElementType.CHECKBOX,
label = "i18n::LinterConfigPlugin.Option.NativeChecks.Label",
- toolTip = "i18n::LinterConfigPlugin.Option.NativeChecks.ToolTip")
+ toolTip = "i18n::LinterConfigPlugin.Option.NativeChecks.ToolTip",
+ defaultValue = "true")
@CommandLine.Option(
names = {"--lint-native-checks"},
description = "Include Hop's own checks alongside lint findings (default: true)")
diff --git a/plugins/misc/lint/src/test/java/org/apache/hop/lint/LinterConfigPluginDefaultsTest.java b/plugins/misc/lint/src/test/java/org/apache/hop/lint/LinterConfigPluginDefaultsTest.java
new file mode 100644
index 00000000000..c9fbe255d64
--- /dev/null
+++ b/plugins/misc/lint/src/test/java/org/apache/hop/lint/LinterConfigPluginDefaultsTest.java
@@ -0,0 +1,101 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.hop.lint;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.DynamicTest.dynamicTest;
+
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.hop.core.config.HopConfig;
+import org.apache.hop.core.gui.plugin.GuiWidgetElement;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.DynamicTest;
+import org.junit.jupiter.api.TestFactory;
+
+/**
+ * The documentation for the configuration perspective reports what each option falls back to when
+ * it has never been set. Most config plugins keep those values on the {@code configClass()} of
+ * their {@code @ConfigPlugin}, where a generator reads the real thing. This one does not: it reads
+ * each option straight from {@link HopConfig} with the fallback written inline at the call site, so
+ * the documented value has to be repeated in {@code GuiWidgetElement#defaultValue()} and can fall
+ * out of step with the code.
+ *
+ * This closes that gap. With nothing configured, every {@code readOption...} call returns its
+ * inline fallback, so the loaded field is by definition the real default - and has to match what
+ * the annotation promises the reader.
+ */
+class LinterConfigPluginDefaultsTest {
+
+ private Map savedConfig;
+
+ @AfterEach
+ void restoreConfiguration() {
+ if (savedConfig != null) {
+ HopConfig.getInstance().getConfigMap().putAll(savedConfig);
+ savedConfig = null;
+ }
+ }
+
+ @TestFactory
+ List everyDeclaredDefaultIsTheRealDefault() {
+ List annotated = new ArrayList<>();
+ for (Field field : LinterConfigPlugin.class.getDeclaredFields()) {
+ GuiWidgetElement widget = field.getAnnotation(GuiWidgetElement.class);
+ if (widget != null && !widget.defaultValue().isEmpty()) {
+ annotated.add(field);
+ }
+ }
+ assertFalse(
+ annotated.isEmpty(),
+ "No option declares a defaultValue, so the cases below would pass without testing anything");
+
+ // An unconfigured Hop: every readOption... call now falls through to its inline default.
+ savedConfig = new HashMap<>(HopConfig.getInstance().getConfigMap());
+ HopConfig.getInstance().getConfigMap().clear();
+ LinterConfigPlugin unconfigured = new LinterConfigPlugin();
+
+ List tests = new ArrayList<>();
+ for (Field field : annotated) {
+ String declared = field.getAnnotation(GuiWidgetElement.class).defaultValue();
+ tests.add(
+ dynamicTest(
+ field.getName(),
+ () -> {
+ field.setAccessible(true);
+ Object actual = field.get(unconfigured);
+ assertEquals(
+ declared,
+ String.valueOf(actual),
+ "@GuiWidgetElement(defaultValue) on LinterConfigPlugin."
+ + field.getName()
+ + " says the option defaults to '"
+ + declared
+ + "', but with nothing configured it loads as '"
+ + actual
+ + "'. The documentation is generated from the annotation, so either the"
+ + " fallback at the readOption call site or this annotation is wrong.");
+ }));
+ }
+ return tests;
+ }
+}
diff --git a/plugins/misc/projects/src/main/java/org/apache/hop/projects/config/ProjectsConfigOptionPlugin.java b/plugins/misc/projects/src/main/java/org/apache/hop/projects/config/ProjectsConfigOptionPlugin.java
index e73a161b3b4..f0a7cce4da4 100644
--- a/plugins/misc/projects/src/main/java/org/apache/hop/projects/config/ProjectsConfigOptionPlugin.java
+++ b/plugins/misc/projects/src/main/java/org/apache/hop/projects/config/ProjectsConfigOptionPlugin.java
@@ -49,7 +49,9 @@
@ConfigPlugin(
id = "ProjectsConfigOptionPlugin",
- description = "Configuration options for the global projects plugin")
+ description = "Configuration options for the global projects plugin",
+ configKey = ProjectsConfig.HOP_CONFIG_PROJECTS_CONFIG_KEY,
+ configClass = ProjectsConfig.class)
@GuiPlugin(
description = "i18n::ProjectConfig.Tab.Name" // label in options dialog
)
@@ -77,17 +79,19 @@ public class ProjectsConfigOptionPlugin
id = WIDGET_ID_ENABLE_PROJECTS,
parentId = ConfigPluginOptionsTab.GUI_WIDGETS_PARENT_ID,
type = GuiElementType.CHECKBOX,
- label = "i18n::ProjectConfig.EnableProjectPlugin.Message")
+ label = "i18n::ProjectConfig.EnableProjectPlugin.Message",
+ toolTip = "i18n::ProjectConfig.EnableProjectPlugin.Description")
@CommandLine.Option(
names = {"-pn", "--projects-enabled"},
description = "Enable or disable the projects plugin")
- private Boolean projectsEnabled;
+ private Boolean enabled;
@GuiWidgetElement(
id = WIDGET_ID_PROJECT_MANDATORY,
parentId = ConfigPluginOptionsTab.GUI_WIDGETS_PARENT_ID,
type = GuiElementType.CHECKBOX,
- label = "i18n::ProjectConfig.ProjectMandatory.Message")
+ label = "i18n::ProjectConfig.ProjectMandatory.Message",
+ toolTip = "i18n::ProjectConfig.ProjectMandatory.Description")
@CommandLine.Option(
names = {"-py", "--project-mandatory"},
description = "Make it mandatory to reference a project")
@@ -97,7 +101,8 @@ public class ProjectsConfigOptionPlugin
id = WIDGET_ID_ENVIRONMENT_MANDATORY,
parentId = ConfigPluginOptionsTab.GUI_WIDGETS_PARENT_ID,
type = GuiElementType.CHECKBOX,
- label = "i18n::ProjectConfig.EnvironmentMandatory.Message")
+ label = "i18n::ProjectConfig.EnvironmentMandatory.Message",
+ toolTip = "i18n::ProjectConfig.EnvironmentMandatory.Description")
@CommandLine.Option(
names = {"-ey", "--environment-mandatory"},
description = "Make it mandatory to reference an environment")
@@ -109,7 +114,8 @@ public class ProjectsConfigOptionPlugin
type = GuiElementType.COMBO,
comboValuesMethod = "getProjectsList",
variables = true,
- label = "i18n::ProjectConfig.DefaultProject.Message")
+ label = "i18n::ProjectConfig.DefaultProject.Message",
+ toolTip = "i18n::ProjectConfig.DefaultProject.Description")
@CommandLine.Option(
names = {"-dp", "--default-project"},
description = "The default project to use when none is specified")
@@ -120,7 +126,8 @@ public class ProjectsConfigOptionPlugin
parentId = ConfigPluginOptionsTab.GUI_WIDGETS_PARENT_ID,
type = GuiElementType.TEXT,
variables = true,
- label = "i18n::ProjectConfig.DefaultEnvironment.Message")
+ label = "i18n::ProjectConfig.DefaultEnvironment.Message",
+ toolTip = "i18n::ProjectConfig.DefaultEnvironment.Description")
@CommandLine.Option(
names = {"-de", "--default-environment"},
description = "The name of the default environment to use when none is specified")
@@ -132,7 +139,8 @@ public class ProjectsConfigOptionPlugin
type = GuiElementType.COMBO,
comboValuesMethod = "getProjectsList",
variables = true,
- label = "i18n::ProjectConfig.ParentProject.Message")
+ label = "i18n::ProjectConfig.ParentProject.Message",
+ toolTip = "i18n::ProjectConfig.ParentProject.Description")
@CommandLine.Option(
names = {"-sp", "--standard-parent-project"},
description =
@@ -144,7 +152,8 @@ public class ProjectsConfigOptionPlugin
parentId = ConfigPluginOptionsTab.GUI_WIDGETS_PARENT_ID,
type = GuiElementType.FOLDER,
variables = true,
- label = "i18n::ProjectConfig.StdProjectFolder.Message")
+ label = "i18n::ProjectConfig.StdProjectFolder.Message",
+ toolTip = "i18n::ProjectConfig.StdProjectFolder.Description")
@CommandLine.Option(
names = {"-sj", "--standard-projects-folder"},
description = "The standard projects folder for new projects")
@@ -155,7 +164,8 @@ public class ProjectsConfigOptionPlugin
parentId = ConfigPluginOptionsTab.GUI_WIDGETS_PARENT_ID,
type = GuiElementType.TEXT,
variables = true,
- label = "i18n::ProjectConfig.StdProjectFilename.Message")
+ label = "i18n::ProjectConfig.StdProjectFilename.Message",
+ toolTip = "i18n::ProjectConfig.StdProjectFilename.Description")
@CommandLine.Option(
names = {"-dc", "--default-projects-folder"},
description = "The project configuration filename for new projects")
@@ -177,7 +187,8 @@ public class ProjectsConfigOptionPlugin
parentId = ConfigPluginOptionsTab.GUI_WIDGETS_PARENT_ID,
type = GuiElementType.CHECKBOX,
variables = false,
- label = "i18n::ProjectConfig.ClearDbCache.Message")
+ label = "i18n::ProjectConfig.ClearDbCache.Message",
+ toolTip = "i18n::ProjectConfig.ClearDbCache.Description")
@CommandLine.Option(
names = {"-cdb", "--clear-db-when-env-switching"},
description = "Clear database cache when switching project/environment")
@@ -188,7 +199,8 @@ public class ProjectsConfigOptionPlugin
parentId = ConfigPluginOptionsTab.GUI_WIDGETS_PARENT_ID,
type = GuiElementType.CHECKBOX,
variables = false,
- label = "i18n::ProjectConfig.SortByNameLastUsedProjects.Message")
+ label = "i18n::ProjectConfig.SortByNameLastUsedProjects.Message",
+ toolTip = "i18n::ProjectConfig.SortByNameLastUsedProjects.Description")
@Getter
@Setter
private Boolean sortByNameLastUsedProjects;
@@ -202,7 +214,7 @@ public static ProjectsConfigOptionPlugin getInstance() {
ProjectsConfigOptionPlugin instance = new ProjectsConfigOptionPlugin();
ProjectsConfig config = ProjectsConfigSingleton.getConfig();
- instance.projectsEnabled = config.isEnabled();
+ instance.enabled = config.isEnabled();
instance.defaultProject = config.getDefaultProject();
instance.defaultEnvironment = config.getDefaultEnvironment();
instance.projectMandatory = config.isProjectMandatory();
@@ -222,9 +234,9 @@ public boolean handleOption(
ProjectsConfig config = ProjectsConfigSingleton.getConfig();
try {
boolean changed = false;
- if (projectsEnabled != null) {
- config.setEnabled(projectsEnabled);
- if (projectsEnabled) {
+ if (enabled != null) {
+ config.setEnabled(enabled);
+ if (enabled) {
log.logBasic("Enabled the projects system");
} else {
log.logBasic("Disabled the projects system");
@@ -338,8 +350,8 @@ public void persistContents(GuiCompositeWidgets compositeWidgets) {
Control control = compositeWidgets.getWidgetsMap().get(widgetId);
switch (widgetId) {
case WIDGET_ID_ENABLE_PROJECTS:
- projectsEnabled = ((Button) control).getSelection();
- ProjectsConfigSingleton.getConfig().setEnabled(projectsEnabled);
+ enabled = ((Button) control).getSelection();
+ ProjectsConfigSingleton.getConfig().setEnabled(enabled);
break;
case WIDGET_ID_PROJECT_MANDATORY:
projectMandatory = ((Button) control).getSelection();
diff --git a/plugins/misc/projects/src/main/resources/org/apache/hop/projects/config/messages/messages_en_US.properties b/plugins/misc/projects/src/main/resources/org/apache/hop/projects/config/messages/messages_en_US.properties
index 0e4d4feb053..cd4b49bf963 100644
--- a/plugins/misc/projects/src/main/resources/org/apache/hop/projects/config/messages/messages_en_US.properties
+++ b/plugins/misc/projects/src/main/resources/org/apache/hop/projects/config/messages/messages_en_US.properties
@@ -34,4 +34,11 @@ ProjectConfig.StdProjectFilename.Message=The project configuration filename for
ProjectConfig.StdProjectFolder.Description=The standard projects folder for new projects
ProjectConfig.StdProjectFolder.Message=The standard projects folder for new projects
ProjectConfig.Tab.Name=Projects
-ProjectConfig.ClearDbCache.Message=Clear DB cache when switching project/environment
\ No newline at end of file
+ProjectConfig.ClearDbCache.Message=Clear DB cache when switching project/environment
+ProjectConfig.EnableProjectPlugin.Description=Switches the projects plugin on or off. With it off, Hop GUI drops the project and environment selectors and files are addressed by path alone.
+ProjectConfig.ProjectMandatory.Description=Refuse to run a pipeline or workflow unless a project is selected. Catches runs against the wrong configuration on a shared installation.
+ProjectConfig.EnvironmentMandatory.Description=Refuse to run a pipeline or workflow unless a lifecycle environment is selected, so every run has explicit environment variables behind it.
+ProjectConfig.ParentProject.Description=The project proposed as the parent when a new project is created. A child project inherits its parent''s variables and configuration.
+ProjectConfig.StdProjectFilename.Description=The name given to the configuration file of a newly created project, relative to the project folder.
+ProjectConfig.ClearDbCache.Description=Empty the database cache when switching project or environment. Keep this on where environments point at different databases that share connection names, so metadata cached for the previous one is not reused.
+ProjectConfig.SortByNameLastUsedProjects.Description=List the last used projects alphabetically rather than most recently used first.
diff --git a/plugins/tech/ai/src/main/java/org/apache/hop/ai/config/HopAiConfigOptionPlugin.java b/plugins/tech/ai/src/main/java/org/apache/hop/ai/config/HopAiConfigOptionPlugin.java
index 739c35cafaa..24ea883d9f1 100644
--- a/plugins/tech/ai/src/main/java/org/apache/hop/ai/config/HopAiConfigOptionPlugin.java
+++ b/plugins/tech/ai/src/main/java/org/apache/hop/ai/config/HopAiConfigOptionPlugin.java
@@ -46,7 +46,9 @@
id = "HopAiConfigOptionPlugin",
description = "Configuration options for Hop AI advisory",
category = ConfigPlugin.CATEGORY_CONFIG,
- classLoaderGroup = "hop-ai")
+ classLoaderGroup = "hop-ai",
+ configKey = HopAiConfig.HOP_CONFIG_KEY,
+ configClass = HopAiConfig.class)
@GuiPlugin(description = "i18n::HopAiConfig.Tab.Name", classLoaderGroup = "hop-ai")
public class HopAiConfigOptionPlugin implements IConfigOptions, IGuiPluginCompositeWidgetsListener {
diff --git a/plugins/tech/azure/src/main/java/org/apache/hop/vfs/azure/config/AzureConfigPlugin.java b/plugins/tech/azure/src/main/java/org/apache/hop/vfs/azure/config/AzureConfigPlugin.java
index 9695f7adcce..e4344ba7550 100644
--- a/plugins/tech/azure/src/main/java/org/apache/hop/vfs/azure/config/AzureConfigPlugin.java
+++ b/plugins/tech/azure/src/main/java/org/apache/hop/vfs/azure/config/AzureConfigPlugin.java
@@ -39,7 +39,9 @@
@ConfigPlugin(
id = "AzureConfigPlugin",
description = "i18n::AzureConfig.ConfigPlugin.Description",
- category = ConfigPlugin.CATEGORY_CONFIG)
+ category = ConfigPlugin.CATEGORY_CONFIG,
+ configKey = AzureConfig.HOP_CONFIG_AZURE_CONFIG_KEY,
+ configClass = AzureConfig.class)
@GuiPlugin(
description = "i18n::Azure.GuiPlugin.Description" // Tab label in options dialog
)
@@ -86,7 +88,7 @@ public class AzureConfigPlugin implements IConfigOptions, IGuiPluginCompositeWid
names = {"-aze", "--azure-emulator"},
description =
"The URL of an Azure emulator (Azurite) to be used for test and development goals instead of official Azure services")
- private String localEmulatorUrl;
+ private String emulatorUrl;
/**
* Gets instance
@@ -99,7 +101,7 @@ public static AzureConfigPlugin getInstance() {
AzureConfig config = AzureConfigSingleton.getConfig();
instance.account = config.getAccount();
instance.key = config.getKey();
- instance.localEmulatorUrl = config.getEmulatorUrl();
+ instance.emulatorUrl = config.getEmulatorUrl();
return instance;
}
@@ -124,9 +126,9 @@ public boolean handleOption(
changed = true;
}
- if (localEmulatorUrl != null) {
- config.setEmulatorUrl(localEmulatorUrl);
- log.logBasic("The Azure emulator URL is set to '" + localEmulatorUrl + "'");
+ if (emulatorUrl != null) {
+ config.setEmulatorUrl(emulatorUrl);
+ log.logBasic("The Azure emulator URL is set to '" + emulatorUrl + "'");
}
// Save to file if anything changed
@@ -170,8 +172,8 @@ public void persistContents(GuiCompositeWidgets compositeWidgets) {
AzureConfigSingleton.getConfig().setKey(key);
break;
case WIDGET_ID_AZURE_LOCAL_EMULATOR_URL:
- localEmulatorUrl = ((TextVar) control).getText();
- AzureConfigSingleton.getConfig().setEmulatorUrl(localEmulatorUrl);
+ emulatorUrl = ((TextVar) control).getText();
+ AzureConfigSingleton.getConfig().setEmulatorUrl(emulatorUrl);
break;
default:
break;
@@ -221,14 +223,14 @@ public void setKey(String key) {
/**
* @return the local emulator url
*/
- public String getLocalEmulatorUrl() {
- return localEmulatorUrl;
+ public String getEmulatorUrl() {
+ return emulatorUrl;
}
/**
- * @param localEmulatorUrl The URL of the Azure blob storage
+ * @param emulatorUrl The URL of the Azure blob storage
*/
- public void setLocalEmulatorUrl(String localEmulatorUrl) {
- this.localEmulatorUrl = localEmulatorUrl;
+ public void setEmulatorUrl(String emulatorUrl) {
+ this.emulatorUrl = emulatorUrl;
}
}
diff --git a/plugins/tech/calcite/src/main/java/org/apache/hop/calcite/config/CalciteSqlFormatConfigPlugin.java b/plugins/tech/calcite/src/main/java/org/apache/hop/calcite/config/CalciteSqlFormatConfigPlugin.java
index e4e4a7825c9..d9fae5b9626 100644
--- a/plugins/tech/calcite/src/main/java/org/apache/hop/calcite/config/CalciteSqlFormatConfigPlugin.java
+++ b/plugins/tech/calcite/src/main/java/org/apache/hop/calcite/config/CalciteSqlFormatConfigPlugin.java
@@ -47,7 +47,9 @@
@ConfigPlugin(
id = "CalciteSqlFormatConfigPlugin",
description = "Apache Calcite SQL formatter options",
- category = ConfigPlugin.CATEGORY_CONFIG)
+ category = ConfigPlugin.CATEGORY_CONFIG,
+ configKey = CalciteSqlFormatConfig.HOP_CONFIG_KEY,
+ configClass = CalciteSqlFormatConfig.class)
@GuiPlugin(description = "i18n::CalciteSqlFormatConfigPlugin.Name")
public class CalciteSqlFormatConfigPlugin
implements IConfigOptions, IGuiPluginCompositeWidgetsListener {
diff --git a/plugins/tech/dropbox/src/main/java/org/apache/hop/vfs/dropbox/config/DropboxConfigPlugin.java b/plugins/tech/dropbox/src/main/java/org/apache/hop/vfs/dropbox/config/DropboxConfigPlugin.java
index 46a03ba14ff..ecc5adb00b7 100644
--- a/plugins/tech/dropbox/src/main/java/org/apache/hop/vfs/dropbox/config/DropboxConfigPlugin.java
+++ b/plugins/tech/dropbox/src/main/java/org/apache/hop/vfs/dropbox/config/DropboxConfigPlugin.java
@@ -39,7 +39,9 @@
@ConfigPlugin(
id = "DropboxConfigPlugin",
description = "Configuration options for Dropbox",
- category = ConfigPlugin.CATEGORY_CONFIG)
+ category = ConfigPlugin.CATEGORY_CONFIG,
+ configKey = DropboxConfigSingleton.HOP_CONFIG_DROPBOX_KEY,
+ configClass = DropboxConfig.class)
@GuiPlugin(
description = "Dropbox" // Tab label in options dialog
)
@@ -52,7 +54,10 @@ public class DropboxConfigPlugin implements IConfigOptions, IGuiPluginCompositeW
parentId = ConfigPluginOptionsTab.GUI_WIDGETS_PARENT_ID,
type = GuiElementType.TEXT,
variables = true,
- label = "Access token")
+ label = "Access token",
+ toolTip =
+ "The Dropbox access token used to authenticate. Generate one for an app in the Dropbox"
+ + " App Console; it grants whatever access that app was given.")
@CommandLine.Option(
names = {"-dbxt", "--dropbox-access-token"},
description = "The Dropbox access token to use for VFS")
diff --git a/plugins/tech/google/src/main/java/org/apache/hop/vfs/googledrive/config/GoogleDriveConfigPlugin.java b/plugins/tech/google/src/main/java/org/apache/hop/vfs/googledrive/config/GoogleDriveConfigPlugin.java
index c3206fb8d88..88a0155ad08 100644
--- a/plugins/tech/google/src/main/java/org/apache/hop/vfs/googledrive/config/GoogleDriveConfigPlugin.java
+++ b/plugins/tech/google/src/main/java/org/apache/hop/vfs/googledrive/config/GoogleDriveConfigPlugin.java
@@ -39,7 +39,9 @@
@ConfigPlugin(
id = "GoogleDriveConfigPlugin",
description = "i18n::GoogleDriveConfig.ConfigPlugin.Description",
- category = ConfigPlugin.CATEGORY_CONFIG)
+ category = ConfigPlugin.CATEGORY_CONFIG,
+ configKey = GoogleDriveConfig.HOP_CONFIG_GOOGLE_DRIVE_CONFIG_KEY,
+ configClass = GoogleDriveConfig.class)
@GuiPlugin(
description = "i18n::GoogleDriveConfig.GuiPlugin.Description" // Tab label in options dialog
)
diff --git a/plugins/tech/google/src/main/java/org/apache/hop/vfs/gs/config/GoogleCloudConfigPlugin.java b/plugins/tech/google/src/main/java/org/apache/hop/vfs/gs/config/GoogleCloudConfigPlugin.java
index cd84792bd20..5f11a9ceb65 100644
--- a/plugins/tech/google/src/main/java/org/apache/hop/vfs/gs/config/GoogleCloudConfigPlugin.java
+++ b/plugins/tech/google/src/main/java/org/apache/hop/vfs/gs/config/GoogleCloudConfigPlugin.java
@@ -44,7 +44,9 @@
@ConfigPlugin(
id = "GoogleCloudStorageConfigPlugin",
description = "Configuration options for Google Cloud",
- category = ConfigPlugin.CATEGORY_CONFIG)
+ category = ConfigPlugin.CATEGORY_CONFIG,
+ configKey = GoogleCloudConfig.HOP_CONFIG_GOOGLE_CLOUD_CONFIG_KEY,
+ configClass = GoogleCloudConfig.class)
@GuiPlugin(
description = "i18n::GoogleCloudPlugin.GuiPlugin.Description" // Tab label in options dialog
)
@@ -98,7 +100,7 @@ public class GoogleCloudConfigPlugin implements IConfigOptions, IGuiPluginCompos
variables = false,
label = "i18n::GoogleCloudPlugin.ScanFolderForLastModificationDate.Label",
toolTip = "i18n::GoogleCloudPlugin.ScanFolderForLastModificationDate.Description")
- private Boolean scanFoldersForModificationDate;
+ private Boolean scanFoldersForLastModifDate;
@GuiWidgetElement(
id = WIDGET_ID_GOOGLE_CLOUD_SERVICE_MAX_ATTEMPTS,
@@ -179,7 +181,7 @@ public class GoogleCloudConfigPlugin implements IConfigOptions, IGuiPluginCompos
variables = true,
label = "i18n::GoogleCloudPlugin.ConnectTimeout.Label",
toolTip = "i18n::GoogleCloudPlugin.ConnectTimeout.Description")
- private String connectTimeout;
+ private String connectionTimeout;
@GuiWidgetElement(
id = WIDGET_ID_GOOGLE_CLOUD_SERVICE_READ_TIMEOUT,
@@ -218,7 +220,7 @@ public static GoogleCloudConfigPlugin getInstance() {
GoogleCloudConfig config = GoogleCloudConfigSingleton.getConfig();
instance.serviceAccountKeyFile = config.getServiceAccountKeyFile();
- instance.scanFoldersForModificationDate = config.getScanFoldersForLastModifDate();
+ instance.scanFoldersForLastModifDate = config.getScanFoldersForLastModifDate();
instance.maxAttempts = config.getMaxAttempts();
instance.initialRetryDelay = config.getInitialRetryDelay();
instance.retryDelayMultiplier = config.getRetryDelayMultiplier();
@@ -227,7 +229,7 @@ public static GoogleCloudConfigPlugin getInstance() {
instance.initialRpcTimeout = config.getInitialRpcTimeout();
instance.rpcTimeoutMultiplier = config.getRpcTimeoutMultiplier();
instance.maxRpcTimeout = config.getMaxRpcTimeout();
- instance.connectTimeout = config.getConnectionTimeout();
+ instance.connectionTimeout = config.getConnectionTimeout();
instance.readTimeout = config.getReadTimeout();
instance.retryNonIdempotentOperations = config.getRetryNonIdempotentOperations();
instance.cacheTtlSeconds = config.getCacheTtlSeconds();
@@ -253,9 +255,8 @@ public boolean handleOption(
changed = true;
}
- if (scanFoldersForModificationDate != null
- && scanFoldersForModificationDate.equals(Boolean.TRUE)) {
- config.setScanFoldersForLastModifDate(scanFoldersForModificationDate);
+ if (scanFoldersForLastModifDate != null && scanFoldersForLastModifDate.equals(Boolean.TRUE)) {
+ config.setScanFoldersForLastModifDate(scanFoldersForLastModifDate);
log.logBasic(
"Google Cloud Storage service will scan folders for the last file modification time.");
changed = true;
@@ -309,9 +310,9 @@ public boolean handleOption(
changed = true;
}
- if (connectTimeout != null) {
- config.setConnectionTimeout(connectTimeout);
- log.logBasic("Google Cloud service connectTimeout set to " + connectTimeout);
+ if (connectionTimeout != null) {
+ config.setConnectionTimeout(connectionTimeout);
+ log.logBasic("Google Cloud service connectionTimeout set to " + connectionTimeout);
changed = true;
}
@@ -372,9 +373,9 @@ public void persistContents(GuiCompositeWidgets compositeWidgets) {
GoogleCloudConfigSingleton.getConfig().setServiceAccountKeyFile(serviceAccountKeyFile);
break;
case WIDGET_ID_GOOGLE_CLOUD_SERVICE_SCAN_FOLDERS_FOR_MODIF_DATE:
- scanFoldersForModificationDate = ((Button) control).getSelection();
+ scanFoldersForLastModifDate = ((Button) control).getSelection();
GoogleCloudConfigSingleton.getConfig()
- .setScanFoldersForLastModifDate(scanFoldersForModificationDate);
+ .setScanFoldersForLastModifDate(scanFoldersForLastModifDate);
break;
case WIDGET_ID_GOOGLE_CLOUD_SERVICE_MAX_ATTEMPTS:
maxAttempts = ((TextVar) control).getText();
@@ -409,8 +410,8 @@ public void persistContents(GuiCompositeWidgets compositeWidgets) {
GoogleCloudConfigSingleton.getConfig().setMaxRpcTimeout(maxRpcTimeout);
break;
case WIDGET_ID_GOOGLE_CLOUD_SERVICE_CONNECT_TIMEOUT:
- connectTimeout = ((TextVar) control).getText();
- GoogleCloudConfigSingleton.getConfig().setConnectionTimeout(connectTimeout);
+ connectionTimeout = ((TextVar) control).getText();
+ GoogleCloudConfigSingleton.getConfig().setConnectionTimeout(connectionTimeout);
break;
case WIDGET_ID_GOOGLE_CLOUD_SERVICE_READ_TIMEOUT:
readTimeout = ((TextVar) control).getText();
diff --git a/pom.xml b/pom.xml
index dfe4ab90547..61091b5fe57 100644
--- a/pom.xml
+++ b/pom.xml
@@ -298,7 +298,11 @@
maven-release-plugin
- -P=-assemblies -DskipTests
+
+ -P=-assemblies -Dconfig-doc=false -DskipTests
@@ -709,6 +713,24 @@
assemblies
+
+
+ config-doc
+
+
+ config-doc
+ !false
+
+
+
+ config-doc
+
+
diff --git a/ui/src/main/java/org/apache/hop/ui/hopgui/file/config/FileValidationConfigPlugin.java b/ui/src/main/java/org/apache/hop/ui/hopgui/file/config/FileValidationConfigPlugin.java
index 0ce254db6c2..03f720c4c2d 100644
--- a/ui/src/main/java/org/apache/hop/ui/hopgui/file/config/FileValidationConfigPlugin.java
+++ b/ui/src/main/java/org/apache/hop/ui/hopgui/file/config/FileValidationConfigPlugin.java
@@ -57,7 +57,8 @@ public class FileValidationConfigPlugin
parentId = ConfigPluginOptionsTab.GUI_WIDGETS_PARENT_ID,
type = GuiElementType.CHECKBOX,
label = "i18n::FileValidationConfigPlugin.ValidateDbConnectionsOnSave.Label",
- toolTip = "i18n::FileValidationConfigPlugin.ValidateDbConnectionsOnSave.ToolTip")
+ toolTip = "i18n::FileValidationConfigPlugin.ValidateDbConnectionsOnSave.ToolTip",
+ defaultValue = "true")
@CommandLine.Option(
names = {"--validate-db-connections-on-save"},
description =
diff --git a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/config/DatabasePerspectiveConfigPlugin.java b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/config/DatabasePerspectiveConfigPlugin.java
index 5ee9d16d9fd..ef7897ad599 100644
--- a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/config/DatabasePerspectiveConfigPlugin.java
+++ b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/config/DatabasePerspectiveConfigPlugin.java
@@ -48,7 +48,9 @@
@ConfigPlugin(
id = "DatabasePerspectiveConfigPlugin",
description = "Configuration options for the Database perspective",
- category = ConfigPlugin.CATEGORY_CONFIG)
+ category = ConfigPlugin.CATEGORY_CONFIG,
+ configKey = DatabasePerspectiveConfig.HOP_CONFIG_KEY,
+ configClass = DatabasePerspectiveConfig.class)
@GuiPlugin(description = "i18n::DatabasePerspectiveConfigPlugin.Name")
public class DatabasePerspectiveConfigPlugin
implements IConfigOptions, IGuiPluginCompositeWidgetsListener {
diff --git a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/config/ExplorerPerspectiveConfigPlugin.java b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/config/ExplorerPerspectiveConfigPlugin.java
index 699b3382f9c..b1a6206c3e1 100644
--- a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/config/ExplorerPerspectiveConfigPlugin.java
+++ b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/config/ExplorerPerspectiveConfigPlugin.java
@@ -46,7 +46,9 @@
@ConfigPlugin(
id = "ExplorerPerspectiveConfigPlugin",
description = "Configuration options for the explorer perspective",
- category = ConfigPlugin.CATEGORY_CONFIG)
+ category = ConfigPlugin.CATEGORY_CONFIG,
+ configKey = ExplorerPerspectiveConfig.HOP_CONFIG_EXPLORER_PERSPECTIVE_CONFIG_KEY,
+ configClass = ExplorerPerspectiveConfig.class)
@GuiPlugin(
description = "Explorer Perspective" // Tab label in options dialog
)
@@ -131,7 +133,8 @@ public class ExplorerPerspectiveConfigPlugin
parentId = ConfigPluginOptionsTab.GUI_WIDGETS_PARENT_ID,
type = GuiElementType.TEXT,
label = "i18n::ExplorerPerspectiveConfig.MaxUndo.Label",
- toolTip = "i18n::ExplorerPerspectiveConfig.MaxUndo.Tooltip")
+ toolTip = "i18n::ExplorerPerspectiveConfig.MaxUndo.Tooltip",
+ defaultValue = "100")
@CommandLine.Option(
names = {"-mu", "--max-undo"},
description =
diff --git a/ui/src/main/java/org/apache/hop/ui/hopgui/search/config/SearchConfigPlugin.java b/ui/src/main/java/org/apache/hop/ui/hopgui/search/config/SearchConfigPlugin.java
index 06500e19e11..5aebf7f43b6 100644
--- a/ui/src/main/java/org/apache/hop/ui/hopgui/search/config/SearchConfigPlugin.java
+++ b/ui/src/main/java/org/apache/hop/ui/hopgui/search/config/SearchConfigPlugin.java
@@ -43,7 +43,9 @@
@ConfigPlugin(
id = "SearchConfigPlugin",
description = "Configuration options for Hop GUI search",
- category = ConfigPlugin.CATEGORY_CONFIG)
+ category = ConfigPlugin.CATEGORY_CONFIG,
+ configKey = SearchConfig.HOP_CONFIG_SEARCH_KEY,
+ configClass = SearchConfig.class)
@GuiPlugin(
description = "Search" // Tab label in options dialog
)
diff --git a/ui/src/main/java/org/apache/hop/ui/hopgui/welcome/WelcomeDialogOptions.java b/ui/src/main/java/org/apache/hop/ui/hopgui/welcome/WelcomeDialogOptions.java
index 0dcbee55f74..f05432917fd 100644
--- a/ui/src/main/java/org/apache/hop/ui/hopgui/welcome/WelcomeDialogOptions.java
+++ b/ui/src/main/java/org/apache/hop/ui/hopgui/welcome/WelcomeDialogOptions.java
@@ -54,12 +54,17 @@ public class WelcomeDialogOptions implements IConfigOptions, IGuiPluginComposite
id = "NoWelcomeDialog",
parentId = ConfigPluginOptionsTab.GUI_WIDGETS_PARENT_ID,
type = GuiElementType.CHECKBOX,
- label = "i18n::WelcomeDialog.Show.Label")
+ label = "i18n::WelcomeDialog.Show.Label",
+ toolTip = "i18n::WelcomeDialog.Show.Tooltip",
+ defaultValue = "true")
private boolean welcomeDialogShowAtStartup;
public WelcomeDialogOptions() {
+ // The stored option is the negative one ("do not show"), this field is the positive one the
+ // checkbox is labelled with, so the two are inverses of each other. They used to be read and
+ // written straight across, which made ticking "show the welcome dialog" hide it.
welcomeDialogShowAtStartup =
- HopConfig.readOptionBoolean(WelcomeDialog.HOP_CONFIG_NO_SHOW_OPTION, false);
+ !HopConfig.readOptionBoolean(WelcomeDialog.HOP_CONFIG_NO_SHOW_OPTION, false);
}
private static WelcomeDialogOptions instance;
@@ -167,6 +172,6 @@ public void widgetModified(
@Override
public void persistContents(GuiCompositeWidgets compositeWidgets) {
HopConfig.getInstance()
- .saveOption(WelcomeDialog.HOP_CONFIG_NO_SHOW_OPTION, welcomeDialogShowAtStartup);
+ .saveOption(WelcomeDialog.HOP_CONFIG_NO_SHOW_OPTION, !welcomeDialogShowAtStartup);
}
}
diff --git a/ui/src/main/resources/org/apache/hop/ui/hopgui/welcome/messages/messages_en_US.properties b/ui/src/main/resources/org/apache/hop/ui/hopgui/welcome/messages/messages_en_US.properties
index a60bcb0af7e..dc4e9967d4f 100644
--- a/ui/src/main/resources/org/apache/hop/ui/hopgui/welcome/messages/messages_en_US.properties
+++ b/ui/src/main/resources/org/apache/hop/ui/hopgui/welcome/messages/messages_en_US.properties
@@ -19,3 +19,4 @@
WelcomeDialog.Description=Welcome Dialog
WelcomeDialog.Show.Label=Show welcome dialog at the startup of the Hop GUI
+WelcomeDialog.Show.Tooltip=Show the welcome dialog when Hop GUI starts. The dialog has a 'Don''t show this at startup' option of its own that sets the same thing, and the '${HOP_NO_WELCOME_DIALOG}' variable suppresses it whatever is set here.
diff --git a/ui/src/test/java/org/apache/hop/ui/hopgui/config/ConfigPluginDefaultsTest.java b/ui/src/test/java/org/apache/hop/ui/hopgui/config/ConfigPluginDefaultsTest.java
new file mode 100644
index 00000000000..80fcc5a278b
--- /dev/null
+++ b/ui/src/test/java/org/apache/hop/ui/hopgui/config/ConfigPluginDefaultsTest.java
@@ -0,0 +1,149 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.hop.ui.hopgui.config;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.DynamicTest.dynamicTest;
+
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.hop.core.Const;
+import org.apache.hop.core.config.HopConfig;
+import org.apache.hop.core.gui.plugin.GuiWidgetElement;
+import org.apache.hop.ui.hopgui.file.config.FileValidationConfigPlugin;
+import org.apache.hop.ui.hopgui.perspective.explorer.config.ExplorerPerspectiveConfigPlugin;
+import org.apache.hop.ui.hopgui.welcome.WelcomeDialog;
+import org.apache.hop.ui.hopgui.welcome.WelcomeDialogOptions;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.DynamicTest;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestFactory;
+
+/**
+ * The hop-ui config plugins that keep a documented default in {@code
+ * GuiWidgetElement#defaultValue()} rather than on a {@code configClass()}, checked against what
+ * they really fall back to. See {@code LinterConfigPluginDefaultsTest} for why those two ways of
+ * holding a default exist and why the annotated one needs guarding.
+ */
+class ConfigPluginDefaultsTest {
+
+ /** Plugins whose no-argument constructor loads every option from {@link HopConfig}. */
+ private static final List> SELF_LOADING =
+ List.of(FileValidationConfigPlugin.class, WelcomeDialogOptions.class);
+
+ private Map savedConfig;
+
+ @AfterEach
+ void restoreConfiguration() {
+ if (savedConfig != null) {
+ HopConfig.getInstance().getConfigMap().clear();
+ HopConfig.getInstance().getConfigMap().putAll(savedConfig);
+ savedConfig = null;
+ }
+ }
+
+ @TestFactory
+ List everyDeclaredDefaultIsTheRealDefault() throws Exception {
+ // An unconfigured Hop: every readOption... call now falls through to its inline default.
+ savedConfig = new HashMap<>(HopConfig.getInstance().getConfigMap());
+ HopConfig.getInstance().getConfigMap().clear();
+
+ List tests = new ArrayList<>();
+ for (Class> pluginClass : SELF_LOADING) {
+ Object unconfigured = pluginClass.getDeclaredConstructor().newInstance();
+ for (Field field : pluginClass.getDeclaredFields()) {
+ GuiWidgetElement widget = field.getAnnotation(GuiWidgetElement.class);
+ if (widget == null || widget.defaultValue().isEmpty()) {
+ continue;
+ }
+ String declared = widget.defaultValue();
+ tests.add(
+ dynamicTest(
+ pluginClass.getSimpleName() + "." + field.getName(),
+ () -> {
+ field.setAccessible(true);
+ Object actual = field.get(unconfigured);
+ assertEquals(
+ declared,
+ String.valueOf(actual),
+ "@GuiWidgetElement(defaultValue) on "
+ + pluginClass.getSimpleName()
+ + "."
+ + field.getName()
+ + " says the option defaults to '"
+ + declared
+ + "', but with nothing configured it loads as '"
+ + actual
+ + "'. The documentation is generated from the annotation, so either the"
+ + " fallback at the readOption call site or this annotation is wrong.");
+ }));
+ }
+ }
+ assertFalse(
+ tests.isEmpty(),
+ "No option declares a defaultValue, so the cases below would pass without testing anything");
+ return tests;
+ }
+
+ /**
+ * The checkbox says "show", the stored option says "do not show". The constructor's inversion is
+ * covered above; this covers the write.
+ */
+ @Test
+ void theWelcomeDialogOptionIsStoredInverted() throws Exception {
+ savedConfig = new HashMap<>(HopConfig.getInstance().getConfigMap());
+ HopConfig hopConfig = HopConfig.getInstance();
+ boolean savedInMemory = HopConfig.isInMemoryMode();
+ try {
+ // Keep the writes off the developer's hop-config.json.
+ HopConfig.setInMemoryMode(true);
+ WelcomeDialogOptions options = new WelcomeDialogOptions();
+
+ options.setWelcomeDialogShowAtStartup(true);
+ options.persistContents(null);
+ assertEquals(
+ Boolean.FALSE, hopConfig.getConfigMap().get(WelcomeDialog.HOP_CONFIG_NO_SHOW_OPTION));
+
+ options.setWelcomeDialogShowAtStartup(false);
+ options.persistContents(null);
+ assertEquals(
+ Boolean.TRUE, hopConfig.getConfigMap().get(WelcomeDialog.HOP_CONFIG_NO_SHOW_OPTION));
+ } finally {
+ HopConfig.setInMemoryMode(savedInMemory);
+ }
+ }
+
+ /**
+ * The explorer perspective reads its undo limit from {@code PropsUi}, which needs a display, so
+ * it is pinned to the constant that feeds it instead of to a loaded instance.
+ */
+ @Test
+ void theUndoLimitDefaultMatchesTheConstantBehindIt() throws Exception {
+ Field field = ExplorerPerspectiveConfigPlugin.class.getDeclaredField("maxUndo");
+ String declared = field.getAnnotation(GuiWidgetElement.class).defaultValue();
+ assertEquals(
+ Integer.toString(Const.MAX_UNDO),
+ declared,
+ "@GuiWidgetElement(defaultValue) on ExplorerPerspectiveConfigPlugin.maxUndo has to keep up"
+ + " with Const.MAX_UNDO, which is what PropsUi.getMaxUndo() falls back to");
+ }
+}