Files
qbit-quick-search/src/main/java/com/kingsrook/qbits/quicksearch/metadata/QuickSearchConfigProcessMetaDataHelper.java
T
james.maesandClaude Opus 5.5 ee76e1e3c7 fix: record screens of the Quick Search tables, process inputs in Next
- quick_search_index_run and quick_search_failed_event are system
  history: Create and Edit are off (Delete stays), with named sections,
  record labels and pickers (run type and status, event status and
  action, and the table picker for failed events' tableName).
- The index record lists its Recent Runs (read-only child list).
- quick_search_validation_run.reportJson uses a JSON code editor.
- Basepull, Full Reindex and Reconcile are table actions on Quick Search
  Index, not app (nav) children. Their table input is searchTableName
  (tableName still accepted; a Quick Search table name is ignored), so
  running them from the table no longer limits them to quick_search_index.
- Process input steps carry HELP_TEXT and EDIT_FORM components, so the
  Next UI renders their fields; Add Search Index's table input is
  searchTableName for the same reason.
- The Search Diagnostics widget shows the pause, priming and pending
  changes as banners; screenshots under docs/images/data-driven-config.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 15:55:24 -05:00

275 lines
16 KiB
Java

/*
* Copyright 2026 Kingsrook, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qbits.quicksearch.metadata;
import java.util.List;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QComponentType;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendComponentMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.AbstractExtractStep;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.AbstractLoadStep;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.AbstractTransformStep;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.ExtractViaQueryStep;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.StreamedETLWithFrontendProcess;
import com.kingsrook.qbits.quicksearch.QuickSearchQBitConfig;
import com.kingsrook.qbits.quicksearch.model.QuickSearchDocumentsChoice;
import com.kingsrook.qbits.quicksearch.model.QuickSearchIndex;
import com.kingsrook.qbits.quicksearch.model.QuickSearchIndexRun;
import com.kingsrook.qbits.quicksearch.model.QuickSearchSettings;
import com.kingsrook.qbits.quicksearch.processes.config.AddSearchIndexSteps;
import com.kingsrook.qbits.quicksearch.processes.config.ApplyIndexChangesSteps;
import com.kingsrook.qbits.quicksearch.processes.config.CancelRunSteps;
import com.kingsrook.qbits.quicksearch.processes.config.SetIndexingPausedSteps;
import com.kingsrook.qbits.quicksearch.processes.config.StopIndexingSteps;
/*******************************************************************************
** The config processes. Each is a standard streamed-ETL process with a
** review step (the transform's summary: what will happen, counts, problems)
** and a result step, run from its table's Actions (never from the nav):
** - Add Search Index, on quick_search_index (no selection needed);
** - Disable / Remove Search Index and Apply Search Index Changes, on the
** selected quick_search_index rows;
** - Cancel Index Run, on the selected quick_search_index_run rows;
** - Pause / Resume Indexing, on the quick_search_settings row.
** All need the admin permission (quick_search_admin by default).
*******************************************************************************/
public class QuickSearchConfigProcessMetaDataHelper
{
public static final String ADD_INDEX_PROCESS_NAME = "quickSearchAddIndex";
public static final String DISABLE_INDEX_PROCESS_NAME = "quickSearchDisableIndex";
public static final String REMOVE_INDEX_PROCESS_NAME = "quickSearchRemoveIndex";
public static final String APPLY_CHANGES_PROCESS_NAME = "quickSearchApplyIndexChanges";
public static final String CANCEL_RUN_PROCESS_NAME = "quickSearchCancelRun";
public static final String PAUSE_PROCESS_NAME = "quickSearchPauseIndexing";
public static final String RESUME_PROCESS_NAME = "quickSearchResumeIndexing";
/*******************************************************************************
** Every config process.
*******************************************************************************/
public static List<QProcessMetaData> buildProcesses(QuickSearchQBitConfig config)
{
return (List.of(
buildAddIndexProcess(config),
buildStopIndexingProcess(config, DISABLE_INDEX_PROCESS_NAME, "Disable Search Index", "toggle_off", StopIndexingSteps.MODE_DISABLE, QuickSearchDocumentsChoice.KEEP),
buildStopIndexingProcess(config, REMOVE_INDEX_PROCESS_NAME, "Remove Search Index", "delete", StopIndexingSteps.MODE_REMOVE, QuickSearchDocumentsChoice.DELETE),
buildApplyChangesProcess(config),
buildCancelRunProcess(config),
buildSetPausedProcess(config, PAUSE_PROCESS_NAME, "Pause Indexing", "pause_circle", true),
buildSetPausedProcess(config, RESUME_PROCESS_NAME, "Resume Indexing", "play_circle", false)));
}
/*******************************************************************************
** Add Search Index: the valid way to add a table.
*******************************************************************************/
public static QProcessMetaData buildAddIndexProcess(QuickSearchQBitConfig config)
{
String indexTable = config.applyTablePrefix(QuickSearchIndex.TABLE_NAME);
QProcessMetaData process = base(config, ADD_INDEX_PROCESS_NAME, "Add Search Index", "add_circle", indexTable,
AddSearchIndexSteps.Extract.class, AddSearchIndexSteps.Transform.class, AddSearchIndexSteps.Load.class, 0, List.of(
new QFieldMetaData(AddSearchIndexSteps.RECORD_TABLE, QFieldType.STRING).withLabel("Table")
.withPossibleValueSourceName(config.applyTablePrefix(QuickSearchSearchableTablePossibleValueSourceProducer.NAME)),
new QFieldMetaData("estimatedRecords", QFieldType.INTEGER).withLabel("Records to Index"),
new QFieldMetaData("fieldCount", QFieldType.INTEGER).withLabel("Fields"),
new QFieldMetaData(AddSearchIndexSteps.FIELD_INTERVAL, QFieldType.INTEGER).withLabel("Update Interval (Minutes)"),
new QFieldMetaData(AddSearchIndexSteps.FIELD_TIMESTAMP, QFieldType.STRING).withLabel("Timestamp Field"),
new QFieldMetaData(AddSearchIndexSteps.FIELD_ENABLED, QFieldType.BOOLEAN)));
process.withStep(0, new QFrontendStepMetaData()
.withName("input")
.withLabel("Choose a table")
.withComponent(new QFrontendComponentMetaData().withType(QComponentType.HELP_TEXT).withValue("text",
"Only tables you can read, that are not indexed yet and are not internal or secret are offered. The review step shows the fields the index starts with "
+ "and how many records its first build indexes; change the fields afterwards under Searchable Fields on the index."))
.withComponent(new QFrontendComponentMetaData().withType(QComponentType.EDIT_FORM))
.withFormField(new QFieldMetaData(AddSearchIndexSteps.FIELD_TABLE_NAME, QFieldType.STRING).withLabel("Table").withIsRequired(true)
.withPossibleValueSourceName(config.applyTablePrefix(QuickSearchConfigMetaDataHelper.ELIGIBLE_TABLE_PVS)))
.withFormField(new QFieldMetaData(AddSearchIndexSteps.FIELD_INTERVAL, QFieldType.INTEGER).withLabel("Update Interval (Minutes)")
.withDefaultValue(config.getDefaultBasepullIntervalMinutes()))
.withFormField(new QFieldMetaData(AddSearchIndexSteps.FIELD_TIMESTAMP, QFieldType.STRING).withLabel("Timestamp Field (blank: detect)")
.withPossibleValueSourceName(config.applyTablePrefix(QuickSearchConfigMetaDataHelper.TIMESTAMP_FIELD_PVS)))
.withFormField(new QFieldMetaData(AddSearchIndexSteps.FIELD_ENABLED, QFieldType.BOOLEAN).withLabel("Enabled").withDefaultValue(true)));
return (process);
}
/*******************************************************************************
** Disable or Remove Search Index, with the documents choice.
*******************************************************************************/
static QProcessMetaData buildStopIndexingProcess(QuickSearchQBitConfig config, String name, String label, String icon, String mode, QuickSearchDocumentsChoice defaultChoice)
{
String indexTable = config.applyTablePrefix(QuickSearchIndex.TABLE_NAME);
QProcessMetaData process = StreamedETLWithFrontendProcess.processMetaDataBuilder()
.withName(config.applyPrefix(name))
.withLabel(label)
.withIcon(new QIcon(icon))
.withTableName(indexTable)
.withSourceTable(indexTable)
.withDestinationTable(indexTable)
.withExtractStepClass(ExtractViaQueryStep.class)
.withTransformStepClass(StopIndexingSteps.Transform.class)
.withLoadStepClass(StopIndexingSteps.Load.class)
.withSupportsFullValidation(true)
.withDoFullValidation(true)
.withTransactionLevelAutoCommit()
.withMinInputRecords(1)
.withFields(List.of(new QFieldMetaData(StopIndexingSteps.FIELD_MODE, QFieldType.STRING).withDefaultValue(mode)))
.withReviewStepRecordFields(indexReviewFields(config))
.getProcessMetaData();
process.withPermissionRules(config.getAdminPermissionRules() == null ? null : config.getAdminPermissionRules().clone());
process.withStep(0, new QFrontendStepMetaData()
.withName("input")
.withLabel("Search documents")
.withComponent(new QFrontendComponentMetaData().withType(QComponentType.HELP_TEXT).withValue("text",
StopIndexingSteps.MODE_REMOVE.equals(mode)
? "Removing stops indexing the table and deletes its index and field rows. Code defaults never add it back (see Quick Search Removed Config)."
: "Disabling stops indexing the table and hides it from search; enabling it again rebuilds it."))
.withComponent(new QFrontendComponentMetaData().withType(QComponentType.EDIT_FORM))
.withFormField(new QFieldMetaData(StopIndexingSteps.FIELD_DOCUMENTS, QFieldType.STRING).withLabel("Search Documents").withIsRequired(true)
.withDefaultValue(defaultChoice.name())
.withPossibleValueSourceName(config.applyTablePrefix(QuickSearchConfigMetaDataHelper.DOCUMENTS_CHOICE_PVS))));
return (process);
}
/*******************************************************************************
** Apply Search Index Changes: preview field changes, then rebuild.
*******************************************************************************/
static QProcessMetaData buildApplyChangesProcess(QuickSearchQBitConfig config)
{
String indexTable = config.applyTablePrefix(QuickSearchIndex.TABLE_NAME);
return (base(config, APPLY_CHANGES_PROCESS_NAME, "Apply Search Index Changes", "published_with_changes", indexTable,
ExtractViaQueryStep.class, ApplyIndexChangesSteps.Transform.class, ApplyIndexChangesSteps.Load.class, 1, indexReviewFields(config)));
}
/*******************************************************************************
** Cancel Index Run: the cooperative cancel flag.
*******************************************************************************/
static QProcessMetaData buildCancelRunProcess(QuickSearchQBitConfig config)
{
String runTable = config.applyTablePrefix(QuickSearchIndexRun.TABLE_NAME);
return (base(config, CANCEL_RUN_PROCESS_NAME, "Cancel Index Run", "cancel", runTable,
ExtractViaQueryStep.class, CancelRunSteps.Transform.class, CancelRunSteps.Load.class, 1, List.of(
new QFieldMetaData("id", QFieldType.INTEGER),
new QFieldMetaData("quickSearchIndexId", QFieldType.INTEGER).withLabel("Search Index").withPossibleValueSourceName(config.applyTablePrefix(QuickSearchIndex.TABLE_NAME)),
new QFieldMetaData("runType", QFieldType.STRING),
new QFieldMetaData("status", QFieldType.STRING),
new QFieldMetaData("recordsProcessed", QFieldType.INTEGER))));
}
/*******************************************************************************
** Pause or Resume Indexing.
*******************************************************************************/
static QProcessMetaData buildSetPausedProcess(QuickSearchQBitConfig config, String name, String label, String icon, boolean pause)
{
String settingsTable = config.applyTablePrefix(QuickSearchSettings.TABLE_NAME);
QProcessMetaData process = StreamedETLWithFrontendProcess.processMetaDataBuilder()
.withName(config.applyPrefix(name))
.withLabel(label)
.withIcon(new QIcon(icon))
.withTableName(settingsTable)
.withSourceTable(settingsTable)
.withDestinationTable(settingsTable)
.withExtractStepClass(ExtractViaQueryStep.class)
.withTransformStepClass(SetIndexingPausedSteps.Transform.class)
.withLoadStepClass(SetIndexingPausedSteps.Load.class)
.withSupportsFullValidation(true)
.withDoFullValidation(true)
.withTransactionLevelAutoCommit()
.withMinInputRecords(1)
.withFields(List.of(new QFieldMetaData(SetIndexingPausedSteps.FIELD_PAUSE, QFieldType.BOOLEAN).withDefaultValue(pause)))
.withReviewStepRecordFields(List.of(
new QFieldMetaData("indexingPaused", QFieldType.BOOLEAN).withLabel("Indexing Paused"),
new QFieldMetaData("pausedAt", QFieldType.DATE_TIME).withLabel("Paused Since"),
new QFieldMetaData("pausedBy", QFieldType.STRING).withLabel("Paused By")))
.getProcessMetaData();
process.withPermissionRules(config.getAdminPermissionRules() == null ? null : config.getAdminPermissionRules().clone());
if(pause)
{
process.withStep(0, new QFrontendStepMetaData()
.withName("input")
.withComponent(new QFrontendComponentMetaData().withType(QComponentType.EDIT_FORM))
.withLabel("Pause indexing")
.withFormField(new QFieldMetaData(SetIndexingPausedSteps.FIELD_REASON, QFieldType.STRING).withLabel("Reason (optional)")));
}
return (process);
}
/*******************************************************************************
** The index fields shown on the review step.
*******************************************************************************/
private static List<QFieldMetaData> indexReviewFields(QuickSearchQBitConfig config)
{
return (List.of(
new QFieldMetaData("id", QFieldType.INTEGER),
new QFieldMetaData("tableName", QFieldType.STRING).withLabel("Table")
.withPossibleValueSourceName(config.applyTablePrefix(QuickSearchSearchableTablePossibleValueSourceProducer.NAME)),
new QFieldMetaData("enabled", QFieldType.BOOLEAN),
new QFieldMetaData("status", QFieldType.STRING),
new QFieldMetaData("documentCount", QFieldType.INTEGER)));
}
/*******************************************************************************
** A streamed-ETL process with a validated review step, gated by the admin
** permission.
*******************************************************************************/
private static QProcessMetaData base(QuickSearchQBitConfig config, String name, String label, String icon, String tableName, Class<? extends AbstractExtractStep> extract,
Class<? extends AbstractTransformStep> transform, Class<? extends AbstractLoadStep> load, Integer minInputRecords, List<QFieldMetaData> reviewFields)
{
QProcessMetaData process = StreamedETLWithFrontendProcess.processMetaDataBuilder()
.withName(config.applyPrefix(name))
.withLabel(label)
.withIcon(new QIcon(icon))
.withTableName(tableName)
.withSourceTable(tableName)
.withDestinationTable(tableName)
.withExtractStepClass(extract)
.withTransformStepClass(transform)
.withLoadStepClass(load)
.withSupportsFullValidation(true)
.withDoFullValidation(true)
.withTransactionLevelAutoCommit()
.withMinInputRecords(minInputRecords)
.withReviewStepRecordFields(reviewFields)
.getProcessMetaData();
process.withPermissionRules(config.getAdminPermissionRules() == null ? null : config.getAdminPermissionRules().clone());
return (process);
}
}