Merge pull request #34 from LD-Reborn/28-create-a-front-end---recent-queries
28 create a front end recent queries
This commit is contained in:
@@ -105,4 +105,27 @@ public class SearchdomainController : ControllerBase
|
|||||||
}
|
}
|
||||||
return Ok(new SearchdomainUpdateResults(){Success = true});
|
return Ok(new SearchdomainUpdateResults(){Success = true});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("GetSearches")]
|
||||||
|
public ActionResult<SearchdomainSearchesResults> GetSearches(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 SearchdomainSearchesResults() { Searches = [], 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 SearchdomainSearchesResults() { Searches = [], Success = false, Message = ex.Message });
|
||||||
|
}
|
||||||
|
Dictionary<string, DateTimedSearchResult> searchCache = searchdomain_.searchCache;
|
||||||
|
|
||||||
|
return Ok(new SearchdomainSearchesResults() { Searches = searchCache, Success = true });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using System.Data.Common;
|
|||||||
using ElmahCore.Mvc.Logger;
|
using ElmahCore.Mvc.Logger;
|
||||||
using MySql.Data.MySqlClient;
|
using MySql.Data.MySqlClient;
|
||||||
using Server.Helper;
|
using Server.Helper;
|
||||||
|
using Shared.Models;
|
||||||
|
|
||||||
namespace Server;
|
namespace Server;
|
||||||
|
|
||||||
@@ -13,7 +14,7 @@ public class Searchdomain
|
|||||||
public AIProvider aIProvider;
|
public AIProvider aIProvider;
|
||||||
public string searchdomain;
|
public string searchdomain;
|
||||||
public int id;
|
public int id;
|
||||||
public Dictionary<string, List<(DateTime, List<(float, string)>)>> searchCache; // Yeah look at this abomination. searchCache[x][0] = last accessed time, searchCache[x][1] = results for x
|
public Dictionary<string, DateTimedSearchResult> searchCache; // Key: query, Value: Search results for that query (with timestamp)
|
||||||
public List<Entity> entityCache;
|
public List<Entity> entityCache;
|
||||||
public List<string> modelsInUse;
|
public List<string> modelsInUse;
|
||||||
public Dictionary<string, Dictionary<string, float[]>> embeddingCache;
|
public Dictionary<string, Dictionary<string, float[]>> embeddingCache;
|
||||||
@@ -22,8 +23,6 @@ public class Searchdomain
|
|||||||
public SQLHelper helper;
|
public SQLHelper helper;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
// TODO Add settings and update cli/program.cs, as well as DatabaseInsertSearchdomain()
|
|
||||||
|
|
||||||
public Searchdomain(string searchdomain, string connectionString, AIProvider aIProvider, Dictionary<string, Dictionary<string, float[]>> embeddingCache, ILogger logger, string provider = "sqlserver", bool runEmpty = false)
|
public Searchdomain(string searchdomain, string connectionString, AIProvider aIProvider, Dictionary<string, Dictionary<string, float[]>> embeddingCache, ILogger logger, string provider = "sqlserver", bool runEmpty = false)
|
||||||
{
|
{
|
||||||
_connectionString = connectionString;
|
_connectionString = connectionString;
|
||||||
@@ -47,6 +46,7 @@ public class Searchdomain
|
|||||||
|
|
||||||
public void UpdateEntityCache()
|
public void UpdateEntityCache()
|
||||||
{
|
{
|
||||||
|
InvalidateSearchCache();
|
||||||
Dictionary<string, dynamic> parametersIDSearchdomain = new()
|
Dictionary<string, dynamic> parametersIDSearchdomain = new()
|
||||||
{
|
{
|
||||||
["id"] = this.id
|
["id"] = this.id
|
||||||
@@ -151,8 +151,14 @@ public class Searchdomain
|
|||||||
embeddingCache = []; // TODO remove this and implement proper remediation to improve performance
|
embeddingCache = []; // TODO remove this and implement proper remediation to improve performance
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<(float, string)> Search(string query, bool sort=true)
|
public List<(float, string)> Search(string query)
|
||||||
{
|
{
|
||||||
|
if (searchCache.TryGetValue(query, out DateTimedSearchResult cachedResult))
|
||||||
|
{
|
||||||
|
cachedResult.AccessDateTimes.Add(DateTime.Now);
|
||||||
|
return [.. cachedResult.Results.Select(r => (r.Score, r.Name))];
|
||||||
|
}
|
||||||
|
|
||||||
if (!embeddingCache.TryGetValue(query, out Dictionary<string, float[]>? queryEmbeddings))
|
if (!embeddingCache.TryGetValue(query, out Dictionary<string, float[]>? queryEmbeddings))
|
||||||
{
|
{
|
||||||
queryEmbeddings = Datapoint.GenerateEmbeddings(query, modelsInUse, aIProvider);
|
queryEmbeddings = Datapoint.GenerateEmbeddings(query, modelsInUse, aIProvider);
|
||||||
@@ -181,8 +187,13 @@ public class Searchdomain
|
|||||||
}
|
}
|
||||||
result.Add((entity.probMethod(datapointProbs), entity.name));
|
result.Add((entity.probMethod(datapointProbs), entity.name));
|
||||||
}
|
}
|
||||||
|
List<(float, string)> results = [.. result.OrderByDescending(s => s.Item1)];
|
||||||
return [.. result.OrderByDescending(s => s.Item1)]; // [.. element] = element.ToList()
|
List<ResultItem> searchResult = new(
|
||||||
|
[.. results.Select(r =>
|
||||||
|
new ResultItem(r.Item1, r.Item2 ))]
|
||||||
|
);
|
||||||
|
searchCache[query] = new DateTimedSearchResult(DateTime.Now, searchResult);
|
||||||
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<string> GetModels(List<Entity> entities)
|
public static List<string> GetModels(List<Entity> entities)
|
||||||
@@ -217,4 +228,9 @@ public class Searchdomain
|
|||||||
reader.Close();
|
reader.Close();
|
||||||
return this.id;
|
return this.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void InvalidateSearchCache()
|
||||||
|
{
|
||||||
|
searchCache = [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,7 +64,9 @@ public class SearchdomainManager
|
|||||||
|
|
||||||
public void InvalidateSearchdomainCache(string searchdomainName)
|
public void InvalidateSearchdomainCache(string searchdomainName)
|
||||||
{
|
{
|
||||||
GetSearchdomain(searchdomainName).UpdateEntityCache();
|
var searchdomain = GetSearchdomain(searchdomainName);
|
||||||
|
searchdomain.UpdateEntityCache();
|
||||||
|
searchdomain.InvalidateSearchCache(); // TODO implement cache remediation (Suggestion: searchdomain-wide setting for cache remediation / invalidation - )
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<string> ListSearchdomains()
|
public List<string> ListSearchdomains()
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
@using Server.Models
|
@using Server.Models
|
||||||
@using System.Web
|
@using System.Web
|
||||||
|
@using Server.Services
|
||||||
|
@inject LocalizationService T
|
||||||
@model HomeIndexViewModel
|
@model HomeIndexViewModel
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Home Page";
|
ViewData["Title"] = "Home Page";
|
||||||
@@ -81,15 +83,17 @@
|
|||||||
placeholder="filter"
|
placeholder="filter"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="spinner d-none"></div>
|
||||||
@* <div class="list-row">
|
<table id="queriesTable" class="table table-striped" style="max-height: 60vh; overflow-y: auto; display: block;">
|
||||||
<span>Some test query</span>
|
<thead>
|
||||||
<button class="btn btn-primary btn-sm">Details</button>
|
<tr>
|
||||||
</div>
|
<th class="visually-hidden">Name</th>
|
||||||
<div class="list-row">
|
<th class="visually-hidden">Action</th>
|
||||||
<span>Some other test query</span>
|
</tr>
|
||||||
<button class="btn btn-primary btn-sm">Details</button>
|
</thead>
|
||||||
</div> *@
|
<tbody>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -116,15 +120,6 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
@* <div class="list-row">
|
|
||||||
<span>Someentity</span>
|
|
||||||
<button class="btn btn-primary btn-sm">Details</button>
|
|
||||||
</div>
|
|
||||||
<div class="list-row">
|
|
||||||
<span>Some other test query</span>
|
|
||||||
<button class="btn btn-primary btn-sm">Details</button>
|
|
||||||
</div> *@
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -182,10 +177,52 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Query Details Modal -->
|
||||||
|
<div class="modal fade" id="queryDetailsModal" tabindex="-1" aria-hidden="true">
|
||||||
|
<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>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-body">
|
||||||
|
|
||||||
|
<!-- Access times -->
|
||||||
|
<h6>Access times</h6>
|
||||||
|
<ul id="queryAccessTimes" class="list-group mb-4"></ul>
|
||||||
|
|
||||||
|
<!-- Results -->
|
||||||
|
<h6>Results</h6>
|
||||||
|
<table class="table table-sm table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Score</th>
|
||||||
|
<th>Name</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="queryResultsBody"></tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
var domains = JSON.parse('@Html.Raw(System.Text.Json.JsonSerializer.Serialize(domains))');
|
var domains = JSON.parse('@Html.Raw(System.Text.Json.JsonSerializer.Serialize(domains))');
|
||||||
var entities = null;
|
var entities = null;
|
||||||
|
var queries = null;
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const filterInput = document.getElementById('entitiesFilter');
|
const filterInput = document.getElementById('entitiesFilter');
|
||||||
@@ -195,36 +232,67 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const entitiesFilter = document.getElementById('entitiesFilter');
|
||||||
|
entitiesFilter.addEventListener('input', () => {
|
||||||
|
populateEntitiesTable(entitiesFilter.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
const queriesFilter = document.querySelector(
|
||||||
|
'#queriesTable'
|
||||||
|
).closest('.card-body').querySelector('input');
|
||||||
|
|
||||||
|
queriesFilter.addEventListener('input', () => {
|
||||||
|
populateQueriesTable(queriesFilter.value);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
function selectDomain(domainKey) {
|
function selectDomain(domainKey) {
|
||||||
// Deselect all domain items
|
|
||||||
document.querySelectorAll('.domain-item').forEach(item => {
|
document.querySelectorAll('.domain-item').forEach(item => {
|
||||||
item.classList.remove('active');
|
item.classList.remove('active');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Select the clicked domain item
|
|
||||||
var selectedItem = document.getElementById('sidebar_domain_' + domainKey);
|
var selectedItem = document.getElementById('sidebar_domain_' + domainKey);
|
||||||
selectedItem.classList.add('active');
|
selectedItem.classList.add('active');
|
||||||
|
|
||||||
// Update main content header
|
|
||||||
var domainName = domains[domainKey];
|
var domainName = domains[domainKey];
|
||||||
document.querySelector('.section-card h4').innerText = domainName;
|
document.querySelector('.section-card h4').innerText = domainName;
|
||||||
|
|
||||||
// Request the entities from that searchdomain
|
/* ---------- ENTITIES ---------- */
|
||||||
let url = `/Entity/List?searchdomain=${encodeURIComponent(domainName)}&returnEmbeddings=false`;
|
let entitiesUrl = `/Entity/List?searchdomain=${encodeURIComponent(domainName)}&returnEmbeddings=false`;
|
||||||
let table = document.querySelector("#entitiesTable").parentElement;
|
let entitiesCard = document.querySelector("#entitiesTable").parentElement;
|
||||||
clearEntitiesTable();
|
clearEntitiesTable();
|
||||||
showEntitiesLoading(table);
|
showEntitiesLoading(entitiesCard);
|
||||||
fetch(url)
|
|
||||||
.then(response => response.json())
|
fetch(entitiesUrl)
|
||||||
|
.then(r => r.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
entities = data.Results;
|
entities = data.Results;
|
||||||
populateEntitiesTable();
|
populateEntitiesTable();
|
||||||
hideEntitiesLoading(table);
|
hideEntitiesLoading(entitiesCard);
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(err => {
|
||||||
console.error('Error fetching entities:', error);
|
console.error(err);
|
||||||
flagSearchdomainAsErroneous(domainKey);
|
flagSearchdomainAsErroneous(domainKey);
|
||||||
hideEntitiesLoading(table);
|
hideEntitiesLoading(entitiesCard);
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ---------- QUERIES ---------- */
|
||||||
|
let queriesUrl = `/Searchdomain/GetSearches?searchdomain=${encodeURIComponent(domainName)}`;
|
||||||
|
let queriesCard = document.querySelector("#queriesTable").parentElement;
|
||||||
|
clearQueriesTable();
|
||||||
|
showQueriesLoading(queriesCard);
|
||||||
|
|
||||||
|
fetch(queriesUrl)
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
queries = Object.entries(data.Searches).map(key => ({"Name": key[0], "AccessDateTimes": key[1].AccessDateTimes, "Results": key[1].Results}));
|
||||||
|
populateQueriesTable();
|
||||||
|
hideQueriesLoading(queriesCard);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error('Error fetching queries:', err);
|
||||||
|
hideQueriesLoading(queriesCard);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,6 +301,10 @@
|
|||||||
tableBody.innerHTML = '';
|
tableBody.innerHTML = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clearQueriesTable() {
|
||||||
|
document.querySelector('#queriesTable tbody').innerHTML = '';
|
||||||
|
}
|
||||||
|
|
||||||
function populateEntitiesTable(filterText = '') {
|
function populateEntitiesTable(filterText = '') {
|
||||||
if (!entities) return;
|
if (!entities) return;
|
||||||
|
|
||||||
@@ -266,6 +338,39 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function populateQueriesTable(filterText = '') {
|
||||||
|
if (!queries) return;
|
||||||
|
|
||||||
|
const tbody = document.querySelector('#queriesTable tbody');
|
||||||
|
tbody.innerHTML = '';
|
||||||
|
|
||||||
|
const normalizedFilter = filterText.toLowerCase();
|
||||||
|
|
||||||
|
queries
|
||||||
|
.filter(q => q.Name?.toLowerCase().includes(normalizedFilter))
|
||||||
|
.forEach(query => {
|
||||||
|
const row = document.createElement('tr');
|
||||||
|
|
||||||
|
const nameCell = document.createElement('td');
|
||||||
|
nameCell.textContent = query.Name;
|
||||||
|
nameCell.classList.add('col-md-12');
|
||||||
|
row.appendChild(nameCell);
|
||||||
|
|
||||||
|
const actionCell = document.createElement('td');
|
||||||
|
const btn = document.createElement('button');
|
||||||
|
btn.className = 'btn btn-sm btn-primary';
|
||||||
|
btn.textContent = '@T["Details"]';
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
showQueryDetails(query);
|
||||||
|
});
|
||||||
|
|
||||||
|
actionCell.appendChild(btn);
|
||||||
|
row.appendChild(actionCell);
|
||||||
|
|
||||||
|
tbody.appendChild(row);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function flagSearchdomainAsErroneous(domainKey) {
|
function flagSearchdomainAsErroneous(domainKey) {
|
||||||
var domainItem = document.getElementById('sidebar_domain_' + domainKey);
|
var domainItem = document.getElementById('sidebar_domain_' + domainKey);
|
||||||
domainItem.classList.add('list-group-item-danger');
|
domainItem.classList.add('list-group-item-danger');
|
||||||
@@ -281,6 +386,16 @@
|
|||||||
element.querySelector('.spinner').classList.add('d-none');
|
element.querySelector('.spinner').classList.add('d-none');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showQueriesLoading(element = null) {
|
||||||
|
if (!element) element = document;
|
||||||
|
element.querySelector('.spinner').classList.remove('d-none');
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideQueriesLoading(element = null) {
|
||||||
|
if (!element) element = document;
|
||||||
|
element.querySelector('.spinner').classList.add('d-none');
|
||||||
|
}
|
||||||
|
|
||||||
function showEntityDetails(entity) {
|
function showEntityDetails(entity) {
|
||||||
// Title
|
// Title
|
||||||
document.getElementById('entityDetailsTitle').innerText = entity.Name;
|
document.getElementById('entityDetailsTitle').innerText = entity.Name;
|
||||||
@@ -329,4 +444,57 @@
|
|||||||
);
|
);
|
||||||
modal.show();
|
modal.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showQueryDetails(query) {
|
||||||
|
// Title
|
||||||
|
document.getElementById('queryDetailsTitle').innerText =
|
||||||
|
`Query: ${query.Name}`;
|
||||||
|
|
||||||
|
/* ---------- Access times ---------- */
|
||||||
|
const accessList = document.getElementById('queryAccessTimes');
|
||||||
|
accessList.innerHTML = '';
|
||||||
|
|
||||||
|
if (!query.AccessDateTimes || query.AccessDateTimes.length === 0) {
|
||||||
|
accessList.innerHTML = `
|
||||||
|
<li class="list-group-item text-muted text-center">
|
||||||
|
No access times
|
||||||
|
</li>`;
|
||||||
|
} else {
|
||||||
|
query.AccessDateTimes.forEach(dt => {
|
||||||
|
const li = document.createElement('li');
|
||||||
|
li.className = 'list-group-item';
|
||||||
|
li.textContent = new Date(dt).toLocaleString();
|
||||||
|
accessList.appendChild(li);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Results ---------- */
|
||||||
|
const resultsBody = document.getElementById('queryResultsBody');
|
||||||
|
resultsBody.innerHTML = '';
|
||||||
|
|
||||||
|
if (!query.Results || query.Results.length === 0) {
|
||||||
|
resultsBody.innerHTML = `
|
||||||
|
<tr>
|
||||||
|
<td colspan="2" class="text-muted text-center">
|
||||||
|
No results
|
||||||
|
</td>
|
||||||
|
</tr>`;
|
||||||
|
} else {
|
||||||
|
query.Results.forEach(r => {
|
||||||
|
const row = document.createElement('tr');
|
||||||
|
row.innerHTML = `
|
||||||
|
<td>${r.Score.toFixed(4)}</td>
|
||||||
|
<td class="text-break">${r.Name}</td>
|
||||||
|
`;
|
||||||
|
resultsBody.appendChild(row);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show modal
|
||||||
|
const modal = new bootstrap.Modal(
|
||||||
|
document.getElementById('queryDetailsModal')
|
||||||
|
);
|
||||||
|
modal.show();
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
19
src/Shared/Models/SearchdomainModels.cs
Normal file
19
src/Shared/Models/SearchdomainModels.cs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Shared.Models;
|
||||||
|
public readonly struct ResultItem(float score, string name)
|
||||||
|
{
|
||||||
|
[JsonPropertyName("Score")]
|
||||||
|
public readonly float Score { get; } = score;
|
||||||
|
[JsonPropertyName("Name")]
|
||||||
|
public readonly string Name { get; } = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct DateTimedSearchResult(DateTime dateTime, List<ResultItem> results)
|
||||||
|
{
|
||||||
|
[JsonPropertyName("AccessDateTimes")]
|
||||||
|
public List<DateTime> AccessDateTimes { get; set; } = [dateTime];
|
||||||
|
[JsonPropertyName("Results")]
|
||||||
|
public List<ResultItem> Results { get; set; } = results;
|
||||||
|
}
|
||||||
@@ -43,3 +43,14 @@ public class SearchdomainDeleteResults
|
|||||||
[JsonPropertyName("DeletedEntities")]
|
[JsonPropertyName("DeletedEntities")]
|
||||||
public required int DeletedEntities { get; set; }
|
public required int DeletedEntities { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class SearchdomainSearchesResults
|
||||||
|
{
|
||||||
|
[JsonPropertyName("Success")]
|
||||||
|
public required bool Success { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("Message")]
|
||||||
|
public string? Message { get; set; }
|
||||||
|
[JsonPropertyName("Searches")]
|
||||||
|
public required Dictionary<string, DateTimedSearchResult> Searches { get; set; }
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user