Merge pull request #39 from LD-Reborn/28-create-a-front-end---recent-queries
28 create a front end recent queries
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
using System.Text.Json;
|
||||
using ElmahCore;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Server.Exceptions;
|
||||
using Server.Helper;
|
||||
using Shared.Models;
|
||||
|
||||
namespace Server.Controllers;
|
||||
@@ -98,14 +100,39 @@ public class SearchdomainController : ControllerBase
|
||||
{
|
||||
_logger.LogError("Unable to update searchdomain {searchdomain} - not found", [searchdomain]);
|
||||
return Ok(new SearchdomainUpdateResults() { Success = false, Message = $"Unable to update searchdomain {searchdomain} - not found" });
|
||||
} catch (Exception)
|
||||
} catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Unable to update searchdomain {searchdomain}", [searchdomain]);
|
||||
_logger.LogError("Unable to update searchdomain {searchdomain} - Exception: {ex.Message} - {ex.StackTrace}", [searchdomain, ex.Message, ex.StackTrace]);
|
||||
return Ok(new SearchdomainUpdateResults() { Success = false, Message = $"Unable to update searchdomain {searchdomain}" });
|
||||
}
|
||||
return Ok(new SearchdomainUpdateResults(){Success = true});
|
||||
}
|
||||
|
||||
[HttpPost("UpdateSettings")]
|
||||
public ActionResult<SearchdomainUpdateResults> UpdateSettings(string searchdomain, [FromBody] SearchdomainSettings request)
|
||||
{
|
||||
try
|
||||
{
|
||||
Searchdomain searchdomain_ = _domainManager.GetSearchdomain(searchdomain);
|
||||
Dictionary<string, dynamic> parameters = new()
|
||||
{
|
||||
{"settings", JsonSerializer.Serialize(request)},
|
||||
{"id", searchdomain_.id}
|
||||
};
|
||||
searchdomain_.helper.ExecuteSQLNonQuery("UPDATE searchdomain set settings = @settings WHERE id = @id", parameters);
|
||||
searchdomain_.settings = request;
|
||||
} catch (SearchdomainNotFoundException)
|
||||
{
|
||||
_logger.LogError("Unable to update settings for searchdomain {searchdomain} - not found", [searchdomain]);
|
||||
return Ok(new SearchdomainUpdateResults() { Success = false, Message = $"Unable to update settings for searchdomain {searchdomain} - not found" });
|
||||
} catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Unable to update settings for searchdomain {searchdomain} - Exception: {ex.Message} - {ex.StackTrace}", [searchdomain, ex.Message, ex.StackTrace]);
|
||||
return Ok(new SearchdomainUpdateResults() { Success = false, Message = $"Unable to update settings for searchdomain {searchdomain}" });
|
||||
}
|
||||
return Ok(new SearchdomainUpdateResults(){Success = true});
|
||||
}
|
||||
|
||||
[HttpGet("GetSearches")]
|
||||
public ActionResult<SearchdomainSearchesResults> GetSearches(string searchdomain)
|
||||
{
|
||||
@@ -128,4 +155,96 @@ public class SearchdomainController : ControllerBase
|
||||
|
||||
return Ok(new SearchdomainSearchesResults() { Searches = searchCache, Success = true });
|
||||
}
|
||||
|
||||
[HttpGet("GetSettings")]
|
||||
public ActionResult<SearchdomainSettingsResults> GetSettings(string searchdomain)
|
||||
{
|
||||
Searchdomain searchdomain_;
|
||||
try
|
||||
{
|
||||
searchdomain_ = _domainManager.GetSearchdomain(searchdomain);
|
||||
}
|
||||
catch (SearchdomainNotFoundException)
|
||||
{
|
||||
_logger.LogError("Unable to retrieve the searchdomain {searchdomain} - it likely does not exist yet", [searchdomain]);
|
||||
return Ok(new SearchdomainSettingsResults() { Settings = null, Success = false, Message = "Searchdomain not found" });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Unable to retrieve the searchdomain {searchdomain} - {ex.Message} - {ex.StackTrace}", [searchdomain, ex.Message, ex.StackTrace]);
|
||||
return Ok(new SearchdomainSettingsResults() { Settings = null, Success = false, Message = ex.Message });
|
||||
}
|
||||
SearchdomainSettings settings = searchdomain_.settings;
|
||||
return Ok(new SearchdomainSettingsResults() { Settings = settings, Success = true });
|
||||
}
|
||||
|
||||
[HttpGet("GetSearchCacheSize")]
|
||||
public ActionResult<SearchdomainSearchCacheSizeResults> GetSearchCacheSize(string searchdomain)
|
||||
{
|
||||
Searchdomain searchdomain_;
|
||||
try
|
||||
{
|
||||
searchdomain_ = _domainManager.GetSearchdomain(searchdomain);
|
||||
}
|
||||
catch (SearchdomainNotFoundException)
|
||||
{
|
||||
_logger.LogError("Unable to retrieve the searchdomain {searchdomain} - it likely does not exist yet", [searchdomain]);
|
||||
return Ok(new SearchdomainSearchCacheSizeResults() { SearchCacheSizeBytes = null, Success = false, Message = "Searchdomain not found" });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Unable to retrieve the searchdomain {searchdomain} - {ex.Message} - {ex.StackTrace}", [searchdomain, ex.Message, ex.StackTrace]);
|
||||
return Ok(new SearchdomainSearchCacheSizeResults() { SearchCacheSizeBytes = null, Success = false, Message = ex.Message });
|
||||
}
|
||||
Dictionary<string, DateTimedSearchResult> searchCache = searchdomain_.searchCache;
|
||||
long sizeInBytes = 0;
|
||||
foreach (var entry in searchCache)
|
||||
{
|
||||
sizeInBytes += sizeof(int); // string length prefix
|
||||
sizeInBytes += entry.Key.Length * sizeof(char); // string characters
|
||||
sizeInBytes += entry.Value.EstimateSize();
|
||||
}
|
||||
return Ok(new SearchdomainSearchCacheSizeResults() { SearchCacheSizeBytes = sizeInBytes, Success = true });
|
||||
}
|
||||
|
||||
[HttpGet("ClearSearchCache")]
|
||||
public ActionResult<SearchdomainInvalidateCacheResults> InvalidateSearchCache(string searchdomain)
|
||||
{
|
||||
try
|
||||
{
|
||||
Searchdomain searchdomain_ = _domainManager.GetSearchdomain(searchdomain);
|
||||
searchdomain_.InvalidateSearchCache();
|
||||
} catch (SearchdomainNotFoundException)
|
||||
{
|
||||
_logger.LogError("Unable to invalidate search cache for searchdomain {searchdomain} - not found", [searchdomain]);
|
||||
return Ok(new SearchdomainInvalidateCacheResults() { Success = false, Message = $"Unable to invalidate search cache for searchdomain {searchdomain} - not found" });
|
||||
} catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Unable to invalidate search cache for searchdomain {searchdomain} - Exception: {ex.Message} - {ex.StackTrace}", [searchdomain, ex.Message, ex.StackTrace]);
|
||||
return Ok(new SearchdomainInvalidateCacheResults() { Success = false, Message = $"Unable to invalidate search cache for searchdomain {searchdomain}" });
|
||||
}
|
||||
return Ok(new SearchdomainInvalidateCacheResults(){Success = true});
|
||||
}
|
||||
|
||||
[HttpGet("GetDatabaseSize")]
|
||||
public ActionResult<SearchdomainGetDatabaseSizeResult> GetDatabaseSize(string searchdomain)
|
||||
{
|
||||
Searchdomain searchdomain_;
|
||||
try
|
||||
{
|
||||
searchdomain_ = _domainManager.GetSearchdomain(searchdomain);
|
||||
}
|
||||
catch (SearchdomainNotFoundException)
|
||||
{
|
||||
_logger.LogError("Unable to retrieve the searchdomain {searchdomain} - it likely does not exist yet", [searchdomain]);
|
||||
return Ok(new SearchdomainGetDatabaseSizeResult() { SearchdomainDatabaseSizeBytes = null, Success = false, Message = "Searchdomain not found" });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Unable to retrieve the searchdomain {searchdomain} - {ex.Message} - {ex.StackTrace}", [searchdomain, ex.Message, ex.StackTrace]);
|
||||
return Ok(new SearchdomainGetDatabaseSizeResult() { SearchdomainDatabaseSizeBytes = null, Success = false, Message = ex.Message });
|
||||
}
|
||||
long sizeInBytes = DatabaseHelper.GetSearchdomainDatabaseSize(searchdomain_.helper, searchdomain);
|
||||
return Ok(new SearchdomainGetDatabaseSizeResult() { SearchdomainDatabaseSizeBytes = sizeInBytes, Success = true });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Data.Common;
|
||||
using System.Text;
|
||||
using Server.Exceptions;
|
||||
using Shared.Models;
|
||||
|
||||
namespace Server.Helper;
|
||||
|
||||
@@ -27,12 +28,12 @@ public class DatabaseHelper(ILogger<DatabaseHelper> logger)
|
||||
helper.ExecuteSQLNonQuery(query.ToString(), parameters);
|
||||
}
|
||||
|
||||
public static int DatabaseInsertSearchdomain(SQLHelper helper, string name)
|
||||
public static int DatabaseInsertSearchdomain(SQLHelper helper, string name, SearchdomainSettings settings = new())
|
||||
{
|
||||
Dictionary<string, dynamic> parameters = new()
|
||||
{
|
||||
{ "name", name },
|
||||
{ "settings", "{}"} // TODO add settings. It's not used yet, but maybe it's needed someday...
|
||||
{ "settings", settings}
|
||||
};
|
||||
return helper.ExecuteSQLCommandGetInsertedID("INSERT INTO searchdomain (name, settings) VALUES (@name, @settings)", parameters);
|
||||
}
|
||||
@@ -176,4 +177,38 @@ public class DatabaseHelper(ILogger<DatabaseHelper> logger)
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
public static long GetSearchdomainDatabaseSize(SQLHelper helper, string searchdomain)
|
||||
{
|
||||
Dictionary<string, dynamic> parameters = new()
|
||||
{
|
||||
{ "searchdomain", searchdomain}
|
||||
};
|
||||
DbDataReader searchdomainSumReader = helper.ExecuteSQLCommand("SELECT SUM(LENGTH(id) + LENGTH(name) + LENGTH(settings)) AS total_bytes FROM embeddingsearch.searchdomain WHERE name=@searchdomain", parameters);
|
||||
bool success = searchdomainSumReader.Read();
|
||||
long result = success && !searchdomainSumReader.IsDBNull(0) ? searchdomainSumReader.GetInt64(0) : 0;
|
||||
searchdomainSumReader.Close();
|
||||
|
||||
DbDataReader entitySumReader = helper.ExecuteSQLCommand("SELECT SUM(LENGTH(e.id) + LENGTH(e.name) + LENGTH(e.probmethod) + LENGTH(e.id_searchdomain)) AS total_bytes FROM embeddingsearch.entity e JOIN embeddingsearch.searchdomain s ON e.id_searchdomain = s.id WHERE s.name=@searchdomain", parameters);
|
||||
success = entitySumReader.Read();
|
||||
result += success && !entitySumReader.IsDBNull(0) ? entitySumReader.GetInt64(0) : 0;
|
||||
entitySumReader.Close();
|
||||
|
||||
DbDataReader datapointSumReader = helper.ExecuteSQLCommand("SELECT SUM(LENGTH(d.id) + LENGTH(d.name) + LENGTH(d.probmethod_embedding) + LENGTH(d.similaritymethod) + LENGTH(d.id_entity) + LENGTH(d.hash)) AS total_bytes FROM embeddingsearch.datapoint d JOIN embeddingsearch.entity e ON d.id_entity = e.id JOIN embeddingsearch.searchdomain s ON e.id_searchdomain = s.id WHERE s.name=@searchdomain", parameters);
|
||||
success = datapointSumReader.Read();
|
||||
result += success && !datapointSumReader.IsDBNull(0) ? datapointSumReader.GetInt64(0) : 0;
|
||||
datapointSumReader.Close();
|
||||
|
||||
DbDataReader embeddingSumReader = helper.ExecuteSQLCommand("SELECT SUM(LENGTH(em.id) + LENGTH(em.id_datapoint) + LENGTH(em.model) + LENGTH(em.embedding)) AS total_bytes FROM embeddingsearch.embedding em JOIN embeddingsearch.datapoint d ON em.id_datapoint = d.id JOIN embeddingsearch.entity e ON d.id_entity = e.id JOIN embeddingsearch.searchdomain s ON e.id_searchdomain = s.id WHERE s.name=@searchdomain", parameters);
|
||||
success = embeddingSumReader.Read();
|
||||
result += success && !embeddingSumReader.IsDBNull(0) ? embeddingSumReader.GetInt64(0) : 0;
|
||||
embeddingSumReader.Close();
|
||||
|
||||
DbDataReader attributeSumReader = helper.ExecuteSQLCommand("SELECT SUM(LENGTH(a.id) + LENGTH(a.id_entity) + LENGTH(a.attribute) + LENGTH(a.value)) AS total_bytes FROM embeddingsearch.attribute a JOIN embeddingsearch.entity e ON a.id_entity = e.id JOIN embeddingsearch.searchdomain s ON e.id_searchdomain = s.id WHERE s.name=@searchdomain", parameters);
|
||||
success = attributeSumReader.Read();
|
||||
result += success && !attributeSumReader.IsDBNull(0) ? attributeSumReader.GetInt64(0) : 0;
|
||||
attributeSumReader.Close();
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Data;
|
||||
using System.Data.Common;
|
||||
using System.Text.Json;
|
||||
using ElmahCore.Mvc.Logger;
|
||||
using MySql.Data.MySqlClient;
|
||||
using Server.Helper;
|
||||
@@ -14,6 +15,7 @@ public class Searchdomain
|
||||
public AIProvider aIProvider;
|
||||
public string searchdomain;
|
||||
public int id;
|
||||
public SearchdomainSettings settings;
|
||||
public Dictionary<string, DateTimedSearchResult> searchCache; // Key: query, Value: Search results for that query (with timestamp)
|
||||
public List<Entity> entityCache;
|
||||
public List<string> modelsInUse;
|
||||
@@ -36,6 +38,7 @@ public class Searchdomain
|
||||
connection = new MySqlConnection(connectionString);
|
||||
connection.Open();
|
||||
helper = new SQLHelper(connection, connectionString);
|
||||
settings = GetSettings();
|
||||
modelsInUse = []; // To make the compiler shut up - it is set in UpdateSearchDomain() don't worry // yeah, about that...
|
||||
if (!runEmpty)
|
||||
{
|
||||
@@ -229,6 +232,19 @@ public class Searchdomain
|
||||
return this.id;
|
||||
}
|
||||
|
||||
public SearchdomainSettings GetSettings()
|
||||
{
|
||||
Dictionary<string, dynamic> parameters = new()
|
||||
{
|
||||
["name"] = searchdomain
|
||||
};
|
||||
DbDataReader reader = helper.ExecuteSQLCommand("SELECT settings from searchdomain WHERE name = @name", parameters);
|
||||
reader.Read();
|
||||
string settingsString = reader.GetString(0);
|
||||
reader.Close();
|
||||
return JsonSerializer.Deserialize<SearchdomainSettings>(settingsString);
|
||||
}
|
||||
|
||||
public void InvalidateSearchCache()
|
||||
{
|
||||
searchCache = [];
|
||||
|
||||
@@ -14,12 +14,14 @@
|
||||
}
|
||||
|
||||
<div class="container-fluid mt-4">
|
||||
<h1 class="visually-hidden">embeddingsearch</h1>
|
||||
<div class="row">
|
||||
|
||||
<!-- Sidebar -->
|
||||
<div class="col-md-4 sidebar">
|
||||
<div class="col-md-4 sidebar" role="complementary">
|
||||
<div class="card">
|
||||
<div class="card-body p-2">
|
||||
<h2 class="visually-hidden">@T["Searchdomain selection"]</h2>
|
||||
<ul class="list-group list-group-flush mb-2" style="max-height: 60vh; overflow-y: auto;">
|
||||
@foreach (var domain in domains)
|
||||
{
|
||||
@@ -28,55 +30,63 @@
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
<button class="btn btn-primary w-100">Add</button>
|
||||
<button id="searchdomainCreate" class="btn btn-primary w-100">@T["Create"]</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="col col-md-8">
|
||||
<div class="col col-md-8" role="main">
|
||||
|
||||
<div class="card section-card">
|
||||
<div class="card-body">
|
||||
<h2 class="visually-hidden">@T["Searchdomain information and settings"]</h2>
|
||||
|
||||
<h3 class="visually-hidden">@T["Actions"]</h3>
|
||||
<!-- Header -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h4 class="mb-0 text-nowrap overflow-auto">Searchdomain1</h4>
|
||||
<p id="searchdomainName" class="mb-0 text-nowrap overflow-auto fs-3">Searchdomain</p>
|
||||
<div class="col-md-3 text-end w-auto">
|
||||
<button class="btn btn-warning btn-sm me-2">Rename</button>
|
||||
<button class="btn btn-danger btn-sm">Delete</button>
|
||||
<button id="searchdomainRename" class="btn btn-warning btn-sm me-2">@T["Rename"]</button>
|
||||
<button id="searchdomainDelete" class="btn btn-danger btn-sm">@T["Delete"]</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Settings -->
|
||||
<div class="row align-items-center mb-3">
|
||||
<h3>@T["Settings"]</h3>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Settings</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
placeholder="JSON-string"
|
||||
disabled
|
||||
/>
|
||||
<input type="checkbox" class="form-check-input" id="searchdomainConfigCacheReconciliation" />
|
||||
<label class="form-check-label" for="searchdomainConfigCacheReconciliation">@T["Cache reconsiliation"]</label>
|
||||
</div>
|
||||
<div class="col-md-2 mt-3 mt-md-0">
|
||||
<button class="btn btn-warning w-100">Update</button>
|
||||
<button class="btn btn-warning w-100" id="searchdomainConfigUpdate">@T["Update"]</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="visually-hidden">@T["Search cache"]</h3>
|
||||
<!-- Cache -->
|
||||
<div class="d-flex align-items-center mb-4">
|
||||
<div class="me-3">
|
||||
<strong>Cache utilization:</strong> 2.47MiB
|
||||
<strong>@T["Search cache utilization"]:</strong> <span id="cacheUtilization">0.00MiB</span>
|
||||
</div>
|
||||
<button class="btn btn-primary btn-sm">Reset</button>
|
||||
<button id="cacheClear" class="btn btn-warning btn-sm">@T["Clear"]</button>
|
||||
</div>
|
||||
|
||||
<h3 class="visually-hidden">@T["Database size"]</h3>
|
||||
<!-- Database size -->
|
||||
<div class="d-flex align-items-center mb-4">
|
||||
<div class="me-3">
|
||||
<strong>@T["Database size"]:</strong> <span id="databaseUtilization">0.00MiB</span>
|
||||
</div>
|
||||
<button id="cacheClear" class="btn btn-warning btn-sm">@T["Clear"]</button>
|
||||
</div>
|
||||
|
||||
<!-- Recent Queries -->
|
||||
<div class="card section-card mb-4">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<strong>Recent queries</strong>
|
||||
<h3>Recent queries</h3>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control form-control-sm w-25"
|
||||
@@ -101,7 +111,7 @@
|
||||
<div class="card section-card">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<strong>Entities</strong>
|
||||
<h3>Entities</h3>
|
||||
<input
|
||||
id="entitiesFilter"
|
||||
type="text"
|
||||
@@ -134,19 +144,19 @@
|
||||
<div class="modal fade" id="entityDetailsModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg modal-dialog-scrollable">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="entityDetailsTitle">Entity Details</h5>
|
||||
<div class="modal-header bg-info">
|
||||
<h2 class="modal-title" id="entityDetailsTitle">@T["Entity Details"]</h2>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<!-- Attributes -->
|
||||
<h6>Attributes</h6>
|
||||
<h3 class="fs-4">@T["Attributes"]</h3>
|
||||
<table class="table table-sm table-bordered mb-4">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Key</th>
|
||||
<th>Value</th>
|
||||
<th>@T["Key"]</th>
|
||||
<th>@T["Value"]</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="entityAttributesBody">
|
||||
@@ -154,13 +164,13 @@
|
||||
</table>
|
||||
|
||||
<!-- Datapoints -->
|
||||
<h6>Datapoints</h6>
|
||||
<h3 class="fs-4">@T["Datapoints"]</h3>
|
||||
<table class="table table-sm table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>ProbMethod</th>
|
||||
<th>SimilarityMethod</th>
|
||||
<th>@T["Name"]</th>
|
||||
<th>@T["ProbMethod"]</th>
|
||||
<th>@T["SimilarityMethod"]</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="entityDatapointsBody">
|
||||
@@ -170,7 +180,7 @@
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||
Close
|
||||
@T["Close"]
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -182,19 +192,19 @@
|
||||
<div class="modal-dialog modal-xl modal-dialog-scrollable">
|
||||
<div class="modal-content">
|
||||
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="queryDetailsTitle">Query Details</h5>
|
||||
<div class="modal-header bg-info">
|
||||
<h2 class="modal-title" id="queryDetailsTitle">@T["Query Details"] - <span id="queryDetailsQueryName"></span></h2>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
|
||||
<!-- Access times -->
|
||||
<h6>Access times</h6>
|
||||
<h3>Access times</h3>
|
||||
<ul id="queryAccessTimes" class="list-group mb-4"></ul>
|
||||
|
||||
<!-- Results -->
|
||||
<h6>Results</h6>
|
||||
<h3>Results</h3>
|
||||
<table class="table table-sm table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -217,6 +227,92 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rename searchdomain Modal -->
|
||||
<div class="modal fade" id="renameSearchdomainModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-m modal-dialog-scrollable">
|
||||
<div class="modal-content">
|
||||
|
||||
<div class="modal-header bg-warning">
|
||||
<h2 class="modal-title" id="renameSearchdomainTitle">@T["Rename searchdomain"]</h2>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<!-- New name -->
|
||||
<div class="mb-3">
|
||||
<label for="renameSearchdomainNewName" class="form-label">New name</label>
|
||||
<input type="text" class="form-control" id="renameSearchdomainNewName" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-warning" onclick="renameSearchdomain(getSelectedDomainKey(), document.getElementById('renameSearchdomainNewName').value)" data-bs-dismiss="modal">
|
||||
Rename
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete searchdomain Modal -->
|
||||
<div class="modal fade" id="deleteSearchdomainModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-m modal-dialog-scrollable">
|
||||
<div class="modal-content">
|
||||
|
||||
<div class="modal-header bg-danger text-white">
|
||||
<h2 class="modal-title" id="deleteSearchdomainTitle">@T["Delete searchdomain"]</h2>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<p>@T["Are you sure you want to delete this searchdomain? This action cannot be undone."]</p>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" id="searchdomainConfirmDelete" class="btn btn-danger" data-bs-dismiss="modal">
|
||||
Delete
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Create searchdomain Modal -->
|
||||
<div class="modal fade" id="createSearchdomainModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-m modal-dialog-scrollable">
|
||||
<div class="modal-content">
|
||||
|
||||
<div class="modal-header bg-primary text-white">
|
||||
<h2 class="modal-title" id="createSearchdomainTitle">@T["Create searchdomain"]</h2>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<label for="createSearchdomainName" class="form-label">@T["Searchdomain name"]</label>
|
||||
<input type="text" class="form-control mb-3" id="createSearchdomainName" placeholder="@T["Searchdomain name"]" />
|
||||
<input type="checkbox" class="form-check-input" id="createSearchdomainWithCacheReconciliation" />
|
||||
<label class="form-check-label" for="createSearchdomainWithCacheReconciliation">@T["Enable cache reconsiliation"]</label>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" id="searchdomainConfirmCreate" class="btn btn-primary" data-bs-dismiss="modal">
|
||||
@T["Create"]
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||
@T["Close"]
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<script>
|
||||
@@ -245,8 +341,185 @@
|
||||
queriesFilter.addEventListener('input', () => {
|
||||
populateQueriesTable(queriesFilter.value);
|
||||
});
|
||||
selectDomain(0);
|
||||
|
||||
document
|
||||
.getElementById('searchdomainRename')
|
||||
.addEventListener('click', () => {
|
||||
const modal = new bootstrap.Modal(
|
||||
document.getElementById('renameSearchdomainModal')
|
||||
);
|
||||
// Fill in searchdomain current name
|
||||
const domainKey = getSelectedDomainKey();
|
||||
document.getElementById(
|
||||
'renameSearchdomainNewName'
|
||||
).value = domains[domainKey];
|
||||
modal.show();
|
||||
});
|
||||
document
|
||||
.getElementById('searchdomainDelete')
|
||||
.addEventListener('click', () => {
|
||||
const modal = new bootstrap.Modal(
|
||||
document.getElementById('deleteSearchdomainModal')
|
||||
);
|
||||
modal.show();
|
||||
});
|
||||
document
|
||||
.getElementById('searchdomainConfirmDelete')
|
||||
.addEventListener('click', () => {
|
||||
const domainKey = getSelectedDomainKey();
|
||||
deleteSearchdomain(domainKey);
|
||||
selectDomain(0);
|
||||
});
|
||||
document
|
||||
.getElementById('searchdomainConfigUpdate')
|
||||
.addEventListener('click', () => {
|
||||
const domainKey = getSelectedDomainKey();
|
||||
const cacheReconciliation = document.getElementById('searchdomainConfigCacheReconciliation').checked;
|
||||
updateSearchdomainConfig(domainKey, { CacheReconciliation: cacheReconciliation});
|
||||
});
|
||||
|
||||
document
|
||||
.getElementById('searchdomainCreate')
|
||||
.addEventListener('click', () => {
|
||||
const modal = new bootstrap.Modal(
|
||||
document.getElementById('createSearchdomainModal')
|
||||
);
|
||||
modal.show();
|
||||
});
|
||||
|
||||
document
|
||||
.getElementById('searchdomainConfirmCreate')
|
||||
.addEventListener('click', () => {
|
||||
const modal = new bootstrap.Modal(
|
||||
document.getElementById('createSearchdomainModal')
|
||||
);
|
||||
const name = document.getElementById('createSearchdomainName').value;
|
||||
const cacheReconciliation = document.getElementById('createSearchdomainWithCacheReconciliation').checked;
|
||||
const settings = { CacheReconciliation: cacheReconciliation };
|
||||
// Implement create logic here
|
||||
fetch(`/Searchdomain/Create?searchdomain=${encodeURIComponent(name)}&settings=${JSON.stringify(settings)}`, {
|
||||
method: 'GET'
|
||||
}).then(response => {
|
||||
if (response.ok) {
|
||||
// TODO add toast
|
||||
console.log('Searchdomain created successfully');
|
||||
// Reload the page to show the new searchdomain
|
||||
location.reload();
|
||||
} else {
|
||||
// TODO add toast
|
||||
console.error('Failed to create searchdomain');
|
||||
}
|
||||
}).catch(error => {
|
||||
console.error('Error creating searchdomain:', error);
|
||||
});
|
||||
});
|
||||
|
||||
document
|
||||
.getElementById('cacheClear')
|
||||
.addEventListener('click', () => {
|
||||
const domainKey = getSelectedDomainKey();
|
||||
fetch(`/Searchdomain/ClearSearchCache?searchdomain=${encodeURIComponent(domains[domainKey])}`, {
|
||||
method: 'GET'
|
||||
}).then(response => {
|
||||
if (response.ok) {
|
||||
// TODO add toast
|
||||
console.log('Searchdomain cache cleared successfully');
|
||||
// Update cache utilization display
|
||||
document.querySelector('#cacheUtilization').innerText = '0.00MiB';
|
||||
} else {
|
||||
// TODO add toast
|
||||
console.error('Failed to clear searchdomain cache');
|
||||
}
|
||||
}).catch(error => {
|
||||
console.error('Error clearing searchdomain cache:', error);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function deleteSearchdomain(domainKey) {
|
||||
// Implement delete logic here
|
||||
fetch(`/Searchdomain/Delete?searchdomain=${encodeURI(domains[domainKey])}`, {
|
||||
method: 'GET'
|
||||
}).then(response => {
|
||||
if (response.ok) {
|
||||
// TODO add toast
|
||||
// Remove from sidebar
|
||||
var domainItem = document.getElementById('sidebar_domain_' + domainKey);
|
||||
domainItem.remove();
|
||||
console.log('Searchdomain deleted successfully');
|
||||
} else {
|
||||
// TODO add toast
|
||||
console.error('Failed to delete searchdomain');
|
||||
}
|
||||
}).catch(error => {
|
||||
console.error('Error deleting searchdomain:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function renameSearchdomain(domainKey, newName) {
|
||||
// Implement rename logic here
|
||||
fetch(`/Searchdomain/Update?searchdomain=${encodeURI(domains[domainKey])}&newName=${newName}`, {
|
||||
method: 'GET'
|
||||
}).then(response => {
|
||||
if (response.ok) {
|
||||
// TODO add toast
|
||||
// Update sidebar and header name
|
||||
var domainItem = document.getElementById('sidebar_domain_' + domainKey);
|
||||
domainItem.innerText = newName;
|
||||
document.querySelector('.section-card h3').innerText = newName;
|
||||
domains[domainKey] = newName;
|
||||
|
||||
console.log('Searchdomain renamed successfully');
|
||||
} else {
|
||||
// TODO add toast
|
||||
console.error('Failed to rename searchdomain');
|
||||
}
|
||||
}).catch(error => {
|
||||
console.error('Error renaming searchdomain:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function updateSearchdomainConfig(domainKey, newSettings) {
|
||||
// Implement update logic here
|
||||
fetch(`/Searchdomain/UpdateSettings?searchdomain=${encodeURIComponent(domains[domainKey])}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(newSettings)
|
||||
}).then(response => {
|
||||
if (response.ok) {
|
||||
// TODO add toast
|
||||
console.log('Searchdomain settings updated successfully');
|
||||
} else {
|
||||
// TODO add toast
|
||||
console.error('Failed to update searchdomain settings');
|
||||
}
|
||||
}).catch(error => {
|
||||
console.error('Error updating searchdomain settings:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function getSelectedDomainKey() {
|
||||
return document.querySelector('.domain-item.active').id.split("_")[2] - 0;
|
||||
}
|
||||
|
||||
function getSearchdomainConfig(domainKey) {
|
||||
return fetch(`/Searchdomain/GetSettings?searchdomain=${encodeURIComponent(domains[domainKey])}`)
|
||||
.then(r => r.json());
|
||||
}
|
||||
|
||||
function getSearchdomainCacheUtilization(domainKey) {
|
||||
return fetch(`/Searchdomain/GetSearchCacheSize?searchdomain=${encodeURIComponent(domains[domainKey])}`)
|
||||
.then(r => r.json());
|
||||
}
|
||||
|
||||
function getSearchdomainDatabaseUtilization(domainKey) {
|
||||
return fetch(`/Searchdomain/GetDatabaseSize?searchdomain=${encodeURIComponent(domains[domainKey])}`)
|
||||
.then(r => r.json());
|
||||
}
|
||||
|
||||
function selectDomain(domainKey) {
|
||||
document.querySelectorAll('.domain-item').forEach(item => {
|
||||
item.classList.remove('active');
|
||||
@@ -256,7 +529,13 @@
|
||||
selectedItem.classList.add('active');
|
||||
|
||||
var domainName = domains[domainKey];
|
||||
document.querySelector('.section-card h4').innerText = domainName;
|
||||
document.querySelector('#searchdomainName').innerText = domainName;
|
||||
|
||||
let searchdomainConfigPromise = getSearchdomainConfig(getSelectedDomainKey());
|
||||
let configElementCacheReconsiliation = document.getElementById('searchdomainConfigCacheReconciliation');
|
||||
|
||||
let cacheUtilizationPromise = getSearchdomainCacheUtilization(getSelectedDomainKey());
|
||||
let databaseUtilizationPromise = getSearchdomainDatabaseUtilization(getSelectedDomainKey());
|
||||
|
||||
/* ---------- ENTITIES ---------- */
|
||||
let entitiesUrl = `/Entity/List?searchdomain=${encodeURIComponent(domainName)}&returnEmbeddings=false`;
|
||||
@@ -294,6 +573,41 @@
|
||||
console.error('Error fetching queries:', err);
|
||||
hideQueriesLoading(queriesCard);
|
||||
});
|
||||
|
||||
searchdomainConfigPromise.then(searchdomainConfig => {
|
||||
if (searchdomainConfig != null && searchdomainConfig.Settings != null)
|
||||
{
|
||||
console.log(searchdomainConfig);
|
||||
configElementCacheReconsiliation.checked = searchdomainConfig.Settings.CacheReconciliation;
|
||||
configElementCacheReconsiliation.disabled = false;
|
||||
} else {
|
||||
//configElement.value = 'Error fetching searchdomain config';
|
||||
configElementCacheReconsiliation.disabled = true;
|
||||
// TODO add toast
|
||||
console.error('Failed to fetch searchdomain config');
|
||||
}
|
||||
});
|
||||
cacheUtilizationPromise.then(cacheUtilization => {
|
||||
if (cacheUtilization != null && cacheUtilization.SearchCacheSizeBytes != null)
|
||||
{
|
||||
document.querySelector('#cacheUtilization').innerText =
|
||||
`${(cacheUtilization.SearchCacheSizeBytes / (1024 * 1024)).toFixed(2)}MiB`;
|
||||
} else {
|
||||
// TODO add toast
|
||||
console.error('Failed to fetch searchdomain cache utilization');
|
||||
}
|
||||
});
|
||||
|
||||
databaseUtilizationPromise.then(databaseUtilization => {
|
||||
if (databaseUtilization != null && databaseUtilization.SearchdomainDatabaseSizeBytes != null)
|
||||
{
|
||||
document.querySelector('#databaseUtilization').innerText =
|
||||
`${(databaseUtilization.SearchdomainDatabaseSizeBytes / (1024 * 1024)).toFixed(2)}MiB`;
|
||||
} else {
|
||||
// TODO add toast
|
||||
console.error('Failed to fetch searchdomain database utilization');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function clearEntitiesTable() {
|
||||
@@ -313,6 +627,7 @@
|
||||
|
||||
const normalizedFilter = filterText.toLowerCase();
|
||||
|
||||
let isFirstEntity = true;
|
||||
entities
|
||||
.filter(e => e.Name.toLowerCase().includes(normalizedFilter))
|
||||
.forEach(entity => {
|
||||
@@ -320,11 +635,15 @@
|
||||
|
||||
var nameCell = document.createElement('td');
|
||||
nameCell.textContent = entity.Name;
|
||||
if (isFirstEntity) {
|
||||
nameCell.classList.add('w-100'); // Otherwise the table doesn't use the full width
|
||||
isFirstEntity = false;
|
||||
}
|
||||
row.appendChild(nameCell);
|
||||
|
||||
var actionCell = document.createElement('td');
|
||||
var detailsButton = document.createElement('button');
|
||||
detailsButton.className = 'btn btn-primary btn-sm';
|
||||
detailsButton.className = 'btn btn-info btn-sm';
|
||||
detailsButton.textContent = 'Details';
|
||||
detailsButton.setAttribute("data-index", entities.findIndex(en => en == entity));
|
||||
detailsButton.addEventListener('click', () => {
|
||||
@@ -341,6 +660,7 @@
|
||||
function populateQueriesTable(filterText = '') {
|
||||
if (!queries) return;
|
||||
|
||||
|
||||
const tbody = document.querySelector('#queriesTable tbody');
|
||||
tbody.innerHTML = '';
|
||||
|
||||
@@ -358,7 +678,7 @@
|
||||
|
||||
const actionCell = document.createElement('td');
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'btn btn-sm btn-primary';
|
||||
btn.className = 'btn btn-sm btn-info';
|
||||
btn.textContent = '@T["Details"]';
|
||||
btn.addEventListener('click', () => {
|
||||
showQueryDetails(query);
|
||||
@@ -369,6 +689,9 @@
|
||||
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
function flagSearchdomainAsErroneous(domainKey) {
|
||||
@@ -447,8 +770,7 @@
|
||||
|
||||
function showQueryDetails(query) {
|
||||
// Title
|
||||
document.getElementById('queryDetailsTitle').innerText =
|
||||
`Query: ${query.Name}`;
|
||||
document.getElementById('queryDetailsQueryName').innerText = query.Name;
|
||||
|
||||
/* ---------- Access times ---------- */
|
||||
const accessList = document.getElementById('queryAccessTimes');
|
||||
|
||||
@@ -45,3 +45,7 @@ body {
|
||||
.d-none {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,25 @@ public readonly struct ResultItem(float score, string name)
|
||||
public readonly float Score { get; } = score;
|
||||
[JsonPropertyName("Name")]
|
||||
public readonly string Name { get; } = name;
|
||||
|
||||
public static long EstimateSize(ResultItem item)
|
||||
{
|
||||
long size = 0;
|
||||
|
||||
// string object
|
||||
if (item.Name != null)
|
||||
{
|
||||
size += MemorySizes.ObjectHeader;
|
||||
size += sizeof(int); // string length
|
||||
size += item.Name.Length * sizeof(char);
|
||||
size = Align(size);
|
||||
}
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
private static long Align(long size)
|
||||
=> (size + 7) & ~7; // 8-byte alignment
|
||||
}
|
||||
|
||||
public struct DateTimedSearchResult(DateTime dateTime, List<ResultItem> results)
|
||||
@@ -16,4 +35,72 @@ public struct DateTimedSearchResult(DateTime dateTime, List<ResultItem> results)
|
||||
public List<DateTime> AccessDateTimes { get; set; } = [dateTime];
|
||||
[JsonPropertyName("Results")]
|
||||
public List<ResultItem> Results { get; set; } = results;
|
||||
|
||||
public long EstimateSize()
|
||||
{
|
||||
long size = 0;
|
||||
|
||||
size += EstimateDateTimeList(AccessDateTimes);
|
||||
size += EstimateResultItemList(Results);
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
private static long EstimateDateTimeList(List<DateTime>? list)
|
||||
{
|
||||
if (list == null)
|
||||
return 0;
|
||||
|
||||
long size = 0;
|
||||
|
||||
// List object
|
||||
size += MemorySizes.ObjectHeader;
|
||||
size += MemorySizes.Reference; // reference to array
|
||||
|
||||
// Internal array
|
||||
size += MemorySizes.ArrayHeader;
|
||||
size += list.Capacity * sizeof(long); // DateTime = 8 bytes
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
private static long EstimateResultItemList(List<ResultItem>? list)
|
||||
{
|
||||
if (list == null)
|
||||
return 0;
|
||||
|
||||
long size = 0;
|
||||
|
||||
// List object
|
||||
size += MemorySizes.ObjectHeader;
|
||||
size += MemorySizes.Reference;
|
||||
|
||||
// Internal array of structs
|
||||
size += MemorySizes.ArrayHeader;
|
||||
int resultItemInlineSize = sizeof(float) + IntPtr.Size; // float + string reference
|
||||
size += list.Capacity * resultItemInlineSize;
|
||||
|
||||
// Heap allocations referenced by ResultItem
|
||||
foreach (var item in list)
|
||||
size += ResultItem.EstimateSize(item);
|
||||
|
||||
return size;
|
||||
}
|
||||
}
|
||||
|
||||
public struct SearchdomainSettings(bool cacheReconciliation = false)
|
||||
{
|
||||
[JsonPropertyName("CacheReconciliation")]
|
||||
public bool CacheReconciliation { get; set; } = cacheReconciliation;
|
||||
}
|
||||
|
||||
internal static class MemorySizes
|
||||
{
|
||||
public static readonly int PointerSize = IntPtr.Size;
|
||||
public static readonly int ObjectHeader = PointerSize * 2;
|
||||
public static readonly int Reference = PointerSize;
|
||||
public static readonly int ArrayHeader = Align(ObjectHeader + sizeof(int));
|
||||
|
||||
public static int Align(int size)
|
||||
=> (size + PointerSize - 1) & ~(PointerSize - 1);
|
||||
}
|
||||
@@ -54,3 +54,49 @@ public class SearchdomainSearchesResults
|
||||
[JsonPropertyName("Searches")]
|
||||
public required Dictionary<string, DateTimedSearchResult> Searches { get; set; }
|
||||
}
|
||||
|
||||
public class SearchdomainSettingsResults
|
||||
{
|
||||
[JsonPropertyName("Success")]
|
||||
public required bool Success { get; set; }
|
||||
|
||||
[JsonPropertyName("Message")]
|
||||
public string? Message { get; set; }
|
||||
|
||||
[JsonPropertyName("Settings")]
|
||||
public required SearchdomainSettings? Settings { get; set; }
|
||||
}
|
||||
|
||||
public class SearchdomainSearchCacheSizeResults
|
||||
{
|
||||
[JsonPropertyName("Success")]
|
||||
public required bool Success { get; set; }
|
||||
|
||||
[JsonPropertyName("Message")]
|
||||
public string? Message { get; set; }
|
||||
|
||||
[JsonPropertyName("SearchCacheSizeBytes")]
|
||||
public required long? SearchCacheSizeBytes { get; set; }
|
||||
}
|
||||
|
||||
public class SearchdomainInvalidateCacheResults
|
||||
{
|
||||
[JsonPropertyName("Success")]
|
||||
public required bool Success { get; set; }
|
||||
|
||||
[JsonPropertyName("Message")]
|
||||
public string? Message { get; set; }
|
||||
}
|
||||
|
||||
public class SearchdomainGetDatabaseSizeResult
|
||||
{
|
||||
[JsonPropertyName("Success")]
|
||||
public required bool Success { get; set; }
|
||||
|
||||
[JsonPropertyName("Message")]
|
||||
public string? Message { get; set; }
|
||||
|
||||
[JsonPropertyName("SearchdomainDatabaseSizeBytes")]
|
||||
public required long? SearchdomainDatabaseSizeBytes { get; set; }
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user