174 lines
8.8 KiB
Java
174 lines
8.8 KiB
Java
/*
|
|
* QQQ - Low-code Application Framework for Engineers.
|
|
* Copyright (C) 2021-2025. Kingsrook, LLC
|
|
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
|
* contact@kingsrook.com
|
|
* https://github.com/Kingsrook/
|
|
*
|
|
* 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
|
|
*
|
|
* https://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.dataanalysis.app;
|
|
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.List;
|
|
import com.kingsrook.qbits.dataanalysis.DataAnalysisQBitConfig;
|
|
import com.kingsrook.qbits.dataanalysis.DataAnalysisRuntime;
|
|
import com.kingsrook.qbits.dataanalysis.model.DataAnalysisTables;
|
|
import com.kingsrook.qbits.dataanalysis.processes.TableConfigDefaultsProcess;
|
|
import com.kingsrook.qqq.backend.core.context.QContext;
|
|
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
|
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
|
|
|
|
|
/*******************************************************************************
|
|
** The Setup card on the Analyses home: each configuration or content table,
|
|
** whether it is set up, and the next step. Reads settings through the same
|
|
** resolver the analysis runtime uses (code defaults, then rows), so the card
|
|
** never reports something the runtime would not do. Counts run as the viewer.
|
|
*******************************************************************************/
|
|
public final class DataAnalysisSetupStatus
|
|
{
|
|
/*******************************************************************************
|
|
** OK: set up. DEFAULT: working on built-in defaults (nothing required).
|
|
** OPTIONAL: a feature that stays off until someone uses it. UNAVAILABLE: the
|
|
** viewer cannot read the table.
|
|
*******************************************************************************/
|
|
public enum State
|
|
{
|
|
OK, DEFAULT, OPTIONAL, UNAVAILABLE, UNKNOWN
|
|
}
|
|
|
|
|
|
|
|
/*******************************************************************************
|
|
** One line of the card.
|
|
*******************************************************************************/
|
|
public record Item(String label, String tableName, State state, String detail, String nextStep)
|
|
{
|
|
}
|
|
|
|
|
|
|
|
/*******************************************************************************
|
|
** Utility class.
|
|
*******************************************************************************/
|
|
private DataAnalysisSetupStatus()
|
|
{
|
|
}
|
|
|
|
|
|
|
|
/*******************************************************************************
|
|
** The card's lines, in the order a person would set things up.
|
|
*******************************************************************************/
|
|
public static List<Item> load() throws QException
|
|
{
|
|
DataAnalysisQBitConfig config = DataAnalysisRuntime.service().getConfigResolver().getDefaults();
|
|
List<Item> items = new ArrayList<>();
|
|
|
|
///////////////////////
|
|
// analysis settings //
|
|
///////////////////////
|
|
List<String> analyzable = TableConfigDefaultsProcess.analyzableTables(QContext.getQInstance(), config);
|
|
if(!DataAnalysisHome.canRead(DataAnalysisTables.TABLE_CONFIG))
|
|
{
|
|
items.add(new Item("Analysis settings", DataAnalysisTables.TABLE_CONFIG, State.UNAVAILABLE, "Unavailable: READ permission required.", null));
|
|
}
|
|
else
|
|
{
|
|
int rows = DataAnalysisHome.count(DataAnalysisTables.TABLE_CONFIG, new QQueryFilter());
|
|
String defaults = "max rows " + config.getDefaultMaxRows() + ", sample " + config.getDefaultSampleRows() + " above " + config.getDefaultSampleThresholdRows()
|
|
+ ", smallest group " + config.getDefaultMinGroupSize() + ", results reused " + config.getDefaultCacheTtlMinutes() + " min";
|
|
if(rows == 0)
|
|
{
|
|
items.add(new Item("Analysis settings", DataAnalysisTables.TABLE_CONFIG, State.DEFAULT,
|
|
"All " + analyzable.size() + " analyzable tables are on, with the defaults (" + defaults + ").",
|
|
"Run Create rows for analyzable tables to see and edit them, or add a row to change one table."));
|
|
}
|
|
else
|
|
{
|
|
items.add(new Item("Analysis settings", DataAnalysisTables.TABLE_CONFIG, rows >= analyzable.size() ? State.OK : State.DEFAULT,
|
|
rows + " settings row" + (rows == 1 ? "" : "s") + " for " + analyzable.size() + " analyzable tables; tables without a row use the defaults (" + defaults + ").",
|
|
rows >= analyzable.size() ? null : "Run Create rows for analyzable tables to add rows for the rest."));
|
|
}
|
|
}
|
|
|
|
/////////////////////////////////////////////////////////////////////
|
|
// result cache: the resolved value, exactly what caching will use //
|
|
/////////////////////////////////////////////////////////////////////
|
|
items.add(cacheItem(analyzable, config));
|
|
|
|
///////////////////////////////////////////////////
|
|
// saved analyses, watches, schedules: optional //
|
|
///////////////////////////////////////////////////
|
|
items.add(optional("Saved analyses", DataAnalysisTables.SAVED, "No saved analyses yet.", "Save an analysis from the Analyze screen, then star or share it."));
|
|
String scheduler = config.getSchedulerName() == null ? " No scheduler is configured, so watches only run with Evaluate now." : "";
|
|
items.add(optional("Watches", DataAnalysisTables.WATCH, "No watches yet." + scheduler, "Add a watch on a saved analysis to be alerted to spikes, drops and new categories."));
|
|
String schedules = config.getSchedulerName() == null ? " No scheduler is configured, so schedules do not run." : "";
|
|
items.add(optional("Schedules", DataAnalysisTables.SCHEDULE, "No schedules yet." + schedules, "Add a schedule on a saved analysis to send it by email."));
|
|
return (items);
|
|
}
|
|
|
|
|
|
|
|
/*******************************************************************************
|
|
** One optional feature: set up once it has rows.
|
|
*******************************************************************************/
|
|
private static Item optional(String label, String table, String emptyDetail, String nextStep) throws QException
|
|
{
|
|
if(!DataAnalysisHome.canRead(table))
|
|
{
|
|
return (new Item(label, table, State.UNAVAILABLE, "Unavailable: READ permission required.", null));
|
|
}
|
|
int rows = DataAnalysisHome.count(table, new QQueryFilter());
|
|
return (rows == 0 ? new Item(label, table, State.OPTIONAL, emptyDetail, nextStep) : new Item(label, table, State.OK, rows + " in use.", null));
|
|
}
|
|
|
|
|
|
|
|
/*******************************************************************************
|
|
** The Result cache line, from the minutes every analyzable table actually
|
|
** resolves to (settings row, then table defaults, then the global default).
|
|
*******************************************************************************/
|
|
private static Item cacheItem(List<String> analyzable, DataAnalysisQBitConfig config)
|
|
{
|
|
java.util.TreeMap<Integer, Integer> tablesPerMinutes = new java.util.TreeMap<>();
|
|
for(String table : analyzable)
|
|
{
|
|
try
|
|
{
|
|
tablesPerMinutes.merge(DataAnalysisRuntime.service().getConfigResolver().resolve(table).getCacheTtlMinutes(), 1, Integer::sum);
|
|
}
|
|
catch(QException e)
|
|
{
|
|
return (new Item("Result cache", DataAnalysisTables.RUN, State.UNKNOWN, "The cache settings of " + table + " could not be read: " + e.getMessage(),
|
|
"Fix the settings row for that table (a negative limit is invalid)."));
|
|
}
|
|
}
|
|
if(tablesPerMinutes.isEmpty() || (tablesPerMinutes.size() == 1 && tablesPerMinutes.firstKey() == 0))
|
|
{
|
|
return (new Item("Result cache", DataAnalysisTables.RUN, State.DEFAULT, "Off: every exact run computes fresh.", "Set cache minutes on Analysis Settings to reuse results."));
|
|
}
|
|
if(tablesPerMinutes.size() == 1)
|
|
{
|
|
return (new Item("Result cache", DataAnalysisTables.RUN, State.OK, "Exact results are reused for " + tablesPerMinutes.firstKey() + " minutes.", null));
|
|
}
|
|
String parts = tablesPerMinutes.entrySet().stream().map(e -> e.getValue() + (e.getValue() == 1 ? " table" : " tables") + (e.getKey() == 0 ? ": off" : ": " + e.getKey() + " min"))
|
|
.collect(java.util.stream.Collectors.joining(", "));
|
|
return (new Item("Result cache", DataAnalysisTables.RUN, State.OK, "Exact results are reused per table (default " + config.getDefaultCacheTtlMinutes() + " min): " + parts + ".", null));
|
|
}
|
|
|
|
}
|