feat(solr): add Apache Solr connection support
This commit is contained in:
1 parent
7b407ccbfc
commit
3687a9cbfb
75 files changed
+5596
-82
No files matched your search
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"><defs><clipPath id="a" clipPathUnits="userSpaceOnUse"><path d="M0 102.654h203.005V0H0Z"/></clipPath></defs><g clip-path="url(#a)" transform="matrix(1.33333 0 0 -1.33333 0 136.872)"><path d="m0 0-33-35.677L8.473-16.543A33.5 33.5 0 0 1 0 0" style="fill:#da3522;fill-opacity:1;fill-rule:nonzero;stroke:none" transform="translate(31.112 96.955)scale(.53706)"/><path d="M0 0c-4.572 0-8.928-.917-12.9-2.572l-4.428-37.314L4.799-.347A34 34 0 0 1 0 0" style="fill:#da3522;fill-opacity:1;fill-rule:nonzero;stroke:none" transform="translate(17.962 102.654)scale(.53706)"/><path d="m0 0-39.298-21.992 36.87 4.375A33.46 33.46 0 0 1 0 0" style="fill:#da3522;fill-opacity:1;fill-rule:nonzero;stroke:none" transform="translate(35.839 86.966)scale(.53706)"/><path d="M0 0a33.75 33.75 0 0 1 10.612 11.619l-34.559-6.863Z" style="fill:#da3522;fill-opacity:1;fill-rule:nonzero;stroke:none" transform="translate(28.058 69.675)scale(.53706)"/><path d="m0 0-19.237-41.695L16.448-8.689C11.973-4.384 6.313-1.303 0 0" style="fill:#da3522;fill-opacity:1;fill-rule:nonzero;stroke:none" transform="translate(21.626 102.28)scale(.53706)"/><path d="M0 0a33.4 33.4 0 0 1 10.54 2.638L-8.818 4.935Z" style="fill:#da3522;fill-opacity:1;fill-rule:nonzero;stroke:none" transform="translate(19.47 66.654)scale(.53706)"/><path d="M0 0a33.4 33.4 0 0 1-2.829-10.792l5.215-9.32z" style="fill:#da3522;fill-opacity:1;fill-rule:nonzero;stroke:none" transform="translate(1.524 92.034)scale(.53706)"/><path d="M0 0a33.74 33.74 0 0 1-11.832-10.567l4.867-24.504z" style="fill:#da3522;fill-opacity:1;fill-rule:nonzero;stroke:none" transform="translate(9.471 100.531)scale(.53706)"/></g></svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -3207,7 +3207,7 @@ async function handleQuickOpenSelect(item: any) {
|
||||
await connectionStore.loadConsulRoot(item.connectionId);
|
||||
} else if (config?.db_type === "mongodb") {
|
||||
await connectionStore.loadMongoDatabases(item.connectionId);
|
||||
} else if (config?.db_type === "elasticsearch" || config?.db_type === "easysearch" || config?.db_type === "meilisearch") {
|
||||
} else if (config?.db_type === "elasticsearch" || config?.db_type === "easysearch" || config?.db_type === "meilisearch" || config?.db_type === "solr") {
|
||||
await connectionStore.openElasticsearchConnectionTree(item.connectionId);
|
||||
} else if (config?.db_type === "qdrant" || config?.db_type === "milvus" || config?.db_type === "weaviate" || config?.db_type === "chromadb") {
|
||||
await connectionStore.loadVectorCollections(item.connectionId);
|
||||
@@ -3234,7 +3234,7 @@ async function handleQuickOpenSelect(item: any) {
|
||||
await connectionStore.loadConsulRoot(item.connectionId);
|
||||
} else if (config?.db_type === "mongodb") {
|
||||
await connectionStore.loadMongoDatabases(item.connectionId);
|
||||
} else if (config?.db_type === "elasticsearch" || config?.db_type === "easysearch" || config?.db_type === "meilisearch") {
|
||||
} else if (config?.db_type === "elasticsearch" || config?.db_type === "easysearch" || config?.db_type === "meilisearch" || config?.db_type === "solr") {
|
||||
await connectionStore.openElasticsearchConnectionTree(item.connectionId);
|
||||
} else if (config?.db_type === "qdrant" || config?.db_type === "milvus" || config?.db_type === "weaviate" || config?.db_type === "chromadb") {
|
||||
await connectionStore.loadVectorCollections(item.connectionId);
|
||||
|
||||
@@ -3241,6 +3241,7 @@ const tlsCapableDatabaseTypes = new Set<DatabaseType>([
|
||||
"elasticsearch",
|
||||
"easysearch",
|
||||
"meilisearch",
|
||||
"solr",
|
||||
"hbase",
|
||||
"qdrant",
|
||||
"milvus",
|
||||
@@ -4569,12 +4570,12 @@ function connectionConfigForSubmit(id: string, generatedName = "", validatePlugi
|
||||
if ((config.client_cert_path && !config.client_key_path) || (!config.client_cert_path && config.client_key_path)) {
|
||||
throw new Error(t("connection.etcdClientCertPairRequired"));
|
||||
}
|
||||
} else if (form.value.db_type !== "consul" && config.db_type !== "elasticsearch" && config.db_type !== "easysearch") {
|
||||
} else if (form.value.db_type !== "consul" && config.db_type !== "elasticsearch" && config.db_type !== "easysearch" && config.db_type !== "solr") {
|
||||
config.etcd_endpoints = undefined;
|
||||
config.client_cert_path = undefined;
|
||||
config.client_key_path = undefined;
|
||||
}
|
||||
if (config.db_type === "elasticsearch" || config.db_type === "easysearch") {
|
||||
if (config.db_type === "elasticsearch" || config.db_type === "easysearch" || config.db_type === "solr") {
|
||||
config.client_cert_path = config.client_cert_path?.trim() || "";
|
||||
config.client_key_path = config.client_key_path?.trim() || "";
|
||||
if ((config.client_cert_path && !config.client_key_path) || (!config.client_cert_path && config.client_key_path)) {
|
||||
@@ -4591,7 +4592,8 @@ function connectionConfigForSubmit(id: string, generatedName = "", validatePlugi
|
||||
config.db_type !== "victoriametrics" &&
|
||||
config.db_type !== "zookeeper" &&
|
||||
config.db_type !== "elasticsearch" &&
|
||||
config.db_type !== "easysearch"
|
||||
config.db_type !== "easysearch" &&
|
||||
config.db_type !== "solr"
|
||||
) {
|
||||
config.ca_cert_path = undefined;
|
||||
} else {
|
||||
@@ -8115,7 +8117,7 @@ function openExternalUrl(url: string) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="form.db_type !== 'hbase' && form.db_type !== 'meilisearch' && form.db_type !== 'spanner'" class="grid grid-cols-4 items-center gap-4">
|
||||
<div v-if="form.db_type !== 'hbase' && form.db_type !== 'meilisearch' && form.db_type !== 'solr' && form.db_type !== 'spanner'" class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelClass">{{ databaseLabel }}</Label>
|
||||
<Input v-model="form.database" class="col-span-3" :placeholder="databasePlaceholder" />
|
||||
</div>
|
||||
|
||||
@@ -114,7 +114,7 @@ let initializingPrefillGeneration = 0;
|
||||
let componentUnmounted = false;
|
||||
let shownSessionError = "";
|
||||
|
||||
const sqlConnections = computed(() => store.connections.filter((connection) => !["redis", "mongodb", "elasticsearch", "easysearch", "meilisearch", "qdrant", "milvus", "weaviate", "chromadb", "etcd", "zookeeper", "consul", "mq", "nacos"].includes(connection.db_type)));
|
||||
const sqlConnections = computed(() => store.connections.filter((connection) => !["redis", "mongodb", "elasticsearch", "easysearch", "meilisearch", "solr", "qdrant", "milvus", "weaviate", "chromadb", "etcd", "zookeeper", "consul", "mq", "nacos"].includes(connection.db_type)));
|
||||
const selectedSourceTableNames = computed(() => sourceTables.value.filter((table) => selectedSourceTables.value.has(table)));
|
||||
const isBatchCompare = computed(() => selectedSourceTableNames.value.length > 1);
|
||||
// Bridge the shared TableMultiSelect `string[]` v-model with the Set-based selection store.
|
||||
|
||||
@@ -84,7 +84,7 @@ const targetSchemas = ref<string[]>([]);
|
||||
const sourceDbVersion = ref<string | null>(null);
|
||||
const targetDbVersion = ref<string | null>(null);
|
||||
|
||||
const sqlConnections = computed(() => store.connections.filter((c: any) => !["mongodb", "redis", "elasticsearch", "easysearch", "meilisearch", "etcd", "zookeeper", "consul", "mq", "nacos"].includes(c.db_type)));
|
||||
const sqlConnections = computed(() => store.connections.filter((c: any) => !["mongodb", "redis", "elasticsearch", "easysearch", "meilisearch", "solr", "etcd", "zookeeper", "consul", "mq", "nacos"].includes(c.db_type)));
|
||||
|
||||
const sourceConfig = computed(() => store.getConfig(props.sourceConnectionId));
|
||||
const targetConfig = computed(() => store.getConfig(props.targetConnectionId));
|
||||
|
||||
@@ -71,7 +71,7 @@ import {
|
||||
stringifyDocumentStoreValue,
|
||||
documentStoreValueForGrid,
|
||||
} from "@/lib/app/documentJsonValues";
|
||||
import { applyDocumentStoreIdentityPlan, formatMeilisearchDocumentOperationPreview, insertDocumentStoreDocument as insertDocumentStoreDocumentCore } from "@/lib/app/documentStoreSave";
|
||||
import { applyDocumentStoreIdentityPlan, formatMeilisearchDocumentOperationPreview, formatSolrDocumentOperationPreview, insertDocumentStoreDocument as insertDocumentStoreDocumentCore } from "@/lib/app/documentStoreSave";
|
||||
import { restoreDocumentBrowserState, saveDocumentBrowserState, type DocumentBrowserDataSnapshot } from "@/lib/tabs/documentBrowserStateCache";
|
||||
import RedisJsonEditor from "@/components/redis/RedisJsonEditor.vue";
|
||||
import { isLosslessJsonNumber, parseJsonPreservingLargeNumbers } from "@/lib/common/safeJsonFormat";
|
||||
@@ -145,7 +145,7 @@ const DYNAMODB_DEFAULT_EXPORT_ROW_LIMIT = 10_000;
|
||||
// stores (DynamoDB/Elasticsearch) cannot resume a page without their cursor
|
||||
// stacks, and infinite scroll always restarts from the first segment.
|
||||
const restoredDocumentBrowserState = props.stateKey ? restoreDocumentBrowserState(props.stateKey) : undefined;
|
||||
const skipBasedDocumentStore = documentStoreProviderFor(props.databaseType).kind === "mongodb" || documentStoreProviderFor(props.databaseType).kind === "meilisearch";
|
||||
const skipBasedDocumentStore = ["mongodb", "meilisearch", "solr"].includes(documentStoreProviderFor(props.databaseType).kind);
|
||||
const restoresSkipBasedPage = !!restoredDocumentBrowserState && !settingsStore.editorSettings.infiniteScroll && skipBasedDocumentStore;
|
||||
|
||||
const documents = ref<JsonRecord[]>([]);
|
||||
@@ -466,6 +466,12 @@ function elasticsearchGridColumnTypesFor(columns: readonly string[]): string[] {
|
||||
});
|
||||
}
|
||||
const elasticsearchGridColumnTypes = computed(() => elasticsearchGridColumnTypesFor(lastGridColumns.value));
|
||||
const solrSchemaFields = ref<ColumnInfo[]>([]);
|
||||
const solrFieldTypes = computed(() => new Map(solrSchemaFields.value.map((field) => [field.name, field.data_type])));
|
||||
function solrGridColumnTypesFor(columns: readonly string[]): string[] {
|
||||
return columns.map((column) => (column === "_id" ? "string" : (solrFieldTypes.value.get(column) ?? "")));
|
||||
}
|
||||
const solrGridColumnTypes = computed(() => solrGridColumnTypesFor(lastGridColumns.value));
|
||||
const dynamodbTableDescription = ref<DynamoDbTableDescription | null>(null);
|
||||
const dynamodbIndexName = ref("__table__");
|
||||
const dynamodbPageCursors = ref<Array<string | undefined>>([undefined]);
|
||||
@@ -536,6 +542,9 @@ const deleteDetails = computed(() => {
|
||||
});
|
||||
}
|
||||
const displayId = mongoDocumentIdForGrid(id);
|
||||
if (props.databaseType === "solr") {
|
||||
return `Solr core: ${props.collection}\nDocument _id: ${String(displayId)}`;
|
||||
}
|
||||
if (props.databaseType === "elasticsearch" || props.databaseType === "easysearch" || props.databaseType === "meilisearch") {
|
||||
const product = props.databaseType === "easysearch" ? "Easysearch" : props.databaseType === "meilisearch" ? "Meilisearch" : "Elasticsearch";
|
||||
return `${product} index: ${props.collection}\nDocument _id: ${String(displayId)}`;
|
||||
@@ -608,7 +617,7 @@ function commitLoadedDocuments(nextDocuments: JsonRecord[], nextCopyDocuments: J
|
||||
|
||||
const gridResult = computed<QueryResult>(() => {
|
||||
const docs = documents.value;
|
||||
const columnTypes = documentStoreProvider.value.kind === "elasticsearch" ? elasticsearchGridColumnTypes.value : lastGridColumnTypes.value;
|
||||
const columnTypes = documentStoreProvider.value.kind === "elasticsearch" ? elasticsearchGridColumnTypes.value : documentStoreProvider.value.kind === "solr" ? solrGridColumnTypes.value : lastGridColumnTypes.value;
|
||||
if (!docs.length) {
|
||||
return {
|
||||
columns: lastGridColumns.value,
|
||||
@@ -637,7 +646,7 @@ const gridResult = computed<QueryResult>(() => {
|
||||
|
||||
async function exportAllDocumentStoreDocuments(onProgress?: (info: { rowsExported: number; totalRows: number | null }) => void): Promise<QueryResult | undefined> {
|
||||
const kind = documentStoreProvider.value.kind;
|
||||
if (kind !== "mongodb" && kind !== "dynamodb" && kind !== "elasticsearch") return undefined;
|
||||
if (kind !== "mongodb" && kind !== "dynamodb" && kind !== "elasticsearch" && kind !== "solr") return undefined;
|
||||
|
||||
const connectionId = props.connectionId;
|
||||
const database = props.database;
|
||||
@@ -673,7 +682,7 @@ async function exportAllDocumentStoreDocuments(onProgress?: (info: { rowsExporte
|
||||
}
|
||||
}
|
||||
|
||||
if ((kind === "mongodb" || kind === "elasticsearch") && result.total_is_exact !== false) totalRows = Math.min(result.total, rowLimit);
|
||||
if ((kind === "mongodb" || kind === "elasticsearch" || kind === "solr") && result.total_is_exact !== false) totalRows = Math.min(result.total, rowLimit);
|
||||
onProgress?.({ rowsExported: exportedDocuments.length, totalRows });
|
||||
|
||||
if (kind === "dynamodb" || kind === "elasticsearch") {
|
||||
@@ -705,7 +714,7 @@ async function exportAllDocumentStoreDocuments(onProgress?: (info: { rowsExporte
|
||||
|
||||
const result = mongoDocumentsToQueryResult(exportedDocuments, performance.now() - exportStartedAt, totalRows ?? exportedDocuments.length, exportedCopyDocuments, totalRows !== null);
|
||||
if (result.columns.length === 0) result.columns = gridResult.value.columns;
|
||||
result.column_types = kind === "mongodb" ? mongoDocumentGridColumnTypes(exportedDocuments, result.columns) : kind === "elasticsearch" ? elasticsearchGridColumnTypesFor(result.columns) : undefined;
|
||||
result.column_types = kind === "mongodb" ? mongoDocumentGridColumnTypes(exportedDocuments, result.columns) : kind === "elasticsearch" ? elasticsearchGridColumnTypesFor(result.columns) : kind === "solr" ? solrGridColumnTypesFor(result.columns) : undefined;
|
||||
result.affected_rows = exportedDocuments.length;
|
||||
result.truncated = (kind === "dynamodb" || kind === "elasticsearch") && !!cursor && exportedDocuments.length >= rowLimit;
|
||||
result.has_more = result.truncated;
|
||||
@@ -717,10 +726,19 @@ const elasticsearchFilterFieldNames = computed(() => {
|
||||
const names = [...elasticsearchMappingFields.value.map((field) => field.name), ...gridResult.value.columns, "_id", "_routing"];
|
||||
return [...new Set(names.filter(Boolean))];
|
||||
});
|
||||
const solrFilterFieldNames = computed(() => {
|
||||
const names = [...solrSchemaFields.value.map((field) => field.name), ...gridResult.value.columns, "_id"];
|
||||
return [...new Set(names.filter(Boolean))];
|
||||
});
|
||||
const documentFilterFieldTree = computed<DocumentFieldPathNode[]>(() => {
|
||||
if (documentStoreProvider.value.kind === "elasticsearch") {
|
||||
return elasticsearchFieldPathTreeFromFieldNames(elasticsearchFilterFieldNames.value, elasticsearchFieldTypes.value);
|
||||
}
|
||||
if (documentStoreProvider.value.kind === "solr") {
|
||||
// Schema fields give the filter builder a complete field list even before
|
||||
// any document page has loaded; unknown Solr types stay leaf-selectable.
|
||||
return elasticsearchFieldPathTreeFromFieldNames(solrFilterFieldNames.value, solrFieldTypes.value);
|
||||
}
|
||||
const tree = documentFieldPathTreeFromDocuments(documents.value);
|
||||
if (tree.length > 0) return tree;
|
||||
return gridResult.value.columns.map((column) => ({
|
||||
@@ -1395,12 +1413,16 @@ async function applyDocumentStructuredFilters() {
|
||||
applyFilter();
|
||||
}
|
||||
|
||||
async function loadElasticsearchMappingFields() {
|
||||
if (documentStoreProvider.value.kind !== "elasticsearch") return;
|
||||
async function loadDocumentStoreSchemaFields() {
|
||||
const kind = documentStoreProvider.value.kind;
|
||||
if (kind !== "elasticsearch" && kind !== "solr") return;
|
||||
try {
|
||||
elasticsearchMappingFields.value = (await api.getColumns(props.connectionId, props.database, "", props.collection)) ?? [];
|
||||
const fields = (await api.getColumns(props.connectionId, props.database, "", props.collection)) ?? [];
|
||||
if (kind === "elasticsearch") elasticsearchMappingFields.value = fields;
|
||||
else solrSchemaFields.value = fields;
|
||||
} catch {
|
||||
elasticsearchMappingFields.value = [];
|
||||
if (kind === "elasticsearch") elasticsearchMappingFields.value = [];
|
||||
else solrSchemaFields.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1687,9 +1709,9 @@ async function previewDocumentChanges(changes: DocumentGridChanges): Promise<str
|
||||
const sourceDocument = documents.value[rowIdx];
|
||||
if (!sourceDocument) continue;
|
||||
const documentId = sourceDocument._id ?? id;
|
||||
const updated = buildPathIdentityUpdatedDocument(sourceDocument, dirtyCols, columns, "meilisearch");
|
||||
const writeDocument = prepareDocumentStoreWriteDocument(updated, { kind: "meilisearch", mode: "update" });
|
||||
stmts.push(formatMeilisearchDocumentOperationPreview({ action: "update", index: coll, id: documentId, document: writeDocument }));
|
||||
const updated = buildPathIdentityUpdatedDocument(sourceDocument, dirtyCols, columns, kind === "solr" ? "solr" : "meilisearch");
|
||||
const writeDocument = prepareDocumentStoreWriteDocument(updated, { kind: kind === "solr" ? "solr" : "meilisearch", mode: "update" });
|
||||
stmts.push(kind === "solr" ? formatSolrDocumentOperationPreview({ action: "update", core: coll, id: documentId, document: writeDocument }) : formatMeilisearchDocumentOperationPreview({ action: "update", index: coll, id: documentId, document: writeDocument }));
|
||||
}
|
||||
} else {
|
||||
const updateDoc = buildMongoUpdateDocument(dirtyCols, columns, documents.value[rowIdx]);
|
||||
@@ -1707,7 +1729,7 @@ async function previewDocumentChanges(changes: DocumentGridChanges): Promise<str
|
||||
continue;
|
||||
}
|
||||
if (!isEs) {
|
||||
stmts.push(formatMeilisearchDocumentOperationPreview({ action: "delete", index: coll, id: documents.value[rowIdx]?._id ?? id }));
|
||||
stmts.push(kind === "solr" ? formatSolrDocumentOperationPreview({ action: "delete", core: coll, id: documents.value[rowIdx]?._id ?? id }) : formatMeilisearchDocumentOperationPreview({ action: "delete", index: coll, id: documents.value[rowIdx]?._id ?? id }));
|
||||
continue;
|
||||
}
|
||||
const routing = documentRoutingFromGridRow(row, columns);
|
||||
@@ -1730,8 +1752,8 @@ async function previewDocumentChanges(changes: DocumentGridChanges): Promise<str
|
||||
}
|
||||
if (!isEs) {
|
||||
const idValue = idColIdx >= 0 ? newRow[idColIdx] : null;
|
||||
const id = idValue === null || idValue === undefined || idValue === "" ? undefined : parseDocumentStoreInputValue(idValue, "meilisearch");
|
||||
stmts.push(formatMeilisearchDocumentOperationPreview({ action: id === undefined ? "insert" : "upsert", index: coll, id, document: doc }));
|
||||
const id = idValue === null || idValue === undefined || idValue === "" ? undefined : parseDocumentStoreInputValue(idValue, kind === "solr" ? "solr" : "meilisearch");
|
||||
stmts.push(kind === "solr" ? formatSolrDocumentOperationPreview({ action: id === undefined ? "insert" : "upsert", core: coll, id, document: doc }) : formatMeilisearchDocumentOperationPreview({ action: id === undefined ? "insert" : "upsert", index: coll, id, document: doc }));
|
||||
continue;
|
||||
}
|
||||
const id = idColIdx >= 0 ? documentIdFromGridValue(newRow[idColIdx]) : null;
|
||||
@@ -1917,7 +1939,7 @@ async function load(options: { page?: number; append?: boolean; offset?: number;
|
||||
if (documentLoadExecutionId.value !== executionId) return;
|
||||
if (connectionId !== props.connectionId || database !== props.database || collection !== props.collection || storeKind !== documentStoreProvider.value.kind) return;
|
||||
const nextDocuments =
|
||||
storeKind === "elasticsearch" && result.raw_documents?.length === result.documents.length
|
||||
(storeKind === "elasticsearch" || storeKind === "solr") && result.raw_documents?.length === result.documents.length
|
||||
? result.raw_documents.map((raw, index) => {
|
||||
try {
|
||||
return asRecord(parseJsonPreservingLargeNumbers(raw));
|
||||
@@ -1997,7 +2019,7 @@ async function countExactDocumentTotal(): Promise<number | undefined> {
|
||||
paginationTotal.value = totals.paginationTotal;
|
||||
return exactCount;
|
||||
}
|
||||
const exactCount = request.storeKind === "dynamodb" ? await api.documentCountDocuments(request.connectionId, request.collection, request.filter) : await api.mongoCountDocuments(request.connectionId, request.database, request.collection, request.filter, "accurate");
|
||||
const exactCount = request.storeKind === "dynamodb" || request.storeKind === "solr" ? await api.documentCountDocuments(request.connectionId, request.collection, request.filter) : await api.mongoCountDocuments(request.connectionId, request.database, request.collection, request.filter, "accurate");
|
||||
if (!isCurrentDocumentQueryTotalCountRequest(request)) return undefined;
|
||||
if (!Number.isFinite(exactCount) || exactCount < 0) {
|
||||
throw new Error("invalid count");
|
||||
@@ -2633,9 +2655,9 @@ onMounted(async () => {
|
||||
console.warn("[DBX] ensureConnected failed for", props.connectionId, e);
|
||||
}
|
||||
await loadDynamoDbTableDescription();
|
||||
// Mapping metadata enriches the filter builder, but it must not delay the
|
||||
// first page of documents when the mapping endpoint is slow.
|
||||
void loadElasticsearchMappingFields();
|
||||
// Schema metadata enriches the filter builder, but it must not delay the
|
||||
// first page of documents when the schema endpoint is slow.
|
||||
void loadDocumentStoreSchemaFields();
|
||||
// A restored snapshot already holds the rows the last load produced, so a tab
|
||||
// switch must not re-issue the collection query (#8679). Refresh and every
|
||||
// mutation path still force a real load.
|
||||
|
||||
@@ -270,7 +270,7 @@ function sqlBehaviorDialect(): "mysql" | "postgres" | "sqlserver" | undefined {
|
||||
|
||||
function queryEditorSelectionLanguage(): "sql" | "text" {
|
||||
const databaseType = props.databaseType;
|
||||
return databaseType === "redis" || databaseType === "mongodb" || databaseType === "elasticsearch" || databaseType === "easysearch" || databaseType === "meilisearch" || databaseType === "victoriametrics" ? "text" : "sql";
|
||||
return databaseType === "redis" || databaseType === "mongodb" || databaseType === "elasticsearch" || databaseType === "easysearch" || databaseType === "meilisearch" || databaseType === "solr" || databaseType === "victoriametrics" ? "text" : "sql";
|
||||
}
|
||||
|
||||
const COMPLETION_REMOTE_LATENCY_BUDGET_MS = 120;
|
||||
@@ -3902,7 +3902,7 @@ async function refreshSemanticDiagnostics(options: { preserveOutsideRanges?: boo
|
||||
setSemanticDiagnostics([]);
|
||||
return;
|
||||
}
|
||||
if (props.databaseType === "elasticsearch" || props.databaseType === "easysearch" || props.databaseType === "meilisearch" || props.databaseType === "victoriametrics") {
|
||||
if (props.databaseType === "elasticsearch" || props.databaseType === "easysearch" || props.databaseType === "meilisearch" || props.databaseType === "solr" || props.databaseType === "victoriametrics") {
|
||||
setSemanticDiagnostics([]);
|
||||
return;
|
||||
}
|
||||
@@ -4955,7 +4955,7 @@ function localCompletionSchemasForDatabaseDisambiguation(completionContext: Retu
|
||||
}
|
||||
|
||||
function shouldInsertSqlCompletionSpace(): boolean {
|
||||
return props.databaseType !== "mongodb" && props.databaseType !== "redis" && props.databaseType !== "elasticsearch" && props.databaseType !== "easysearch" && props.databaseType !== "meilisearch" && props.databaseType !== "victoriametrics";
|
||||
return props.databaseType !== "mongodb" && props.databaseType !== "redis" && props.databaseType !== "elasticsearch" && props.databaseType !== "easysearch" && props.databaseType !== "meilisearch" && props.databaseType !== "solr" && props.databaseType !== "victoriametrics";
|
||||
}
|
||||
|
||||
// Snippet expansion normally follows from the item type; a provider can also
|
||||
@@ -5196,7 +5196,7 @@ async function provideSqlCompletions(context: CompletionContext) {
|
||||
if (props.databaseType === "mongodb") {
|
||||
return provideMongoCompletions(currentState, position, explicit);
|
||||
}
|
||||
if (props.databaseType === "meilisearch") return null;
|
||||
if (props.databaseType === "meilisearch" || props.databaseType === "solr") return null;
|
||||
if (props.databaseType === "elasticsearch" || props.databaseType === "easysearch") {
|
||||
if (!isSqlLikeCompletionStatement(fullDoc, position, sqlCompletionDialectOptions())) {
|
||||
return provideElasticsearchCompletions(currentState, position, explicit);
|
||||
@@ -5571,7 +5571,7 @@ function shouldStartSqlCompletionAfterInput(insertedText: string, removedText: s
|
||||
if (props.databaseType === "mongodb") {
|
||||
return !!(insertedText || removedText) && shouldAutoOpenMongoCompletion(fullDoc, position);
|
||||
}
|
||||
if (props.databaseType === "victoriametrics" || props.databaseType === "meilisearch") return false;
|
||||
if (props.databaseType === "victoriametrics" || props.databaseType === "meilisearch" || props.databaseType === "solr") return false;
|
||||
if (props.databaseType === "redis" || props.databaseType === "elasticsearch" || props.databaseType === "easysearch") {
|
||||
// Preserve old character-based checks for non-SQL providers.
|
||||
if (!insertedText && removedText) {
|
||||
|
||||
@@ -115,7 +115,7 @@ const exportElapsedText = computed(() => {
|
||||
return formatDataTransferDuration((exportFinishedAt.value ?? currentTime.value) - exportStartedAt.value);
|
||||
});
|
||||
|
||||
const sqlConnections = computed(() => store.connections.filter((c) => !["redis", "mongodb", "elasticsearch", "easysearch", "meilisearch", "qdrant", "milvus", "weaviate", "chromadb", "etcd", "zookeeper", "consul", "mq", "nacos"].includes(c.db_type)));
|
||||
const sqlConnections = computed(() => store.connections.filter((c) => !["redis", "mongodb", "elasticsearch", "easysearch", "meilisearch", "solr", "qdrant", "milvus", "weaviate", "chromadb", "etcd", "zookeeper", "consul", "mq", "nacos"].includes(c.db_type)));
|
||||
|
||||
const canExport = computed(() => {
|
||||
const hasContent = includeStructure.value || includeData.value || includeObjects.value;
|
||||
|
||||
@@ -31,6 +31,7 @@ const assetIcons: Record<string, string> = {
|
||||
elasticsearch: "elasticsearch",
|
||||
easysearch: "easysearch",
|
||||
meilisearch: "meilisearch",
|
||||
solr: "solr",
|
||||
oracle: "oracle",
|
||||
"oracle-10g": "oracle",
|
||||
"oracle-legacy": "oracle",
|
||||
@@ -135,7 +136,7 @@ const assetSrc = computed(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<img v-if="assetName" :src="assetSrc" alt="" class="database-logo object-contain" :class="{ 'database-logo-light': useLightIconInDarkMode, 'database-logo-impala': normalizedType === 'impala' }" aria-hidden="true" />
|
||||
<img v-if="assetName" :src="assetSrc" alt="" class="database-logo object-contain" :class="{ 'database-logo-light': useLightIconInDarkMode, 'database-logo-impala': normalizedType === 'impala', 'database-logo-solr': normalizedType === 'solr' }" aria-hidden="true" />
|
||||
<Database v-else class="text-blue-400" />
|
||||
</template>
|
||||
|
||||
@@ -152,4 +153,10 @@ const assetSrc = computed(() => {
|
||||
.database-logo-impala {
|
||||
transform: scale(1.55);
|
||||
}
|
||||
|
||||
/* solr.svg 的图形撑满整个 viewBox(无内边距),其他 logo 留白约 20-25%,
|
||||
统一 scale(1.35) 下视觉偏大,单独收敛到与多数 logo 一致的占幅。 */
|
||||
.database-logo-solr {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
</style>
|
||||
@@ -129,6 +129,7 @@ const DamengJobAdmin = defineAsyncComponent(() => import("@/components/admin/Dam
|
||||
|
||||
const DamengUserAdmin = defineAsyncComponent(() => import("@/components/admin/DamengUserAdmin.vue"));
|
||||
const DamengRoleAdmin = defineAsyncComponent(() => import("@/components/admin/DamengRoleAdmin.vue"));
|
||||
const SolrAdmin = defineAsyncComponent(() => import("@/components/solr/SolrAdmin.vue"));
|
||||
const PluginFilesystemTab = defineAsyncComponent(() => import("@/components/plugins/PluginFilesystemTab.vue"));
|
||||
const ExplainPlanViewer = defineAsyncComponent(() => import("@/components/explain/ExplainPlanViewer.vue"));
|
||||
const QueryChart = defineAsyncComponent(() => import("@/components/chart/QueryChart.vue"));
|
||||
@@ -505,7 +506,7 @@ const activeStatementExecutionMarkers = computed(() =>
|
||||
const activeElasticsearchJsonResponse = computed(() => elasticsearchJsonResponseForResult(activeEffectiveDatabaseType.value, activeResultSql.value, props.activeTab.result));
|
||||
/** Whether the active result is an Elasticsearch _source table that also has a raw JSON toggle. */
|
||||
const activeElasticsearchRawBody = computed(() => {
|
||||
if (activeEffectiveDatabaseType.value !== "elasticsearch" && activeEffectiveDatabaseType.value !== "easysearch") return undefined;
|
||||
if (activeEffectiveDatabaseType.value !== "elasticsearch" && activeEffectiveDatabaseType.value !== "easysearch" && activeEffectiveDatabaseType.value !== "solr") return undefined;
|
||||
return props.activeTab.result?.elasticsearch_raw_body;
|
||||
});
|
||||
/** ES `_search?profile=true` body extracted from the active result, when present. */
|
||||
@@ -2863,6 +2864,12 @@ defineExpose({
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="activeTab.mode === 'solr-admin'">
|
||||
<div class="min-h-0 flex-1">
|
||||
<SolrAdmin :key="activeTab.id" :connection-id="activeTab.connectionId" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="activeTab.mode === 'dolt-version-control'">
|
||||
<div class="min-h-0 flex-1">
|
||||
<DoltVersionControl :key="activeTab.id" :connection-id="activeTab.connectionId" :database="activeTab.database" :initial-branch="activeTab.workspaceBranch" />
|
||||
|
||||
@@ -209,6 +209,7 @@ const supportsExplain = computed(() => {
|
||||
dbType !== "elasticsearch" &&
|
||||
dbType !== "easysearch" &&
|
||||
dbType !== "meilisearch" &&
|
||||
dbType !== "solr" &&
|
||||
dbType !== "qdrant" &&
|
||||
dbType !== "milvus" &&
|
||||
dbType !== "weaviate" &&
|
||||
@@ -758,6 +759,7 @@ async function changeCatalog(selectedCatalog: string) {
|
||||
activeConnection?.db_type !== 'elasticsearch' &&
|
||||
activeConnection?.db_type !== 'easysearch' &&
|
||||
activeConnection?.db_type !== 'meilisearch' &&
|
||||
activeConnection?.db_type !== 'solr' &&
|
||||
activeConnection?.db_type !== 'qdrant' &&
|
||||
activeConnection?.db_type !== 'milvus' &&
|
||||
activeConnection?.db_type !== 'weaviate' &&
|
||||
|
||||
@@ -1747,7 +1747,7 @@ async function ensureTreeLoadedForTarget(target: ActiveTabSidebarTarget, opts?:
|
||||
await store.loadMongoDatabases(connId);
|
||||
} else if (config.db_type === "dynamodb") {
|
||||
await store.loadDynamoDbTables(connId);
|
||||
} else if (config.db_type === "elasticsearch" || config.db_type === "easysearch" || config.db_type === "meilisearch") {
|
||||
} else if (config.db_type === "elasticsearch" || config.db_type === "easysearch" || config.db_type === "meilisearch" || config.db_type === "solr") {
|
||||
await store.loadElasticsearchIndices(connId);
|
||||
} else if (config.db_type === "qdrant" || config.db_type === "milvus" || config.db_type === "weaviate" || config.db_type === "chromadb") {
|
||||
await store.loadVectorCollections(connId);
|
||||
|
||||
@@ -967,8 +967,8 @@ async function toggle(requestId = beginNavigationRequest()) {
|
||||
await connectionStore.loadMongoDatabases(node.connectionId);
|
||||
} else if (config?.db_type === "dynamodb") {
|
||||
await connectionStore.loadDynamoDbTables(node.connectionId);
|
||||
} else if (config?.db_type === "elasticsearch" || config?.db_type === "easysearch" || config?.db_type === "meilisearch") {
|
||||
// Expand: list indices (like other db types list databases).
|
||||
} else if (config?.db_type === "elasticsearch" || config?.db_type === "easysearch" || config?.db_type === "meilisearch" || config?.db_type === "solr") {
|
||||
// Expand: list indices/cores (like other db types list databases).
|
||||
await connectionStore.loadElasticsearchIndices(node.connectionId);
|
||||
} else if (config?.db_type === "milvus") {
|
||||
await connectionStore.loadMilvusDatabases(node.connectionId);
|
||||
@@ -1826,6 +1826,8 @@ async function openServerDashboard() {
|
||||
connectionStore.activeConnectionId = node.connectionId;
|
||||
if (currentDatabaseType() === "nacos") {
|
||||
queryStore.openNacosDashboard(node.connectionId);
|
||||
} else if (currentDatabaseType() === "solr") {
|
||||
queryStore.openSolrAdmin(node.connectionId);
|
||||
} else if (connectionSupportsXuguServerDashboard(connectionStore.getConfig(node.connectionId))) {
|
||||
queryStore.openXuguDashboard(node.connectionId);
|
||||
} else if (connectionSupportsPgServerDashboard(connectionStore.getConfig(node.connectionId))) {
|
||||
@@ -4686,7 +4688,7 @@ const canOpenSqlFileExecution = computed(() => {
|
||||
const canExportAllDatabases = computed(() => {
|
||||
if (activeNode.value.type !== "connection" || !activeNode.value.connectionId) return false;
|
||||
const dbType = connectionStore.getConfig(activeNode.value.connectionId)?.db_type;
|
||||
return !["redis", "mongodb", "dynamodb", "elasticsearch", "easysearch", "meilisearch", "qdrant", "milvus", "weaviate", "chromadb", "etcd", "zookeeper", "consul", "mq", "nacos", "plugin"].includes(dbType || "");
|
||||
return !["redis", "mongodb", "dynamodb", "elasticsearch", "easysearch", "solr", "meilisearch", "qdrant", "milvus", "weaviate", "chromadb", "etcd", "zookeeper", "consul", "mq", "nacos", "plugin"].includes(dbType || "");
|
||||
});
|
||||
|
||||
const canOpenScheduledBackups = computed(() => {
|
||||
@@ -5597,7 +5599,11 @@ function buildConnectionSidebarMenu(context: SidebarMenuFactoryContext): boolean
|
||||
}
|
||||
if (
|
||||
node.connectionId &&
|
||||
(currentDatabaseType() === "nacos" || connectionSupportsXuguServerDashboard(connectionStore.getConfig(node.connectionId)) || connectionSupportsServerDashboard(connectionStore.getConfig(node.connectionId)) || connectionSupportsPgServerDashboard(connectionStore.getConfig(node.connectionId)))
|
||||
(currentDatabaseType() === "nacos" ||
|
||||
currentDatabaseType() === "solr" ||
|
||||
connectionSupportsXuguServerDashboard(connectionStore.getConfig(node.connectionId)) ||
|
||||
connectionSupportsServerDashboard(connectionStore.getConfig(node.connectionId)) ||
|
||||
connectionSupportsPgServerDashboard(connectionStore.getConfig(node.connectionId)))
|
||||
) {
|
||||
items.push({ label: t("contextMenu.serverDashboard"), action: openServerDashboard, icon: Gauge });
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -167,7 +167,7 @@ function resetPerFileState() {
|
||||
currentFileName.value = "";
|
||||
}
|
||||
|
||||
const sqlConnections = computed(() => store.connections.filter((c) => !["redis", "mongodb", "elasticsearch", "easysearch", "meilisearch", "qdrant", "milvus", "weaviate", "chromadb", "etcd", "zookeeper", "consul", "mq", "nacos"].includes(c.db_type)));
|
||||
const sqlConnections = computed(() => store.connections.filter((c) => !["redis", "mongodb", "elasticsearch", "easysearch", "meilisearch", "solr", "qdrant", "milvus", "weaviate", "chromadb", "etcd", "zookeeper", "consul", "mq", "nacos"].includes(c.db_type)));
|
||||
// Mirrors the core executor gate (`relational_constraint_bypass_kind` in
|
||||
// sql_file_import.rs): MySQL-family types use the session-scoped
|
||||
// FOREIGN_KEY_CHECKS toggle, PostgreSQL-family types use DISABLE/ENABLE
|
||||
|
||||
@@ -25,7 +25,7 @@ const INITIAL_SQL_FILE_LIMIT = 20;
|
||||
const CONTENT_SEARCH_DEBOUNCE_MS = 200;
|
||||
const CONTENT_SEARCH_MAX_RESULTS = 500;
|
||||
|
||||
const REMOTE_SEARCH_UNSUPPORTED_TYPES = new Set<ConnectionConfig["db_type"]>(["redis", "mongodb", "elasticsearch", "easysearch", "meilisearch", "qdrant", "milvus", "weaviate", "chromadb", "neo4j", "influxdb", "victoriametrics", "etcd", "zookeeper", "mq", "nacos", "consul"]);
|
||||
const REMOTE_SEARCH_UNSUPPORTED_TYPES = new Set<ConnectionConfig["db_type"]>(["redis", "mongodb", "elasticsearch", "easysearch", "meilisearch", "solr", "qdrant", "milvus", "weaviate", "chromadb", "neo4j", "influxdb", "victoriametrics", "etcd", "zookeeper", "mq", "nacos", "consul"]);
|
||||
|
||||
export interface QuickOpenItem {
|
||||
id: string;
|
||||
|
||||
@@ -18,6 +18,7 @@ import { defaultViewForResult } from "@/lib/query/queryResultDefaultView";
|
||||
import { isQueryExecutionErrorResult } from "@/lib/query/queryResultError";
|
||||
import { classifyRedisCommandSafety } from "@/lib/redis/redisCommandSafety";
|
||||
import { isRedisCommentLine } from "@/lib/redis/redisCommandTokenizer";
|
||||
import { isDangerousSolrRequest } from "@/lib/solr/solrRequestRisk";
|
||||
import { isSqlExecutionSnapshot, resolveExecutableSql, type SqlExecutionOverride, type SqlExecutionSnapshot } from "@/lib/sql/sqlExecutionTarget";
|
||||
import { isElasticsearchRestRequestText, parseElasticsearchRestRequestTarget, splitSqlStatementRanges, sqlStatementParameterOptionsForCompatibility } from "@/lib/sql/sqlStatementRanges";
|
||||
import { extractSqlParameterDescriptors, type SqlParameterDescriptor, type SqlParameterSyntax } from "@/lib/sql/sqlParameters";
|
||||
@@ -96,12 +97,12 @@ function isDangerousMeilisearchRequest(method: "GET" | "POST" | "PUT" | "PATCH"
|
||||
}
|
||||
|
||||
export function isDangerousSql(sql: string, databaseType?: DatabaseType): boolean {
|
||||
if (databaseType === "elasticsearch" || databaseType === "easysearch" || databaseType === "meilisearch") {
|
||||
if (databaseType === "elasticsearch" || databaseType === "easysearch" || databaseType === "meilisearch" || databaseType === "solr") {
|
||||
const requests = splitSqlStatementRanges(sql, databaseType)
|
||||
.map((statement) => parseElasticsearchRestRequestTarget(statement.sql))
|
||||
.filter((request): request is NonNullable<typeof request> => request !== null);
|
||||
if (requests.length > 0) {
|
||||
return requests.some((request) => (databaseType === "meilisearch" ? isDangerousMeilisearchRequest(request.method, request.path) : isDangerousElasticsearchRequest(request.method, request.path)));
|
||||
return requests.some((request) => (databaseType === "meilisearch" ? isDangerousMeilisearchRequest(request.method, request.path) : databaseType === "solr" ? isDangerousSolrRequest(request.method, request.path) : isDangerousElasticsearchRequest(request.method, request.path)));
|
||||
}
|
||||
}
|
||||
const cleaned = stripSqlComments(sql);
|
||||
@@ -921,7 +922,7 @@ export function useSqlExecution(deps: {
|
||||
|
||||
export function supportsSqlTemplateParameters(connection: Pick<ConnectionConfig, "db_type"> | undefined, sql = ""): boolean {
|
||||
if (!connection) return false;
|
||||
if (connection.db_type === "meilisearch") return false;
|
||||
if (connection.db_type === "meilisearch" || connection.db_type === "solr") return false;
|
||||
if (connection.db_type === "elasticsearch" || connection.db_type === "easysearch") return !isElasticsearchRestRequestText(sql);
|
||||
return connection.db_type !== "redis" && connection.db_type !== "mongodb" && connection.db_type !== "victoriametrics";
|
||||
}
|
||||
|
||||
@@ -4209,6 +4209,154 @@ export default {
|
||||
unknown: "Unknown",
|
||||
},
|
||||
},
|
||||
solrAdmin: {
|
||||
title: "Solr Admin",
|
||||
refresh: "Refresh",
|
||||
selectCore: "Select core",
|
||||
noCore: "No core selected. Pick a core to view core-level admin pages.",
|
||||
emptyBody: "Empty response body.",
|
||||
optional: "optional",
|
||||
sections: {
|
||||
server: "Server",
|
||||
core: "Core",
|
||||
},
|
||||
views: {
|
||||
dashboard: "Dashboard",
|
||||
logging: "Logging",
|
||||
javaProps: "Java Properties",
|
||||
threads: "Thread Dump",
|
||||
coreAdmin: "Core Admin",
|
||||
metrics: "Metrics",
|
||||
overview: "Overview",
|
||||
analysis: "Analysis",
|
||||
documents: "Documents",
|
||||
paramsets: "Paramsets",
|
||||
files: "Files",
|
||||
ping: "Ping",
|
||||
plugins: "Plugins / Stats",
|
||||
query: "Query",
|
||||
replication: "Replication",
|
||||
schema: "Schema",
|
||||
segments: "Segments info",
|
||||
},
|
||||
dashboard: {
|
||||
instance: "Instance",
|
||||
start: "Start",
|
||||
uptime: "Uptime",
|
||||
versions: "Versions",
|
||||
jvmTitle: "JVM",
|
||||
runtime: "Runtime",
|
||||
args: "Args",
|
||||
system: "System",
|
||||
physMem: "Physical Memory",
|
||||
swap: "Swap Space",
|
||||
fd: "File Descriptor Count",
|
||||
jvmMemory: "JVM-Memory",
|
||||
outdatedWarning: "This Solr release is more than a year old. Consider upgrading.",
|
||||
mode: "Mode",
|
||||
solr_home: "Solr Home",
|
||||
core_root: "Core Root",
|
||||
processors: "Processors",
|
||||
},
|
||||
logging: {
|
||||
name: "Logger",
|
||||
level: "Level",
|
||||
set: "Set",
|
||||
},
|
||||
core: {
|
||||
name: "Core",
|
||||
docs: "Docs",
|
||||
size: "Size",
|
||||
segments: "Segs",
|
||||
uptime: "Uptime",
|
||||
ops: "Actions",
|
||||
},
|
||||
schema: {
|
||||
fields: "fields",
|
||||
dynamicFields: "dynamic fields",
|
||||
fieldTypes: "field types",
|
||||
name: "Name",
|
||||
type: "Type",
|
||||
flags: "Flags",
|
||||
},
|
||||
analysis: {
|
||||
field: "Field name",
|
||||
indexValue: "Index value",
|
||||
queryValue: "Query value (optional)",
|
||||
indexPlaceholder: "Text indexed into the field",
|
||||
queryPlaceholder: "Query text analyzed at query time",
|
||||
run: "Analyse",
|
||||
index: "Index chain",
|
||||
query: "Query chain",
|
||||
empty: "No analysis stages returned. Check the field name.",
|
||||
},
|
||||
segments: {
|
||||
name: "Segment",
|
||||
deleted: "Deleted",
|
||||
age: "Age",
|
||||
source: "Source",
|
||||
},
|
||||
actions: {
|
||||
create: "Create Core",
|
||||
reload: "Reload",
|
||||
unload: "Unload",
|
||||
rename: "Rename",
|
||||
swap: "Swap",
|
||||
confirm: 'Run {action} on core "{core}"?',
|
||||
success: '{action} on "{core}" succeeded',
|
||||
failed: "Core admin action failed: {message}",
|
||||
newName: "New core name",
|
||||
swapWith: "Swap with core",
|
||||
},
|
||||
queryForm: {
|
||||
defaultParser: "default parser",
|
||||
paramsets: "paramset(s)",
|
||||
jsonHint: "non-empty switches to POST JSON DSL",
|
||||
execute: "Execute Query",
|
||||
openInConsole: "Open in console",
|
||||
rowsReturned: "rows",
|
||||
tableView: "Table",
|
||||
rawJson: "Raw JSON",
|
||||
},
|
||||
overview: {
|
||||
statistics: "Statistics",
|
||||
instance: "Instance",
|
||||
replication: "Replication",
|
||||
healthcheck: "Healthcheck",
|
||||
lastModified: "Last Modified",
|
||||
numDocs: "Num Docs",
|
||||
maxDoc: "Max Doc",
|
||||
deletedDocs: "Deleted Docs",
|
||||
version: "Version",
|
||||
segmentCount: "Segment Count",
|
||||
current: "Current",
|
||||
cwd: "CWD",
|
||||
instanceDir: "Instance",
|
||||
data: "Data",
|
||||
index: "Index",
|
||||
impl: "Impl",
|
||||
colVersion: "Version",
|
||||
colGen: "Gen",
|
||||
colSize: "Size",
|
||||
leaderSearching: "Leader (Searching)",
|
||||
leaderReplicable: "Leader (Replicable)",
|
||||
noReplication: "No leader replication details for this core.",
|
||||
noHealthcheck: "Ping request handler is not configured with a healthcheck file.",
|
||||
justNow: "just now",
|
||||
minutesAgo: "about {n} minutes ago",
|
||||
hoursAgo: "about {n} hours ago",
|
||||
daysAgo: "about {n} days ago",
|
||||
},
|
||||
replication: {
|
||||
refresh: "Refresh Status",
|
||||
disable: "Disable Replication",
|
||||
enable: "Enable Replication",
|
||||
index: "Index",
|
||||
settingsLeader: "Settings (Leader)",
|
||||
settingsFollower: "Settings (Follower)",
|
||||
noDetails: "No replication details for this core.",
|
||||
},
|
||||
},
|
||||
userAdmin: {
|
||||
title: "Users & Privileges",
|
||||
unsupported: "MySQL-compatible and PostgreSQL-compatible connections are supported. SQL Server, Oracle, and other permission models can be added next.",
|
||||
|
||||
@@ -10217,4 +10217,152 @@ export default withEnglishFallback({
|
||||
titleLabel: "Título (opcional)",
|
||||
titlePlaceholder: "p. ej. Resultado de consulta",
|
||||
},
|
||||
solrAdmin: {
|
||||
title: "Administración de Solr",
|
||||
refresh: "Actualizar",
|
||||
selectCore: "Seleccionar core",
|
||||
noCore: "No se ha seleccionado ningún core. Seleccione un core antes de ver la página de administración a nivel de core.",
|
||||
emptyBody: "El cuerpo de la respuesta está vacío.",
|
||||
optional: "Opcional",
|
||||
sections: {
|
||||
server: "Servidor",
|
||||
core: "Core",
|
||||
},
|
||||
views: {
|
||||
dashboard: "Panel",
|
||||
logging: "Registro",
|
||||
javaProps: "Propiedades de Java",
|
||||
threads: "Volcado de hilos",
|
||||
coreAdmin: "Administración de Core",
|
||||
metrics: "Métricas",
|
||||
overview: "Resumen",
|
||||
analysis: "Análisis de tokenización",
|
||||
documents: "Documentos",
|
||||
paramsets: "Conjuntos de parámetros",
|
||||
files: "Archivos",
|
||||
ping: "Ping",
|
||||
plugins: "Complementos / Estadísticas",
|
||||
query: "Consulta",
|
||||
replication: "Replicación",
|
||||
schema: "Schema",
|
||||
segments: "Información de segmentos",
|
||||
},
|
||||
dashboard: {
|
||||
instance: "Instancia",
|
||||
start: "Hora de inicio",
|
||||
uptime: "Tiempo de actividad",
|
||||
versions: "Versiones",
|
||||
jvmTitle: "JVM",
|
||||
runtime: "Tiempo de ejecución",
|
||||
args: "Argumentos de inicio",
|
||||
system: "Sistema",
|
||||
physMem: "Memoria física",
|
||||
swap: "Espacio de intercambio",
|
||||
fd: "Número de descriptores de archivos",
|
||||
jvmMemory: "Memoria de JVM",
|
||||
outdatedWarning: "Esta versión de Solr se publicó hace más de un año; se recomienda actualizar.",
|
||||
mode: "Modo de ejecución",
|
||||
solr_home: "Solr Home",
|
||||
core_root: "Directorio raíz de Core",
|
||||
processors: "Número de procesadores",
|
||||
},
|
||||
logging: {
|
||||
name: "Logger",
|
||||
level: "Nivel",
|
||||
set: "Establecido",
|
||||
},
|
||||
core: {
|
||||
name: "Core",
|
||||
docs: "Número de documentos",
|
||||
size: "Tamaño",
|
||||
segments: "Número de segmentos",
|
||||
uptime: "Tiempo de actividad",
|
||||
ops: "Operaciones",
|
||||
},
|
||||
schema: {
|
||||
fields: "campos",
|
||||
dynamicFields: "campos dinámicos",
|
||||
fieldTypes: "tipos de campo",
|
||||
name: "Nombre",
|
||||
type: "Tipo",
|
||||
flags: "Indicadores",
|
||||
},
|
||||
analysis: {
|
||||
field: "Nombre del campo",
|
||||
indexValue: "Texto del lado del índice",
|
||||
queryValue: "Texto del lado de consulta (opcional)",
|
||||
indexPlaceholder: "Texto analizado al escribir en este campo",
|
||||
queryPlaceholder: "Texto analizado en el momento de la consulta",
|
||||
run: "Analizar",
|
||||
index: "Cadena de indexación",
|
||||
query: "Cadena de consulta",
|
||||
empty: "No se devolvieron fases de análisis; compruebe que el nombre del campo sea correcto.",
|
||||
},
|
||||
segments: {
|
||||
name: "Segmento",
|
||||
deleted: "Eliminados",
|
||||
age: "Antigüedad",
|
||||
source: "Origen",
|
||||
},
|
||||
actions: {
|
||||
create: "Crear Core",
|
||||
reload: "Recargar",
|
||||
unload: "Descargar",
|
||||
rename: "Renombrar",
|
||||
swap: "Intercambiar",
|
||||
confirm: '¿Ejecutar {action} en el core "{core}"?',
|
||||
success: '{action} en "{core}" se ejecutó correctamente',
|
||||
failed: "Error en la operación de administración de Core: {message}",
|
||||
newName: "Nombre del nuevo core",
|
||||
swapWith: "Intercambiar con core",
|
||||
},
|
||||
queryForm: {
|
||||
defaultParser: "Parser predeterminado",
|
||||
paramsets: "paramset(s)",
|
||||
jsonHint: "Si no está vacío, enviar con JSON DSL",
|
||||
execute: "Ejecutar consulta",
|
||||
openInConsole: "Abrir en la consola",
|
||||
rowsReturned: "Filas",
|
||||
tableView: "Tabla",
|
||||
rawJson: "JSON sin formato",
|
||||
},
|
||||
overview: {
|
||||
statistics: "Estadísticas",
|
||||
instance: "Instancia",
|
||||
replication: "Replicación",
|
||||
healthcheck: "Comprobación de estado",
|
||||
lastModified: "Última modificación",
|
||||
numDocs: "Número de documentos",
|
||||
maxDoc: "Número máximo de documentos",
|
||||
deletedDocs: "Documentos eliminados",
|
||||
version: "Versión",
|
||||
segmentCount: "Número de segmentos",
|
||||
current: "Actual",
|
||||
cwd: "Directorio de trabajo",
|
||||
instanceDir: "Directorio de instancias",
|
||||
data: "Directorio de datos",
|
||||
index: "Directorio de índices",
|
||||
impl: "Clase de implementación",
|
||||
colVersion: "Versión",
|
||||
colGen: "Gen",
|
||||
colSize: "Tamaño",
|
||||
leaderSearching: "Leader (buscando)",
|
||||
leaderReplicable: "Leader (replicable)",
|
||||
noReplication: "Este core no tiene detalles de replicación de leader.",
|
||||
noHealthcheck: "El controlador de solicitudes Ping no tiene configurado el archivo healthcheck.",
|
||||
justNow: "Hace un momento",
|
||||
minutesAgo: "Hace aproximadamente {n} minutos",
|
||||
hoursAgo: "Hace aproximadamente {n} horas",
|
||||
daysAgo: "Hace aproximadamente {n} días",
|
||||
},
|
||||
replication: {
|
||||
refresh: "Actualizar estado",
|
||||
disable: "Deshabilitar replicación",
|
||||
enable: "Habilitar replicación",
|
||||
index: "Índice",
|
||||
settingsLeader: "Configuración (Leader)",
|
||||
settingsFollower: "Configuración (Follower)",
|
||||
noDetails: "Este core no tiene detalles de replicación.",
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -10217,4 +10217,152 @@ export default withEnglishFallback({
|
||||
titleLabel: "Titolo (facoltativo)",
|
||||
titlePlaceholder: "es. Risultato della query",
|
||||
},
|
||||
solrAdmin: {
|
||||
title: "Amministrazione Solr",
|
||||
refresh: "Aggiorna",
|
||||
selectCore: "Seleziona core",
|
||||
noCore: "Nessun core selezionato. Seleziona prima un core per visualizzare la pagina di amministrazione a livello di core.",
|
||||
emptyBody: "Il corpo della risposta è vuoto.",
|
||||
optional: "Opzionale",
|
||||
sections: {
|
||||
server: "Server",
|
||||
core: "Core",
|
||||
},
|
||||
views: {
|
||||
dashboard: "Dashboard",
|
||||
logging: "Log",
|
||||
javaProps: "Proprietà Java",
|
||||
threads: "Dump dei thread",
|
||||
coreAdmin: "Amministrazione core",
|
||||
metrics: "Metriche",
|
||||
overview: "Panoramica",
|
||||
analysis: "Analisi",
|
||||
documents: "Documenti",
|
||||
paramsets: "Set di parametri",
|
||||
files: "File",
|
||||
ping: "Ping",
|
||||
plugins: "Plugin / Statistiche",
|
||||
query: "Query",
|
||||
replication: "Replicazione",
|
||||
schema: "Schema",
|
||||
segments: "Informazioni sui segmenti",
|
||||
},
|
||||
dashboard: {
|
||||
instance: "Istanza",
|
||||
start: "Ora di avvio",
|
||||
uptime: "Tempo di attività",
|
||||
versions: "Versioni",
|
||||
jvmTitle: "JVM",
|
||||
runtime: "Runtime",
|
||||
args: "Parametri di avvio",
|
||||
system: "Sistema",
|
||||
physMem: "Memoria fisica",
|
||||
swap: "Spazio di swap",
|
||||
fd: "Descrittori di file",
|
||||
jvmMemory: "Memoria JVM",
|
||||
outdatedWarning: "Questa versione di Solr è stata rilasciata da oltre un anno; si consiglia l'aggiornamento.",
|
||||
mode: "Modalità di esecuzione",
|
||||
solr_home: "Solr Home",
|
||||
core_root: "Directory radice dei core",
|
||||
processors: "Numero di processori",
|
||||
},
|
||||
logging: {
|
||||
name: "Logger",
|
||||
level: "Livello",
|
||||
set: "Imposta",
|
||||
},
|
||||
core: {
|
||||
name: "Core",
|
||||
docs: "Numero di documenti",
|
||||
size: "Dimensione",
|
||||
segments: "Numero di segmenti",
|
||||
uptime: "Tempo di attività",
|
||||
ops: "Operazioni",
|
||||
},
|
||||
schema: {
|
||||
fields: "campi",
|
||||
dynamicFields: "campi dinamici",
|
||||
fieldTypes: "tipi di campo",
|
||||
name: "Nome",
|
||||
type: "Tipo",
|
||||
flags: "Flag",
|
||||
},
|
||||
analysis: {
|
||||
field: "Nome del campo",
|
||||
indexValue: "Testo lato indice",
|
||||
queryValue: "Testo lato query (opzionale)",
|
||||
indexPlaceholder: "Testo analizzato durante la scrittura in questo campo",
|
||||
queryPlaceholder: "Testo analizzato durante la query",
|
||||
run: "Analizza",
|
||||
index: "Catena di indicizzazione",
|
||||
query: "Catena di query",
|
||||
empty: "Nessuna fase di analisi restituita. Controlla che il nome del campo sia corretto.",
|
||||
},
|
||||
segments: {
|
||||
name: "Segmento",
|
||||
deleted: "Eliminato",
|
||||
age: "Età",
|
||||
source: "Origine",
|
||||
},
|
||||
actions: {
|
||||
create: "Crea core",
|
||||
reload: "Ricarica",
|
||||
unload: "Scarica",
|
||||
rename: "Rinomina",
|
||||
swap: "Scambia",
|
||||
confirm: 'Eseguire {action} sul core "{core}"?',
|
||||
success: '{action} su "{core}" eseguito correttamente',
|
||||
failed: "Operazione di amministrazione core non riuscita: {message}",
|
||||
newName: "Nuovo nome core",
|
||||
swapWith: "Scambia con il core",
|
||||
},
|
||||
queryForm: {
|
||||
defaultParser: "Parser predefinito",
|
||||
paramsets: "paramset",
|
||||
jsonHint: "Se non vuoto, invia con JSON DSL",
|
||||
execute: "Esegui query",
|
||||
openInConsole: "Apri nella console",
|
||||
rowsReturned: "Righe",
|
||||
tableView: "Tabella",
|
||||
rawJson: "JSON non elaborato",
|
||||
},
|
||||
overview: {
|
||||
statistics: "Statistiche",
|
||||
instance: "Istanza",
|
||||
replication: "Replicazione",
|
||||
healthcheck: "Controllo di integrità",
|
||||
lastModified: "Ultima modifica",
|
||||
numDocs: "Numero di documenti",
|
||||
maxDoc: "Numero massimo di documenti",
|
||||
deletedDocs: "Documenti eliminati",
|
||||
version: "Versione",
|
||||
segmentCount: "Numero di segmenti",
|
||||
current: "Corrente",
|
||||
cwd: "Directory di lavoro",
|
||||
instanceDir: "Directory dell'istanza",
|
||||
data: "Directory dei dati",
|
||||
index: "Directory dell'indice",
|
||||
impl: "Classe di implementazione",
|
||||
colVersion: "Versione",
|
||||
colGen: "Gen",
|
||||
colSize: "Dimensione",
|
||||
leaderSearching: "Leader (ricerca in corso)",
|
||||
leaderReplicable: "Leader (replicabile)",
|
||||
noReplication: "Questo core non dispone di dettagli di replica del leader.",
|
||||
noHealthcheck: "Il gestore di richieste Ping non ha configurato un file healthcheck.",
|
||||
justNow: "Proprio ora",
|
||||
minutesAgo: "Circa {n} minuti fa",
|
||||
hoursAgo: "Circa {n} ore fa",
|
||||
daysAgo: "Circa {n} giorni fa",
|
||||
},
|
||||
replication: {
|
||||
refresh: "Aggiorna stato",
|
||||
disable: "Disabilita replicazione",
|
||||
enable: "Abilita replicazione",
|
||||
index: "Indice",
|
||||
settingsLeader: "Impostazioni (Leader)",
|
||||
settingsFollower: "Impostazioni (Follower)",
|
||||
noDetails: "Questo core non ha dettagli di replicazione.",
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -10267,4 +10267,152 @@ export default withEnglishFallback({
|
||||
titleLabel: "タイトル(任意)",
|
||||
titlePlaceholder: "例:クエリ結果",
|
||||
},
|
||||
solrAdmin: {
|
||||
title: "Solr 管理",
|
||||
refresh: "更新",
|
||||
selectCore: "コアを選択",
|
||||
noCore: "コアが選択されていません。コアレベルの管理ページを表示するには、まずコアを選択してください。",
|
||||
emptyBody: "レスポンスボディが空です。",
|
||||
optional: "任意",
|
||||
sections: {
|
||||
server: "サーバー",
|
||||
core: "コア",
|
||||
},
|
||||
views: {
|
||||
dashboard: "ダッシュボード",
|
||||
logging: "ログ",
|
||||
javaProps: "Java プロパティ",
|
||||
threads: "スレッドダンプ",
|
||||
coreAdmin: "コア管理",
|
||||
metrics: "メトリクス",
|
||||
overview: "概要",
|
||||
analysis: "アナライズ",
|
||||
documents: "ドキュメント",
|
||||
paramsets: "パラメータセット",
|
||||
files: "ファイル",
|
||||
ping: "Ping",
|
||||
plugins: "プラグイン / 統計",
|
||||
query: "クエリ",
|
||||
replication: "レプリケーション",
|
||||
schema: "スキーマ",
|
||||
segments: "セグメント情報",
|
||||
},
|
||||
dashboard: {
|
||||
instance: "インスタンス",
|
||||
start: "起動時間",
|
||||
uptime: "稼働時間",
|
||||
versions: "バージョン",
|
||||
jvmTitle: "JVM",
|
||||
runtime: "ランタイム",
|
||||
args: "起動引数",
|
||||
system: "システム",
|
||||
physMem: "物理メモリ",
|
||||
swap: "スワップ領域",
|
||||
fd: "ファイル記述子数",
|
||||
jvmMemory: "JVM メモリ",
|
||||
outdatedWarning: "この Solr バージョンはリリースから1年以上経過しています。アップグレードをおすすめします。",
|
||||
mode: "実行モード",
|
||||
solr_home: "Solr Home",
|
||||
core_root: "コアルートディレクトリ",
|
||||
processors: "プロセッサ数",
|
||||
},
|
||||
logging: {
|
||||
name: "ロガー",
|
||||
level: "レベル",
|
||||
set: "設定済み",
|
||||
},
|
||||
core: {
|
||||
name: "コア",
|
||||
docs: "ドキュメント数",
|
||||
size: "サイズ",
|
||||
segments: "セグメント数",
|
||||
uptime: "稼働時間",
|
||||
ops: "操作",
|
||||
},
|
||||
schema: {
|
||||
fields: "フィールド",
|
||||
dynamicFields: "動的フィールド",
|
||||
fieldTypes: "フィールドタイプ",
|
||||
name: "名前",
|
||||
type: "型",
|
||||
flags: "フラグ",
|
||||
},
|
||||
analysis: {
|
||||
field: "フィールド名",
|
||||
indexValue: "インデックス側テキスト",
|
||||
queryValue: "クエリ側テキスト(任意)",
|
||||
indexPlaceholder: "このフィールドに書き込む際に分析されるテキスト",
|
||||
queryPlaceholder: "クエリ時に分析されるテキスト",
|
||||
run: "分析",
|
||||
index: "インデックスチェーン",
|
||||
query: "クエリチェーン",
|
||||
empty: "分析フェーズが返されませんでした。フィールド名が正しいか確認してください。",
|
||||
},
|
||||
segments: {
|
||||
name: "セグメント",
|
||||
deleted: "削除済み",
|
||||
age: "経過時間",
|
||||
source: "ソース",
|
||||
},
|
||||
actions: {
|
||||
create: "コアの新規作成",
|
||||
reload: "再読み込み",
|
||||
unload: "アンロード",
|
||||
rename: "名前の変更",
|
||||
swap: "交換",
|
||||
confirm: 'コア "{core}" に対して {action} を実行しますか?',
|
||||
success: '{action} "{core}" が成功しました',
|
||||
failed: "コア管理操作に失敗しました:{message}",
|
||||
newName: "新しいコア名",
|
||||
swapWith: "コアと交換",
|
||||
},
|
||||
queryForm: {
|
||||
defaultParser: "デフォルトパーサー",
|
||||
paramsets: "paramset(s)",
|
||||
jsonHint: "空でない場合、JSON DSL で送信します",
|
||||
execute: "クエリを実行",
|
||||
openInConsole: "コンソールで開く",
|
||||
rowsReturned: "行",
|
||||
tableView: "テーブル",
|
||||
rawJson: "生の JSON",
|
||||
},
|
||||
overview: {
|
||||
statistics: "統計",
|
||||
instance: "インスタンス",
|
||||
replication: "レプリケーション",
|
||||
healthcheck: "ヘルスチェック",
|
||||
lastModified: "最終更新",
|
||||
numDocs: "ドキュメント数",
|
||||
maxDoc: "最大ドキュメント数",
|
||||
deletedDocs: "削除済みドキュメント",
|
||||
version: "バージョン",
|
||||
segmentCount: "セグメント数",
|
||||
current: "現在",
|
||||
cwd: "作業ディレクトリ",
|
||||
instanceDir: "インスタンスディレクトリ",
|
||||
data: "データディレクトリ",
|
||||
index: "インデックスディレクトリ",
|
||||
impl: "実装クラス",
|
||||
colVersion: "バージョン",
|
||||
colGen: "Gen",
|
||||
colSize: "サイズ",
|
||||
leaderSearching: "リーダー(検索中)",
|
||||
leaderReplicable: "リーダー(複製可能)",
|
||||
noReplication: "このコアにはリーダーの複製詳細がありません。",
|
||||
noHealthcheck: "Ping リクエストハンドラに healthcheck ファイルが設定されていません。",
|
||||
justNow: "たった今",
|
||||
minutesAgo: "約 {n} 分前",
|
||||
hoursAgo: "約 {n} 時間前",
|
||||
daysAgo: "約 {n} 日前",
|
||||
},
|
||||
replication: {
|
||||
refresh: "状態を更新",
|
||||
disable: "レプリケーションを無効化",
|
||||
enable: "レプリケーションを有効化",
|
||||
index: "インデックス",
|
||||
settingsLeader: "設定(リーダー)",
|
||||
settingsFollower: "設定(フォロワー)",
|
||||
noDetails: "このコアにはレプリケーションの詳細がありません。",
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -9675,4 +9675,152 @@ export default withEnglishFallback({
|
||||
titleLabel: "제목 (선택)",
|
||||
titlePlaceholder: "예: 쿼리 결과",
|
||||
},
|
||||
solrAdmin: {
|
||||
title: "Solr 관리",
|
||||
refresh: "새로 고침",
|
||||
selectCore: "코어 선택",
|
||||
noCore: "코어를 선택하지 않았습니다. 먼저 코어를 선택한 후 코어 수준 관리 페이지를 확인하세요.",
|
||||
emptyBody: "응답 본문이 비어 있습니다.",
|
||||
optional: "선택 사항",
|
||||
sections: {
|
||||
server: "서버",
|
||||
core: "코어",
|
||||
},
|
||||
views: {
|
||||
dashboard: "대시보드",
|
||||
logging: "로깅",
|
||||
javaProps: "Java 속성",
|
||||
threads: "스레드 덤프",
|
||||
coreAdmin: "코어 관리",
|
||||
metrics: "지표",
|
||||
overview: "개요",
|
||||
analysis: "분석",
|
||||
documents: "문서",
|
||||
paramsets: "매개변수 집합",
|
||||
files: "파일",
|
||||
ping: "Ping",
|
||||
plugins: "플러그인 / 통계",
|
||||
query: "쿼리",
|
||||
replication: "복제",
|
||||
schema: "스키마",
|
||||
segments: "세그먼트 정보",
|
||||
},
|
||||
dashboard: {
|
||||
instance: "인스턴스",
|
||||
start: "시작 시간",
|
||||
uptime: "가동 시간",
|
||||
versions: "버전",
|
||||
jvmTitle: "JVM",
|
||||
runtime: "런타임",
|
||||
args: "시작 매개변수",
|
||||
system: "시스템",
|
||||
physMem: "물리 메모리",
|
||||
swap: "스왑 공간",
|
||||
fd: "파일 디스크립터 수",
|
||||
jvmMemory: "JVM 메모리",
|
||||
outdatedWarning: "이 Solr 버전은 출시된 지 1년이 넘었습니다. 업그레이드를 권장합니다.",
|
||||
mode: "실행 모드",
|
||||
solr_home: "Solr Home",
|
||||
core_root: "코어 루트 디렉터리",
|
||||
processors: "프로세서 수",
|
||||
},
|
||||
logging: {
|
||||
name: "로거",
|
||||
level: "레벨",
|
||||
set: "설정됨",
|
||||
},
|
||||
core: {
|
||||
name: "코어",
|
||||
docs: "문서 수",
|
||||
size: "크기",
|
||||
segments: "세그먼트 수",
|
||||
uptime: "가동 시간",
|
||||
ops: "작업",
|
||||
},
|
||||
schema: {
|
||||
fields: "필드",
|
||||
dynamicFields: "동적 필드",
|
||||
fieldTypes: "필드 유형",
|
||||
name: "이름",
|
||||
type: "유형",
|
||||
flags: "플래그",
|
||||
},
|
||||
analysis: {
|
||||
field: "필드 이름",
|
||||
indexValue: "색인 측 텍스트",
|
||||
queryValue: "쿼리 측 텍스트(선택 사항)",
|
||||
indexPlaceholder: "이 필드에 쓸 때 분석되는 텍스트",
|
||||
queryPlaceholder: "쿼리 시 분석되는 텍스트",
|
||||
run: "분석",
|
||||
index: "색인 체인",
|
||||
query: "쿼리 체인",
|
||||
empty: "분석 단계가 반환되지 않았습니다. 필드 이름이 올바른지 확인하세요.",
|
||||
},
|
||||
segments: {
|
||||
name: "세그먼트",
|
||||
deleted: "삭제됨",
|
||||
age: "나이",
|
||||
source: "소스",
|
||||
},
|
||||
actions: {
|
||||
create: "새 코어 생성",
|
||||
reload: "다시 로드",
|
||||
unload: "언로드",
|
||||
rename: "이름 바꾸기",
|
||||
swap: "교환",
|
||||
confirm: '코어 "{core}"에 대해 {action}을(를) 실행하시겠습니까?',
|
||||
success: '{action} "{core}" 실행 성공',
|
||||
failed: "코어 관리 작업 실패: {message}",
|
||||
newName: "새 코어 이름",
|
||||
swapWith: "코어와 교환",
|
||||
},
|
||||
queryForm: {
|
||||
defaultParser: "기본 파서",
|
||||
paramsets: "paramset(s)",
|
||||
jsonHint: "비어 있지 않으면 JSON DSL로 제출합니다",
|
||||
execute: "쿼리 실행",
|
||||
openInConsole: "콘솔에서 열기",
|
||||
rowsReturned: "행",
|
||||
tableView: "표",
|
||||
rawJson: "원시 JSON",
|
||||
},
|
||||
overview: {
|
||||
statistics: "통계",
|
||||
instance: "인스턴스",
|
||||
replication: "복제",
|
||||
healthcheck: "상태 확인",
|
||||
lastModified: "최종 수정",
|
||||
numDocs: "문서 수",
|
||||
maxDoc: "최대 문서 수",
|
||||
deletedDocs: "삭제된 문서",
|
||||
version: "버전",
|
||||
segmentCount: "세그먼트 수",
|
||||
current: "현재",
|
||||
cwd: "작업 디렉터리",
|
||||
instanceDir: "인스턴스 디렉터리",
|
||||
data: "데이터 디렉터리",
|
||||
index: "색인 디렉터리",
|
||||
impl: "구현 클래스",
|
||||
colVersion: "버전",
|
||||
colGen: "Gen",
|
||||
colSize: "크기",
|
||||
leaderSearching: "리더(검색 중)",
|
||||
leaderReplicable: "리더(복제 가능)",
|
||||
noReplication: "이 코어에는 리더 복제 세부 정보가 없습니다.",
|
||||
noHealthcheck: "Ping 요청 핸들러에 healthcheck 파일이 구성되지 않았습니다.",
|
||||
justNow: "방금",
|
||||
minutesAgo: "약 {n}분 전",
|
||||
hoursAgo: "약 {n}시간 전",
|
||||
daysAgo: "약 {n}일 전",
|
||||
},
|
||||
replication: {
|
||||
refresh: "상태 새로 고침",
|
||||
disable: "복제 사용 안 함",
|
||||
enable: "복제 사용",
|
||||
index: "색인",
|
||||
settingsLeader: "설정(리더)",
|
||||
settingsFollower: "설정(팔로워)",
|
||||
noDetails: "이 코어에는 복제 세부 정보가 없습니다.",
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -10218,4 +10218,152 @@ export default withEnglishFallback({
|
||||
titleLabel: "Título (opcional)",
|
||||
titlePlaceholder: "ex.: Resultado da consulta",
|
||||
},
|
||||
solrAdmin: {
|
||||
title: "Administração do Solr",
|
||||
refresh: "Atualizar",
|
||||
selectCore: "Selecionar core",
|
||||
noCore: "Nenhum core selecionado. Selecione um core primeiro para visualizar a página de administração do core.",
|
||||
emptyBody: "O corpo da resposta está vazio.",
|
||||
optional: "Opcional",
|
||||
sections: {
|
||||
server: "Servidor",
|
||||
core: "Core",
|
||||
},
|
||||
views: {
|
||||
dashboard: "Painel",
|
||||
logging: "Logs",
|
||||
javaProps: "Propriedades Java",
|
||||
threads: "Dump de threads",
|
||||
coreAdmin: "Administração do Core",
|
||||
metrics: "Métricas",
|
||||
overview: "Visão geral",
|
||||
analysis: "Análise",
|
||||
documents: "Documentos",
|
||||
paramsets: "Conjuntos de parâmetros",
|
||||
files: "Arquivos",
|
||||
ping: "Ping",
|
||||
plugins: "Plugins / Estatísticas",
|
||||
query: "Consulta",
|
||||
replication: "Replicação",
|
||||
schema: "Schema",
|
||||
segments: "Informações de segmentos",
|
||||
},
|
||||
dashboard: {
|
||||
instance: "Instância",
|
||||
start: "Hora de início",
|
||||
uptime: "Tempo de atividade",
|
||||
versions: "Versões",
|
||||
jvmTitle: "JVM",
|
||||
runtime: "Tempo de execução",
|
||||
args: "Argumentos de inicialização",
|
||||
system: "Sistema",
|
||||
physMem: "Memória física",
|
||||
swap: "Swap",
|
||||
fd: "Descritores de arquivo",
|
||||
jvmMemory: "Memória da JVM",
|
||||
outdatedWarning: "Esta versão do Solr foi lançada há mais de um ano, recomenda-se atualizar.",
|
||||
mode: "Modo de execução",
|
||||
solr_home: "Solr Home",
|
||||
core_root: "Diretório raiz do Core",
|
||||
processors: "Número de processadores",
|
||||
},
|
||||
logging: {
|
||||
name: "Logger",
|
||||
level: "Nível",
|
||||
set: "Definido",
|
||||
},
|
||||
core: {
|
||||
name: "Core",
|
||||
docs: "Número de documentos",
|
||||
size: "Tamanho",
|
||||
segments: "Número de segmentos",
|
||||
uptime: "Tempo de atividade",
|
||||
ops: "Operações",
|
||||
},
|
||||
schema: {
|
||||
fields: "campos",
|
||||
dynamicFields: "campos dinâmicos",
|
||||
fieldTypes: "tipos de campo",
|
||||
name: "Nome",
|
||||
type: "Tipo",
|
||||
flags: "Sinalizadores",
|
||||
},
|
||||
analysis: {
|
||||
field: "Nome do campo",
|
||||
indexValue: "Texto do lado do índice",
|
||||
queryValue: "Texto do lado da consulta (opcional)",
|
||||
indexPlaceholder: "Texto analisado ao indexar este campo",
|
||||
queryPlaceholder: "Texto analisado durante a consulta",
|
||||
run: "Analisar",
|
||||
index: "Cadeia de indexação",
|
||||
query: "Cadeia de consulta",
|
||||
empty: "Nenhuma fase de análise foi retornada. Verifique se o nome do campo está correto.",
|
||||
},
|
||||
segments: {
|
||||
name: "Segmento",
|
||||
deleted: "Excluído",
|
||||
age: "Idade",
|
||||
source: "Origem",
|
||||
},
|
||||
actions: {
|
||||
create: "Criar Core",
|
||||
reload: "Recarregar",
|
||||
unload: "Descarregar",
|
||||
rename: "Renomear",
|
||||
swap: "Trocar",
|
||||
confirm: 'Executar {action} no core "{core}"?',
|
||||
success: '{action} "{core}" executado com sucesso',
|
||||
failed: "Falha na operação de administração do Core: {message}",
|
||||
newName: "Nome do novo core",
|
||||
swapWith: "Trocar com o core",
|
||||
},
|
||||
queryForm: {
|
||||
defaultParser: "Parser padrão",
|
||||
paramsets: "paramset(s)",
|
||||
jsonHint: "Se não vazio, enviar usando JSON DSL",
|
||||
execute: "Executar consulta",
|
||||
openInConsole: "Abrir no console",
|
||||
rowsReturned: "Linhas",
|
||||
tableView: "Tabela",
|
||||
rawJson: "JSON bruto",
|
||||
},
|
||||
overview: {
|
||||
statistics: "Estatísticas",
|
||||
instance: "Instância",
|
||||
replication: "Replicação",
|
||||
healthcheck: "Verificação de integridade",
|
||||
lastModified: "Última modificação",
|
||||
numDocs: "Número de documentos",
|
||||
maxDoc: "Número máximo de documentos",
|
||||
deletedDocs: "Documentos excluídos",
|
||||
version: "Versão",
|
||||
segmentCount: "Número de segmentos",
|
||||
current: "Atual",
|
||||
cwd: "Diretório de trabalho",
|
||||
instanceDir: "Diretório da instância",
|
||||
data: "Diretório de dados",
|
||||
index: "Diretório do índice",
|
||||
impl: "Classe de implementação",
|
||||
colVersion: "Versão",
|
||||
colGen: "Gen",
|
||||
colSize: "Tamanho",
|
||||
leaderSearching: "Líder (pesquisando)",
|
||||
leaderReplicable: "Líder (replicável)",
|
||||
noReplication: "Este core não possui detalhes de replicação do líder.",
|
||||
noHealthcheck: "O manipulador de requisições Ping não configurou o arquivo healthcheck.",
|
||||
justNow: "Agora mesmo",
|
||||
minutesAgo: "Há cerca de {n} minutos",
|
||||
hoursAgo: "Há cerca de {n} horas",
|
||||
daysAgo: "Há cerca de {n} dias",
|
||||
},
|
||||
replication: {
|
||||
refresh: "Atualizar status",
|
||||
disable: "Desabilitar replicação",
|
||||
enable: "Habilitar replicação",
|
||||
index: "Índice",
|
||||
settingsLeader: "Configurações (Líder)",
|
||||
settingsFollower: "Configurações (Seguidor)",
|
||||
noDetails: "Este core não possui detalhes de replicação.",
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -4185,6 +4185,154 @@ export default withEnglishFallback({
|
||||
unknown: "未知",
|
||||
},
|
||||
},
|
||||
solrAdmin: {
|
||||
title: "Solr 管理",
|
||||
refresh: "刷新",
|
||||
selectCore: "选择 core",
|
||||
noCore: "未选择 core。请先选择一个 core 再查看 core 级管理页。",
|
||||
emptyBody: "响应体为空。",
|
||||
optional: "可选",
|
||||
sections: {
|
||||
server: "服务器",
|
||||
core: "Core",
|
||||
},
|
||||
views: {
|
||||
dashboard: "仪表盘",
|
||||
logging: "日志",
|
||||
javaProps: "Java 属性",
|
||||
threads: "线程转储",
|
||||
coreAdmin: "Core 管理",
|
||||
metrics: "指标",
|
||||
overview: "概览",
|
||||
analysis: "分词分析",
|
||||
documents: "文档",
|
||||
paramsets: "参数集",
|
||||
files: "文件",
|
||||
ping: "Ping",
|
||||
plugins: "插件 / 统计",
|
||||
query: "查询",
|
||||
replication: "复制",
|
||||
schema: "Schema",
|
||||
segments: "段信息",
|
||||
},
|
||||
dashboard: {
|
||||
instance: "实例",
|
||||
start: "启动时间",
|
||||
uptime: "运行时长",
|
||||
versions: "版本",
|
||||
jvmTitle: "JVM",
|
||||
runtime: "运行时",
|
||||
args: "启动参数",
|
||||
system: "系统",
|
||||
physMem: "物理内存",
|
||||
swap: "交换空间",
|
||||
fd: "文件描述符数",
|
||||
jvmMemory: "JVM 内存",
|
||||
outdatedWarning: "该 Solr 版本发布已超过一年,建议升级。",
|
||||
mode: "运行模式",
|
||||
solr_home: "Solr Home",
|
||||
core_root: "Core 根目录",
|
||||
processors: "处理器数",
|
||||
},
|
||||
logging: {
|
||||
name: "Logger",
|
||||
level: "级别",
|
||||
set: "已设置",
|
||||
},
|
||||
core: {
|
||||
name: "Core",
|
||||
docs: "文档数",
|
||||
size: "大小",
|
||||
segments: "段数",
|
||||
uptime: "运行时长",
|
||||
ops: "操作",
|
||||
},
|
||||
schema: {
|
||||
fields: "个字段",
|
||||
dynamicFields: "个动态字段",
|
||||
fieldTypes: "个字段类型",
|
||||
name: "名称",
|
||||
type: "类型",
|
||||
flags: "标记",
|
||||
},
|
||||
analysis: {
|
||||
field: "字段名",
|
||||
indexValue: "索引侧文本",
|
||||
queryValue: "查询侧文本(可选)",
|
||||
indexPlaceholder: "写入该字段时分析的文本",
|
||||
queryPlaceholder: "查询时分析的文本",
|
||||
run: "分析",
|
||||
index: "索引链",
|
||||
query: "查询链",
|
||||
empty: "未返回分析阶段,请检查字段名是否正确。",
|
||||
},
|
||||
segments: {
|
||||
name: "段",
|
||||
deleted: "已删除",
|
||||
age: "年龄",
|
||||
source: "来源",
|
||||
},
|
||||
actions: {
|
||||
create: "新建 Core",
|
||||
reload: "重新加载",
|
||||
unload: "卸载",
|
||||
rename: "重命名",
|
||||
swap: "交换",
|
||||
confirm: '对 core "{core}" 执行 {action}?',
|
||||
success: '{action} "{core}" 执行成功',
|
||||
failed: "Core 管理操作失败:{message}",
|
||||
newName: "新 core 名称",
|
||||
swapWith: "与 core 交换",
|
||||
},
|
||||
queryForm: {
|
||||
defaultParser: "默认解析器",
|
||||
paramsets: "paramset(s)",
|
||||
jsonHint: "非空时改用 JSON DSL 提交",
|
||||
execute: "执行查询",
|
||||
openInConsole: "在控制台打开",
|
||||
rowsReturned: "行",
|
||||
tableView: "表格",
|
||||
rawJson: "原始 JSON",
|
||||
},
|
||||
overview: {
|
||||
statistics: "统计",
|
||||
instance: "实例",
|
||||
replication: "复制",
|
||||
healthcheck: "健康检查",
|
||||
lastModified: "最后修改",
|
||||
numDocs: "文档数",
|
||||
maxDoc: "最大文档数",
|
||||
deletedDocs: "已删除文档",
|
||||
version: "版本",
|
||||
segmentCount: "段数",
|
||||
current: "当前",
|
||||
cwd: "工作目录",
|
||||
instanceDir: "实例目录",
|
||||
data: "数据目录",
|
||||
index: "索引目录",
|
||||
impl: "实现类",
|
||||
colVersion: "版本",
|
||||
colGen: "Gen",
|
||||
colSize: "大小",
|
||||
leaderSearching: "Leader(检索中)",
|
||||
leaderReplicable: "Leader(可复制)",
|
||||
noReplication: "该 core 没有 leader 复制详情。",
|
||||
noHealthcheck: "Ping 请求处理器未配置 healthcheck 文件。",
|
||||
justNow: "刚刚",
|
||||
minutesAgo: "约 {n} 分钟前",
|
||||
hoursAgo: "约 {n} 小时前",
|
||||
daysAgo: "约 {n} 天前",
|
||||
},
|
||||
replication: {
|
||||
refresh: "刷新状态",
|
||||
disable: "禁用复制",
|
||||
enable: "启用复制",
|
||||
index: "索引",
|
||||
settingsLeader: "设置(Leader)",
|
||||
settingsFollower: "设置(Follower)",
|
||||
noDetails: "该 core 没有复制详情。",
|
||||
},
|
||||
},
|
||||
userAdmin: {
|
||||
title: "用户与权限",
|
||||
unsupported: "当前支持 MySQL 兼容与 PostgreSQL 兼容连接。SQL Server、Oracle 等会按各自权限模型继续扩展。",
|
||||
|
||||
@@ -10214,4 +10214,152 @@ export default withEnglishFallback({
|
||||
titleLabel: "標題(可選)",
|
||||
titlePlaceholder: "例如:查詢結果",
|
||||
},
|
||||
solrAdmin: {
|
||||
title: "Solr 管理",
|
||||
refresh: "重新整理",
|
||||
selectCore: "選擇 Core",
|
||||
noCore: "未選擇 Core。請先選擇一個 Core 再檢視 Core 層級管理頁。",
|
||||
emptyBody: "回應主體為空。",
|
||||
optional: "選填",
|
||||
sections: {
|
||||
server: "伺服器",
|
||||
core: "Core",
|
||||
},
|
||||
views: {
|
||||
dashboard: "儀表板",
|
||||
logging: "日誌",
|
||||
javaProps: "Java 屬性",
|
||||
threads: "執行緒傾印",
|
||||
coreAdmin: "Core 管理",
|
||||
metrics: "指標",
|
||||
overview: "總覽",
|
||||
analysis: "分詞分析",
|
||||
documents: "文件",
|
||||
paramsets: "參數集",
|
||||
files: "檔案",
|
||||
ping: "Ping",
|
||||
plugins: "外掛 / 統計",
|
||||
query: "查詢",
|
||||
replication: "複製",
|
||||
schema: "Schema",
|
||||
segments: "段資訊",
|
||||
},
|
||||
dashboard: {
|
||||
instance: "執行個體",
|
||||
start: "啟動時間",
|
||||
uptime: "運作時間",
|
||||
versions: "版本",
|
||||
jvmTitle: "JVM",
|
||||
runtime: "執行階段",
|
||||
args: "啟動參數",
|
||||
system: "系統",
|
||||
physMem: "實體記憶體",
|
||||
swap: "交換空間",
|
||||
fd: "檔案描述符數",
|
||||
jvmMemory: "JVM 記憶體",
|
||||
outdatedWarning: "此 Solr 版本發佈已超過一年,建議升級。",
|
||||
mode: "運作模式",
|
||||
solr_home: "Solr Home",
|
||||
core_root: "Core 根目錄",
|
||||
processors: "處理器數",
|
||||
},
|
||||
logging: {
|
||||
name: "Logger",
|
||||
level: "等級",
|
||||
set: "已設定",
|
||||
},
|
||||
core: {
|
||||
name: "Core",
|
||||
docs: "文件數",
|
||||
size: "大小",
|
||||
segments: "段數",
|
||||
uptime: "運作時間",
|
||||
ops: "操作",
|
||||
},
|
||||
schema: {
|
||||
fields: "個欄位",
|
||||
dynamicFields: "個動態欄位",
|
||||
fieldTypes: "個欄位類型",
|
||||
name: "名稱",
|
||||
type: "類型",
|
||||
flags: "標記",
|
||||
},
|
||||
analysis: {
|
||||
field: "欄位名稱",
|
||||
indexValue: "索引端文字",
|
||||
queryValue: "查詢端文字(選填)",
|
||||
indexPlaceholder: "寫入該欄位時分析的文字",
|
||||
queryPlaceholder: "查詢時分析的文字",
|
||||
run: "分析",
|
||||
index: "索引鏈",
|
||||
query: "查詢鏈",
|
||||
empty: "未傳回分析階段,請檢查欄位名稱是否正確。",
|
||||
},
|
||||
segments: {
|
||||
name: "段",
|
||||
deleted: "已刪除",
|
||||
age: "年齡",
|
||||
source: "來源",
|
||||
},
|
||||
actions: {
|
||||
create: "新增 Core",
|
||||
reload: "重新載入",
|
||||
unload: "卸載",
|
||||
rename: "重新命名",
|
||||
swap: "交換",
|
||||
confirm: '對 Core "{core}" 執行 {action}?',
|
||||
success: '{action} "{core}" 執行成功',
|
||||
failed: "Core 管理操作失敗:{message}",
|
||||
newName: "新 Core 名稱",
|
||||
swapWith: "與 Core 交換",
|
||||
},
|
||||
queryForm: {
|
||||
defaultParser: "預設解析器",
|
||||
paramsets: "參數集",
|
||||
jsonHint: "非空時改用 JSON DSL 提交",
|
||||
execute: "執行查詢",
|
||||
openInConsole: "在主控台開啟",
|
||||
rowsReturned: "列",
|
||||
tableView: "表格",
|
||||
rawJson: "原始 JSON",
|
||||
},
|
||||
overview: {
|
||||
statistics: "統計",
|
||||
instance: "執行個體",
|
||||
replication: "複製",
|
||||
healthcheck: "健康檢查",
|
||||
lastModified: "最後修改",
|
||||
numDocs: "文件數",
|
||||
maxDoc: "最大文件數",
|
||||
deletedDocs: "已刪除文件",
|
||||
version: "版本",
|
||||
segmentCount: "段數",
|
||||
current: "目前",
|
||||
cwd: "工作目錄",
|
||||
instanceDir: "執行個體目錄",
|
||||
data: "資料目錄",
|
||||
index: "索引目錄",
|
||||
impl: "實作類別",
|
||||
colVersion: "版本",
|
||||
colGen: "Gen",
|
||||
colSize: "大小",
|
||||
leaderSearching: "Leader(搜尋中)",
|
||||
leaderReplicable: "Leader(可複製)",
|
||||
noReplication: "此 Core 沒有 Leader 複製詳細資料。",
|
||||
noHealthcheck: "Ping 請求處理常式未設定 healthcheck 檔案。",
|
||||
justNow: "剛剛",
|
||||
minutesAgo: "約 {n} 分鐘前",
|
||||
hoursAgo: "約 {n} 小時前",
|
||||
daysAgo: "約 {n} 天前",
|
||||
},
|
||||
replication: {
|
||||
refresh: "重新整理狀態",
|
||||
disable: "停用複製",
|
||||
enable: "啟用複製",
|
||||
index: "索引",
|
||||
settingsLeader: "設定(Leader)",
|
||||
settingsFollower: "設定(Follower)",
|
||||
noDetails: "此 Core 沒有複製詳細資料。",
|
||||
},
|
||||
},
|
||||
});
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { buildAiContext, buildSystemPrompt, type AiContext } from "@/lib/ai/ai";
|
||||
import type { ConnectionConfig, QueryTab } from "@/types/database";
|
||||
|
||||
const apiMock = vi.hoisted(() => ({
|
||||
listTables: vi.fn(),
|
||||
getColumns: vi.fn(),
|
||||
listIndexes: vi.fn(),
|
||||
listForeignKeys: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/backend/api", () => apiMock);
|
||||
|
||||
function solrConnection(): ConnectionConfig {
|
||||
return {
|
||||
id: "solr-1",
|
||||
name: "Solr",
|
||||
db_type: "solr",
|
||||
host: "127.0.0.1",
|
||||
port: 8983,
|
||||
username: "",
|
||||
password: "",
|
||||
};
|
||||
}
|
||||
|
||||
function queryTab(): QueryTab {
|
||||
return {
|
||||
id: "tab-1",
|
||||
title: "Query",
|
||||
connectionId: "solr-1",
|
||||
database: "default",
|
||||
sql: "GET /products/select?q=*:*",
|
||||
isExecuting: false,
|
||||
isCancelling: false,
|
||||
isExplaining: false,
|
||||
mode: "query",
|
||||
};
|
||||
}
|
||||
|
||||
function solrAiContext(): AiContext {
|
||||
return {
|
||||
connectionId: "solr-1",
|
||||
connectionName: "Solr",
|
||||
databaseType: "solr",
|
||||
database: "default",
|
||||
currentSql: "GET /products/select?q=*:*",
|
||||
tables: [
|
||||
{
|
||||
name: "products",
|
||||
tableType: "CORE",
|
||||
columns: [
|
||||
{ name: "id", data_type: "string", is_primary_key: true, is_nullable: false, column_default: null, extra: null },
|
||||
{ name: "title", data_type: "text_general", is_primary_key: false, is_nullable: true, column_default: null, extra: null },
|
||||
],
|
||||
},
|
||||
],
|
||||
sqlFiles: [],
|
||||
csvFiles: [],
|
||||
schemaScope: "database",
|
||||
truncated: false,
|
||||
};
|
||||
}
|
||||
|
||||
describe("Solr AI context", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
apiMock.listTables.mockResolvedValue([{ name: "products", table_type: "CORE" }]);
|
||||
apiMock.getColumns.mockResolvedValue([]);
|
||||
apiMock.listIndexes.mockResolvedValue([]);
|
||||
apiMock.listForeignKeys.mockResolvedValue([]);
|
||||
});
|
||||
|
||||
it("loads Solr cores as schema tables through the generic metadata path", async () => {
|
||||
const context = await buildAiContext(queryTab(), solrConnection());
|
||||
|
||||
expect(context.databaseType).toBe("solr");
|
||||
expect(apiMock.listTables).toHaveBeenCalledWith("solr-1", "default", "default");
|
||||
expect(apiMock.getColumns).toHaveBeenCalledWith("solr-1", "default", "default", "products");
|
||||
expect(context.tables.map((table) => table.name)).toEqual(["products"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Solr AI system prompt", () => {
|
||||
it("describes the REST-console request format instead of SQL", () => {
|
||||
const prompt = buildSystemPrompt("generate", solrAiContext(), "ask");
|
||||
|
||||
expect(prompt).toContain("Apache Solr");
|
||||
expect(prompt).toContain("METHOD /path");
|
||||
expect(prompt).toContain("Solr");
|
||||
// The prompt must not instruct the model to emit SQL for Solr.
|
||||
expect(prompt).not.toContain("adapt SQL to the active database dialect");
|
||||
expect(prompt).not.toContain("Generate SQL and explanations only");
|
||||
});
|
||||
|
||||
it("frames agent-mode tools with core semantics", () => {
|
||||
const prompt = buildSystemPrompt("query", solrAiContext(), "agent");
|
||||
|
||||
expect(prompt).toContain("list_tables");
|
||||
expect(prompt).toContain("execute_query");
|
||||
expect(prompt).toContain("METHOD /path");
|
||||
expect(prompt).toContain("core");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { documentStoreProviderFor } from "@/lib/app/documentStoreProvider";
|
||||
|
||||
describe("Solr document store provider", () => {
|
||||
it("uses the Solr document provider", () => {
|
||||
const provider = documentStoreProviderFor("solr");
|
||||
|
||||
expect(provider.kind).toBe("solr");
|
||||
const preview = provider.queryPreview({
|
||||
collection: "mycore",
|
||||
filterJson: '{"status":"paid"}',
|
||||
sortJson: '{"id":1}',
|
||||
skip: 0,
|
||||
limit: 100,
|
||||
});
|
||||
expect(preview).toContain("DBX SOLR QUERY DOCUMENTS");
|
||||
expect(preview).toContain('core: "mycore"');
|
||||
expect(preview).toContain("offset: 0");
|
||||
expect(preview).toContain("limit: 100");
|
||||
expect(preview).not.toContain("/_search");
|
||||
expect(preview).not.toContain("_routing");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,80 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { applyParsedConnectionUrl, connectionProfileForScheme, parseConnectionUrl } from "@/lib/connection/connectionUrl";
|
||||
import type { ConnectionConfig } from "@/types/database";
|
||||
|
||||
describe("Solr connection URLs", () => {
|
||||
it("rejects an empty host URL", () => {
|
||||
expect(() => parseConnectionUrl("", "solr")).toThrow("Connection URL is empty");
|
||||
});
|
||||
|
||||
it("parses the dedicated scheme with the default port and credentials", () => {
|
||||
expect(parseConnectionUrl("solr://solr:SolrRocks@search.example.com")).toMatchObject({
|
||||
dbType: "solr",
|
||||
driverProfile: "solr",
|
||||
driverLabel: "Apache Solr",
|
||||
host: "search.example.com",
|
||||
port: 8983,
|
||||
username: "solr",
|
||||
password: "SolrRocks",
|
||||
database: undefined,
|
||||
ssl: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps Solr selected for HTTP/HTTPS URLs", () => {
|
||||
expect(parseConnectionUrl("http://search.example.com:8984", "solr")).toMatchObject({
|
||||
dbType: "solr",
|
||||
host: "search.example.com",
|
||||
port: 8984,
|
||||
ssl: false,
|
||||
});
|
||||
expect(parseConnectionUrl("https://search.example.com?insecure=true", "solr")).toMatchObject({
|
||||
dbType: "solr",
|
||||
host: "search.example.com",
|
||||
port: 8983,
|
||||
urlParams: "insecure=true",
|
||||
ssl: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("does not treat the /solr context path as a database", () => {
|
||||
// The driver appends /solr itself; storing it as the database would make the
|
||||
// parsed value look like a selectable namespace that does not exist.
|
||||
for (const url of ["solr://search.example.com/solr", "http://search.example.com:8983/solr", "https://search.example.com/solr/"]) {
|
||||
expect(parseConnectionUrl(url, "solr")).toMatchObject({
|
||||
dbType: "solr",
|
||||
database: undefined,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it("does not resurrect a database through applyParsedConnectionUrl", () => {
|
||||
const parsed = parseConnectionUrl("solr://search.example.com/solr", "solr");
|
||||
const config = applyParsedConnectionUrl(
|
||||
{
|
||||
db_type: "solr",
|
||||
driver_profile: "solr",
|
||||
driver_label: "Apache Solr",
|
||||
host: "localhost",
|
||||
port: 8983,
|
||||
username: "",
|
||||
password: "",
|
||||
database: "stale-core",
|
||||
url_params: "",
|
||||
ssl: false,
|
||||
} as Omit<ConnectionConfig, "id">,
|
||||
parsed,
|
||||
);
|
||||
|
||||
expect(config.database).toBeUndefined();
|
||||
});
|
||||
|
||||
it("exposes the Solr scheme profile", () => {
|
||||
expect(connectionProfileForScheme("solr")).toEqual({
|
||||
type: "solr",
|
||||
profile: "solr",
|
||||
label: "Apache Solr",
|
||||
defaultPort: 8983,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -28,7 +28,7 @@ describe("Cloud Spanner connection dialog", () => {
|
||||
// database that opts out of one of them means updating the string here too.
|
||||
expect(dialogSource).toContain("<div v-if=\"form.db_type !== 'meilisearch' && form.db_type !== 'spanner'\" class=\"grid grid-cols-4 items-center gap-4\">");
|
||||
expect(dialogSource).toContain('<div v-if="form.db_type !== \'spanner\'" class="grid grid-cols-4 items-center gap-4">');
|
||||
expect(dialogSource).toContain("<div v-if=\"form.db_type !== 'hbase' && form.db_type !== 'meilisearch' && form.db_type !== 'spanner'\" class=\"grid grid-cols-4 items-center gap-4\">");
|
||||
expect(dialogSource).toContain("<div v-if=\"form.db_type !== 'hbase' && form.db_type !== 'meilisearch' && form.db_type !== 'solr' && form.db_type !== 'spanner'\" class=\"grid grid-cols-4 items-center gap-4\">");
|
||||
// Switching to the Spanner profile clears host and credentials.
|
||||
expect(dialogSource).toContain('if (profile.type === "spanner") {');
|
||||
// Save/Test gating and submit-time normalization.
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { classifySqlRisk, classifySqlStatementRisk } from "@/lib/sql/sqlRisk";
|
||||
import { sqlLooksLikeMutation } from "@/lib/database/readOnlyWriteAccess";
|
||||
|
||||
const SEARCH_REQUEST = 'POST /solr/mycore/select\n{\n "query": "*:*",\n "limit": 100\n}';
|
||||
|
||||
describe("Solr request risk", () => {
|
||||
it("treats queries and other retrieval requests as reads", () => {
|
||||
expect(classifySqlRisk(SEARCH_REQUEST, { dialect: "solr" }).risk).toBe("read");
|
||||
expect(classifySqlRisk("GET /solr/mycore/select?q=*:*", { dialect: "solr" }).risk).toBe("read");
|
||||
expect(classifySqlRisk("GET /solr/admin/cores?action=STATUS", { dialect: "solr" }).risk).toBe("read");
|
||||
expect(classifySqlRisk("HEAD /solr/mycore/admin/ping", { dialect: "solr" }).risk).toBe("read");
|
||||
expect(classifySqlRisk('POST /solr/mycore/query\n{"query":"*:*"}', { dialect: "solr" }).risk).toBe("read");
|
||||
expect(classifySqlRisk('POST /mycore/suggest\n{"suggest":{"q":"sol"}}', { dialect: "solr" }).risk).toBe("read");
|
||||
});
|
||||
|
||||
it("treats document updates as writes and admin/schema changes as dangerous", () => {
|
||||
expect(classifySqlRisk('POST /solr/mycore/update\n{"add":{"doc":{"id":"1"}}}', { dialect: "solr" }).risk).toBe("write");
|
||||
expect(classifySqlRisk('POST /mycore/update/json/docs\n{"a":1}', { dialect: "solr" }).risk).toBe("write");
|
||||
expect(classifySqlRisk('POST /solr/mycore/update?commit=true\n{"delete":{"query":"*:*"}}', { dialect: "solr" }).risk).toBe("write");
|
||||
expect(classifySqlRisk("POST /solr/admin/cores?action=CREATE&name=x", { dialect: "solr" }).risk).toBe("ddl");
|
||||
expect(classifySqlRisk('POST /solr/mycore/schema\n{"add-field":{"name":"f","type":"string"}}', { dialect: "solr" }).risk).toBe("ddl");
|
||||
expect(classifySqlRisk("DELETE /solr/mycore/update", { dialect: "solr" }).risk).toBe("write");
|
||||
expect(classifySqlRisk("DELETE /solr/mycore/config", { dialect: "solr" }).risk).toBe("ddl");
|
||||
});
|
||||
|
||||
it("treats GET-driven update and admin actions as mutations", () => {
|
||||
// Solr update handlers answer GET (commit/optimize/stream.body) and
|
||||
// CoreAdmin actions ride on GET as well, so the method alone cannot mark
|
||||
// these paths read-only.
|
||||
expect(classifySqlRisk("GET /solr/mycore/update?commit=true", { dialect: "solr" }).risk).toBe("write");
|
||||
expect(classifySqlRisk("GET /solr/admin/cores?action=CREATE&name=x", { dialect: "solr" }).risk).toBe("ddl");
|
||||
expect(classifySqlRisk("GET /solr/admin/cores?action=UNLOAD&core=mycore", { dialect: "solr" }).risk).toBe("ddl");
|
||||
expect(classifySqlRisk("GET /solr/admin/cores?action=REQUESTSTATUS&requestid=1", { dialect: "solr" }).risk).toBe("read");
|
||||
expect(classifySqlRisk("GET /solr/admin/info/system", { dialect: "solr" }).risk).toBe("read");
|
||||
});
|
||||
|
||||
it("treats GET replication commands outside the read whitelist as dangerous", () => {
|
||||
expect(classifySqlRisk("GET /solr/mycore/replication?command=details", { dialect: "solr" }).risk).toBe("read");
|
||||
expect(classifySqlRisk("GET /solr/mycore/replication?command=restorestatus", { dialect: "solr" }).risk).toBe("read");
|
||||
expect(classifySqlRisk("GET /solr/mycore/replication?command=filelist", { dialect: "solr" }).risk).toBe("read");
|
||||
expect(classifySqlRisk("GET /solr/mycore/replication", { dialect: "solr" }).risk).toBe("read");
|
||||
expect(classifySqlRisk("GET /solr/mycore/replication?command=disablereplication", { dialect: "solr" }).risk).toBe("ddl");
|
||||
expect(classifySqlRisk("GET /solr/mycore/replication?command=enablereplication", { dialect: "solr" }).risk).toBe("ddl");
|
||||
expect(classifySqlRisk("GET /solr/mycore/replication?command=fetchindex", { dialect: "solr" }).risk).toBe("ddl");
|
||||
});
|
||||
|
||||
it("reports the highest risk across every request in the editor text", () => {
|
||||
const source = `${SEARCH_REQUEST}\n\nPOST /solr/admin/cores?action=UNLOAD&core=mycore`;
|
||||
expect(classifySqlRisk(source, { dialect: "solr" }).risk).toBe("ddl");
|
||||
expect(classifySqlStatementRisk(SEARCH_REQUEST, { dialect: "solr" }).risk).toBe("read");
|
||||
});
|
||||
|
||||
it("reads the request line through query strings, trailing slashes and CRLF endings", () => {
|
||||
expect(classifySqlRisk("GET /solr/mycore/select?q=*:*&rows=1", { dialect: "solr" }).risk).toBe("read");
|
||||
expect(classifySqlRisk("post /solr/mycore/select/\r\n{}", { dialect: "solr" }).risk).toBe("read");
|
||||
expect(classifySqlRisk('POST /solr/mycore/update?commitWithin=500\n{"delete":{"id":"1"}}', { dialect: "solr" }).risk).toBe("write");
|
||||
expect(classifySqlRisk(" GET /solr/mycore/select // inline comment", { dialect: "solr" }).risk).toBe("read");
|
||||
});
|
||||
|
||||
it("ignores commented-out requests", () => {
|
||||
expect(classifySqlRisk(`# DELETE /solr/mycore/config\n// DELETE /solr/mycore/config\n/* DELETE /solr/mycore/config */\n${SEARCH_REQUEST}`, { dialect: "solr" }).risk).toBe("read");
|
||||
expect(classifySqlRisk("/*\nDELETE /solr/mycore/config\n*/\nGET /solr/mycore/select", { dialect: "solr" }).risk).toBe("read");
|
||||
});
|
||||
|
||||
it("leaves text that does not start with a request line to the SQL rules", () => {
|
||||
// The backend anchors on the first line too, so a stray request line further
|
||||
// down must not turn unparseable text into a read.
|
||||
expect(classifySqlRisk('{"query":"*:*"}\nGET /solr/mycore/select', { dialect: "solr" }).risk).toBe("unknown");
|
||||
});
|
||||
|
||||
it("keeps REST requests classified as SQL for other database types", () => {
|
||||
expect(classifySqlRisk(SEARCH_REQUEST, { dialect: "mysql" }).risk).toBe("unknown");
|
||||
expect(classifySqlRisk(SEARCH_REQUEST, { dialect: "elasticsearch" }).risk).not.toBe("read");
|
||||
});
|
||||
|
||||
it("does not ask a read-only connection to unlock writes for a search", () => {
|
||||
expect(sqlLooksLikeMutation(SEARCH_REQUEST, "solr")).toBe(false);
|
||||
expect(sqlLooksLikeMutation("GET /solr/mycore/select?q=*:*", "solr")).toBe(false);
|
||||
expect(sqlLooksLikeMutation('POST /solr/mycore/update\n{"delete":{"query":"*:*"}}', "solr")).toBe(true);
|
||||
expect(sqlLooksLikeMutation("POST /solr/admin/cores?action=CREATE&name=x", "solr")).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -32,6 +32,7 @@ function dbLabel(dbType: DatabaseType): string {
|
||||
milvus: "Milvus",
|
||||
weaviate: "Weaviate",
|
||||
chromadb: "ChromaDB",
|
||||
solr: "Apache Solr",
|
||||
};
|
||||
return labels[dbType] || dbType;
|
||||
}
|
||||
@@ -318,6 +319,9 @@ export function buildSystemPrompt(action: AiAction, context: AiContext, mode: Ai
|
||||
if (context.databaseType === "redis") {
|
||||
return buildRedisSystemPrompt(context, mode, custom);
|
||||
}
|
||||
if (context.databaseType === "solr") {
|
||||
return buildSolrSystemPrompt(context, mode, custom);
|
||||
}
|
||||
const schema = formatSchema(context);
|
||||
const resultPreview = context.lastResultPreview ? `\nLast result preview:\n${context.lastResultPreview}\n` : "";
|
||||
const lastError = context.lastError ? `\nLast error:\n${context.lastError}\n` : "";
|
||||
@@ -397,6 +401,59 @@ function buildRedisSystemPrompt(context: AiContext, mode: AiAssistantMode, custo
|
||||
return lines.filter(Boolean).join("\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Solr prompt: Solr speaks REST, not SQL, so the generic dialect instructions
|
||||
* would push the model toward SELECT statements that the Solr driver rejects.
|
||||
* The query surface is the DBX REST console (`METHOD /path` + optional JSON
|
||||
* body); schema context entries are Solr cores whose "columns" are schema
|
||||
* fields (the PK-flagged field is the core's uniqueKey).
|
||||
*/
|
||||
function buildSolrSystemPrompt(context: AiContext, mode: AiAssistantMode, custom?: CustomPromptContext): string {
|
||||
const isZh = isChineseLocale(currentLocale());
|
||||
const schema = formatSchema(context);
|
||||
const resultPreview = context.lastResultPreview ? `\nLast result preview:\n${context.lastResultPreview}\n` : "";
|
||||
const lastError = context.lastError ? `\nLast error:\n${context.lastError}\n` : "";
|
||||
const referencedSqlFiles = formatReferencedSqlFiles(context);
|
||||
const lines: string[] = [
|
||||
isZh ? "你是 DBX 内置的 Apache Solr 助手。用中文回复。" : "You are DBX's built-in Apache Solr assistant. Reply in English.",
|
||||
isZh
|
||||
? 'Solr 不使用 SQL。查询一律使用 DBX REST 控制台格式:首行 `METHOD /path`,后续可选 JSON 请求体,例如 `GET /{core}/select?q=*:*&rows=20`、`POST /{core}/query` 加 {"query":"..."} JSON body。路径可以省略开头的 /solr 段,wt=json 会自动补上。'
|
||||
: 'Solr does not use SQL. Always express queries in the DBX REST-console format: `METHOD /path` on the first line plus an optional JSON body, e.g. `GET /{core}/select?q=*:*&rows=20` or `POST /{core}/query` with a body such as {"query":"..."}. A leading /solr path segment may be omitted and wt=json is appended automatically.',
|
||||
isZh
|
||||
? "Schema 上下文中每个条目是一个 Solr core(类型标注为 CORE);其下列出的是 schema fields,标记为 PK 的字段是该 core 的唯一键(uniqueKey)。用真实字段名构造 q/fq 参数,不要编造不存在的字段。"
|
||||
: "Each entry in the schema context is a Solr core (marked CORE); the listed items are its schema fields and the PK-flagged field is the core's uniqueKey. Build q/fq parameters from real field names and never invent fields that are not listed.",
|
||||
...buildModePromptLines(mode, isZh, context.databaseType),
|
||||
...buildRichContentPromptLines(isZh),
|
||||
...buildCustomInstructionLines(custom, isZh),
|
||||
attachmentSafetyInstruction(isZh),
|
||||
// The ```sql fence is only a transport convention: the editor treats the
|
||||
// block content as a Solr REST request, never as SQL.
|
||||
isZh ? "返回请求时放在 ```sql 代码块中,块内容是 Solr REST 请求文本而非 SQL。额外说明简短实用。" : "Put the request in a fenced ```sql code block; the block holds Solr REST request text, not SQL. Keep extra explanation short and practical.",
|
||||
"",
|
||||
"Database type: solr",
|
||||
`Connection: ${context.connectionName}`,
|
||||
`Database: ${context.database}`,
|
||||
context.schema ? `Selected schema: ${context.schema}` : "",
|
||||
schemaCoverageLine(context, isZh),
|
||||
"",
|
||||
`Current request:\n${context.currentSql.trim() || "(empty)"}`,
|
||||
referencedSqlFiles,
|
||||
lastError,
|
||||
resultPreview,
|
||||
`Schema:\n${schema}`,
|
||||
];
|
||||
|
||||
if (context.schemaScope === "focused_table") {
|
||||
lines.push(
|
||||
isZh
|
||||
? "Schema 上下文只覆盖当前打开的 core;连接中可能还有其他 core。用户询问有哪些 core 或提到上下文中不存在的 core 时,不要直接断言不存在,先用 list_tables 确认。"
|
||||
: "Schema context covers only the currently opened core; the connection may contain other cores. When the user asks what cores exist or mentions a core absent from context, do not conclude it is missing; use list_tables to verify first.",
|
||||
);
|
||||
}
|
||||
|
||||
return lines.filter(Boolean).join("\n");
|
||||
}
|
||||
|
||||
function buildBasePromptLines(isZh: boolean): string[] {
|
||||
return [
|
||||
isZh ? "你是 DBX 内置的数据库助手。用中文回复。" : "You are DBX's built-in database assistant. Reply in English.",
|
||||
@@ -554,6 +611,28 @@ function buildModePromptLines(mode: AiAssistantMode, isZh: boolean, databaseType
|
||||
currentTimeGuidance,
|
||||
];
|
||||
}
|
||||
if (databaseType === "solr") {
|
||||
if (mode === "agent") {
|
||||
return [
|
||||
isZh
|
||||
? "你处于 Solr Agent 模式。你有以下工具可用:list_tables(列出 Solr cores)、get_columns(返回某个 core 的 schema fields)、execute_query、get_current_time。"
|
||||
: "You are in Solr Agent mode. You have the following tools available: list_tables (lists Solr cores), get_columns (returns a core's schema fields), execute_query, get_current_time.",
|
||||
isZh
|
||||
? "execute_query 接受 DBX REST 控制台格式的 Solr 请求而不是 SQL:首行 `METHOD /path`,后续可选 JSON body,例如 `GET /{core}/select?q=*:*&rows=20` 或 `POST /{core}/query` 加 JSON body。用户提出数据查询意图时,必须调用该工具获取真实结果后再回答。"
|
||||
: "execute_query accepts Solr REST-console requests, not SQL: `METHOD /path` on the first line plus an optional JSON body, for example `GET /{core}/select?q=*:*&rows=20` or `POST /{core}/query` with a JSON body. For data queries, call the tool and answer from its actual results.",
|
||||
currentTimeGuidance,
|
||||
isZh
|
||||
? "禁止不经确认直接执行 Solr 写请求(`/{core}/update`、commit、schema/admin 变更);如果安全执行条件不满足,先说明原因,再给出只读替代方案。"
|
||||
: "Never execute Solr write requests (`/{core}/update`, commits, schema/admin changes) without confirmation. If safe execution requirements are not met, explain why and provide a read-only alternative.",
|
||||
];
|
||||
}
|
||||
return [
|
||||
isZh
|
||||
? "你处于 Solr Ask 模式。只生成 Solr REST 请求(`METHOD /path` + 可选 JSON body)和说明,不要生成 SQL,也不要暗示已经执行或即将自动执行。"
|
||||
: "You are in Solr Ask mode. Generate Solr REST requests (`METHOD /path` plus optional JSON body) and explanations only, not SQL, and do not imply that anything has run or will auto-run.",
|
||||
currentTimeGuidance,
|
||||
];
|
||||
}
|
||||
if (mode === "agent") {
|
||||
return [
|
||||
isZh ? "你处于 Agent 模式。你有以下工具可用:list_tables、get_columns、execute_query、get_sample_data、get_current_time。" : "You are in Agent mode. You have the following tools available: list_tables, get_columns, execute_query, get_sample_data, get_current_time.",
|
||||
|
||||
@@ -41,6 +41,9 @@ export function documentStoreValueForGrid(value: unknown, kind: DocumentStoreKin
|
||||
export function serializeDocumentStoreId(value: unknown, kind: DocumentStoreKind): string {
|
||||
if (kind !== "mongodb" && isLosslessJsonNumber(value)) return value.raw;
|
||||
if (kind === "elasticsearch") return String(value);
|
||||
// Solr document ids are the uniqueKey field value verbatim — no quoting or
|
||||
// sentinel encoding like Meilisearch's string-id marker.
|
||||
if (kind === "solr") return String(value);
|
||||
if (kind === "meilisearch") return typeof value === "string" ? `__dbx_meilisearch_string_id__${JSON.stringify(value)}` : String(value);
|
||||
if (kind === "dynamodb") return stringifyJsonPreservingLargeNumbers(value);
|
||||
return serializeMongoDocumentId(value);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { ComposerTranslation } from "vue-i18n";
|
||||
import { normalizeJsonArgument } from "@dbx-app/mongo-shell";
|
||||
import { isElasticsearchCompatibleDatabaseType, isMeilisearchDatabaseType, type DatabaseType } from "@/types/database";
|
||||
import { isElasticsearchCompatibleDatabaseType, isMeilisearchDatabaseType, isSolrDatabaseType, type DatabaseType } from "@/types/database";
|
||||
import { quoteUnquotedObjectKeys } from "@/lib/mongo/mongoShellCommand";
|
||||
import { formatMongoShellLiteral } from "@/lib/mongo/mongoDocumentValues";
|
||||
|
||||
export type DocumentStoreKind = "mongodb" | "dynamodb" | "elasticsearch" | "meilisearch";
|
||||
export type DocumentStoreKind = "mongodb" | "dynamodb" | "elasticsearch" | "meilisearch" | "solr";
|
||||
export type DocumentFilterMode = "equals" | "not-equals" | "like" | "not-like" | "begins-with" | "ends-with" | "greater-than" | "greater-than-or-equal" | "less-than" | "less-than-or-equal" | "in" | "not-in" | "between" | "not-between" | "is-null" | "is-not-null";
|
||||
export type DocumentFilterValueType = "auto" | "string" | "number" | "boolean" | "object-id" | "date" | "int32" | "int64" | "decimal128" | "json";
|
||||
export type ElasticsearchBoolClause = "filter" | "must" | "should" | "must_not";
|
||||
@@ -80,7 +80,9 @@ const MONGO_ONLY_DOCUMENT_FILTER_MODES = new Set<DocumentFilterMode>(["begins-wi
|
||||
|
||||
export function documentFilterModeOptionsFor(kind: DocumentStoreKind): Array<{ value: DocumentFilterMode; labelKey: string }> {
|
||||
if (kind === "meilisearch") return documentFilterModeOptions.filter((option) => option.value !== "like" && option.value !== "not-like" && !MONGO_ONLY_DOCUMENT_FILTER_MODES.has(option.value));
|
||||
if (kind === "mongodb") return documentFilterModeOptions;
|
||||
// Solr's driver translates every mode — including $in/$nin and range pairs —
|
||||
// into fq clauses, so the full Mongo-style operator set is available.
|
||||
if (kind === "mongodb" || kind === "solr") return documentFilterModeOptions;
|
||||
return documentFilterModeOptions.filter((option) => !MONGO_ONLY_DOCUMENT_FILTER_MODES.has(option.value));
|
||||
}
|
||||
|
||||
@@ -166,6 +168,22 @@ const meilisearchDocumentProvider: DocumentStoreProvider = {
|
||||
sortInputForColumn: mongoDocumentProvider.sortInputForColumn,
|
||||
};
|
||||
|
||||
const solrDocumentProvider: DocumentStoreProvider = {
|
||||
kind: "solr",
|
||||
filterInputLabel: "filter",
|
||||
sortInputLabel: "sort",
|
||||
documentsLabel: ({ total }) => `${total} Documents`,
|
||||
queryPreview: ({ collection, filterJson, sortJson, skip, limit }) => {
|
||||
const lines = ["DBX SOLR QUERY DOCUMENTS", `core: ${JSON.stringify(collection)}`, `offset: ${skip}`, `limit: ${limit}`];
|
||||
const filter = documentStorePreviewJson(filterJson);
|
||||
if (filter) lines.push("filter:", filter);
|
||||
const sort = documentStorePreviewJson(sortJson);
|
||||
if (sort) lines.push("sort:", sort);
|
||||
return lines.join("\n");
|
||||
},
|
||||
sortInputForColumn: mongoDocumentProvider.sortInputForColumn,
|
||||
};
|
||||
|
||||
const dynamodbDocumentProvider: DocumentStoreProvider = {
|
||||
kind: "dynamodb",
|
||||
filterInputLabel: "filter",
|
||||
@@ -197,6 +215,7 @@ export function documentStoreProviderFor(databaseType?: DatabaseType): DocumentS
|
||||
if (databaseType === "dynamodb") return dynamodbDocumentProvider;
|
||||
if (isElasticsearchCompatibleDatabaseType(databaseType)) return elasticsearchDocumentProvider;
|
||||
if (isMeilisearchDatabaseType(databaseType)) return meilisearchDocumentProvider;
|
||||
if (isSolrDatabaseType(databaseType)) return solrDocumentProvider;
|
||||
return mongoDocumentProvider;
|
||||
}
|
||||
|
||||
@@ -590,7 +609,10 @@ function escapeRegexLiteral(value: string): string {
|
||||
}
|
||||
|
||||
function mongoDocumentFilterKind(options: DocumentFilterParseOptions): boolean {
|
||||
return options.kind === undefined || options.kind === "mongodb";
|
||||
// Solr shares the Mongo-style filter document — the driver translates $in,
|
||||
// $gte/$lte and anchored $regex into fq clauses — so it uses the same
|
||||
// operator-rich condition shapes.
|
||||
return options.kind === undefined || options.kind === "mongodb" || options.kind === "solr";
|
||||
}
|
||||
|
||||
/** Splits a comma/newline separated filter value list, keeping quoted values intact. */
|
||||
@@ -824,6 +846,7 @@ const documentQueryInputNormalizers: Record<DocumentStoreKind, DocumentQueryInpu
|
||||
dynamodb: quoteUnquotedObjectKeys,
|
||||
elasticsearch: quoteUnquotedObjectKeys,
|
||||
meilisearch: quoteUnquotedObjectKeys,
|
||||
solr: quoteUnquotedObjectKeys,
|
||||
};
|
||||
|
||||
function normalizeDocumentQueryObjectInput(input: string, kind?: DocumentStoreKind): string {
|
||||
|
||||
@@ -14,6 +14,13 @@ export function formatMeilisearchDocumentOperationPreview(options: { action: "in
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
export function formatSolrDocumentOperationPreview(options: { action: "insert" | "upsert" | "update" | "delete"; core: string; id?: unknown; document?: Record<string, unknown> }): string {
|
||||
const lines = [`DBX SOLR ${options.action.toUpperCase()} DOCUMENT`, `core: ${JSON.stringify(options.core)}`];
|
||||
if (options.id !== undefined) lines.push(`id: ${stringifyDocumentStoreValue(options.id, "solr")}`);
|
||||
if (options.document) lines.push("document:", stringifyDocumentStoreValue(options.document, "solr", 2));
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a document body under a known identity.
|
||||
* - `put`: Elasticsearch index-by-id / Mongo update-by-id (identity via path, not body).
|
||||
|
||||
@@ -142,6 +142,7 @@ export function connectionDisplayUrlScheme(connection: Pick<ConnectionConfig, "d
|
||||
case "elasticsearch":
|
||||
case "easysearch":
|
||||
case "meilisearch":
|
||||
case "solr":
|
||||
case "qdrant":
|
||||
case "milvus":
|
||||
case "weaviate":
|
||||
@@ -233,6 +234,9 @@ export function connectionUrlPlaceholder(dbType: DatabaseType, driverProfile?: s
|
||||
case "meilisearch":
|
||||
return "http://host:port/base/path";
|
||||
|
||||
case "solr":
|
||||
return "http://user:password@host:8983";
|
||||
|
||||
case "dameng":
|
||||
return "dm://user:password@host:port";
|
||||
|
||||
|
||||
@@ -59,6 +59,7 @@ const SCHEME_PROFILES: Record<string, ConnectionProfile> = {
|
||||
elasticsearch: { type: "elasticsearch", profile: "elasticsearch", label: "Elasticsearch", defaultPort: 9200 },
|
||||
easysearch: { type: "easysearch", profile: "easysearch", label: "Easysearch", defaultPort: 9200 },
|
||||
meilisearch: { type: "meilisearch", profile: "meilisearch", label: "Meilisearch", defaultPort: 7700 },
|
||||
solr: { type: "solr", profile: "solr", label: "Apache Solr", defaultPort: 8983 },
|
||||
qdrant: { type: "qdrant", profile: "qdrant", label: "Qdrant", defaultPort: 6333 },
|
||||
milvus: { type: "milvus", profile: "milvus", label: "Milvus", defaultPort: 19530 },
|
||||
weaviate: { type: "weaviate", profile: "weaviate", label: "Weaviate", defaultPort: 8080 },
|
||||
@@ -97,6 +98,7 @@ const HTTP_SELECTED_PROFILES: Record<string, ConnectionProfile> = {
|
||||
elasticsearch: SCHEME_PROFILES.elasticsearch,
|
||||
easysearch: SCHEME_PROFILES.easysearch,
|
||||
meilisearch: SCHEME_PROFILES.meilisearch,
|
||||
solr: SCHEME_PROFILES.solr,
|
||||
qdrant: SCHEME_PROFILES.qdrant,
|
||||
milvus: SCHEME_PROFILES.milvus,
|
||||
weaviate: SCHEME_PROFILES.weaviate,
|
||||
@@ -795,7 +797,7 @@ export function parseConnectionUrl(value: string, preferredProfile?: string): Pa
|
||||
...(profile.type === "sqlserver" && parsed.port ? { portExplicit: true } : {}),
|
||||
username: jdbcCredentials?.username ?? decodeUrlPart(parsed.username),
|
||||
password: jdbcCredentials?.password ?? decodeUrlPart(parsed.password),
|
||||
database: profile.type === "victoriametrics" ? "metrics" : profile.type === "dynamodb" ? dynamodbRegionFromHost(parsed.hostname) : isMeilisearch ? undefined : pathDatabase,
|
||||
database: profile.type === "victoriametrics" ? "metrics" : profile.type === "dynamodb" ? dynamodbRegionFromHost(parsed.hostname) : isMeilisearch || profile.type === "solr" ? undefined : pathDatabase,
|
||||
urlParams: effectiveUrlParams,
|
||||
ssl: scheme === "rediss" || scheme === "https" || (redisPath?.tls ?? false) || urlParamsRequireTls(profile.type, effectiveUrlParams) || (profile.type === "mysql" && isTidbCloudHost(parsed.hostname)),
|
||||
...(profile.type === "victoriametrics" ? { apiPath: parsed.pathname.replace(/\/+$/, "") } : {}),
|
||||
|
||||
@@ -74,7 +74,7 @@ const STANDARD_URL_SCHEMES: Partial<Record<DatabaseType, string>> = {
|
||||
};
|
||||
|
||||
/** Schemes that are plain HTTP(S) endpoints. */
|
||||
const HTTP_URL_DB_TYPES = new Set<DatabaseType>(["elasticsearch", "easysearch", "meilisearch", "qdrant", "milvus", "weaviate", "chromadb", "rqlite", "consul", "victoriametrics", "influxdb", "influxdb3", "dynamodb"]);
|
||||
const HTTP_URL_DB_TYPES = new Set<DatabaseType>(["elasticsearch", "easysearch", "meilisearch", "solr", "qdrant", "milvus", "weaviate", "chromadb", "rqlite", "consul", "victoriametrics", "influxdb", "influxdb3", "dynamodb"]);
|
||||
|
||||
/**
|
||||
* Generic `prefix://host:port/db` JDBC dialects. Dialects with a different URL
|
||||
|
||||
@@ -5,7 +5,7 @@ const DRAFT_VISIBLE_DATABASES_PREFIX = "__visible_draft_";
|
||||
|
||||
// Turso and Cloudflare D1 target one fixed SQLite-compatible `main` namespace;
|
||||
// non-database services expose their own root objects rather than database namespaces.
|
||||
const UNSUPPORTED_VISIBLE_DATABASE_TYPES = new Set<DatabaseType>(["turso", "cloudflare-d1", "dynamodb", "elasticsearch", "easysearch", "meilisearch", "qdrant", "milvus", "weaviate", "chromadb", "etcd", "zookeeper", "mq", "nacos", "consul", "plugin"]);
|
||||
const UNSUPPORTED_VISIBLE_DATABASE_TYPES = new Set<DatabaseType>(["turso", "cloudflare-d1", "dynamodb", "elasticsearch", "easysearch", "meilisearch", "solr", "qdrant", "milvus", "weaviate", "chromadb", "etcd", "zookeeper", "mq", "nacos", "consul", "plugin"]);
|
||||
|
||||
type VisibleDatabaseConnectionFields = Pick<
|
||||
ConnectionConfig,
|
||||
|
||||
@@ -25,7 +25,7 @@ export function isColumnNameCopySeparator(value: unknown): value is ColumnNameCo
|
||||
}
|
||||
|
||||
// SQL 执行能力是产品能力,不代表数据库支持 SQL 标识符引用。
|
||||
const NON_SQL_DATABASE_TYPES = new Set<DatabaseType>(["redis", "mongodb", "elasticsearch", "easysearch", "meilisearch", "qdrant", "milvus", "weaviate", "chromadb", "etcd", "zookeeper", "consul", "mq", "nacos", "victoriametrics"]);
|
||||
const NON_SQL_DATABASE_TYPES = new Set<DatabaseType>(["redis", "mongodb", "elasticsearch", "easysearch", "meilisearch", "solr", "qdrant", "milvus", "weaviate", "chromadb", "etcd", "zookeeper", "consul", "mq", "nacos", "victoriametrics"]);
|
||||
// quoteTableIdentifier 对 jdbc/iotdb 原样返回(无引用字符可用)。
|
||||
const UNQUOTABLE_DATABASE_TYPES = new Set<DatabaseType>(["jdbc", "iotdb"]);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { isElasticsearchCompatibleDatabaseType, type DatabaseType } from "@/types/database";
|
||||
import { isElasticsearchCompatibleDatabaseType, isSolrDatabaseType, type DatabaseType } from "@/types/database";
|
||||
|
||||
/**
|
||||
* Resolve the data type to display in a data-grid column header.
|
||||
@@ -207,6 +207,13 @@ const INTEGER_COLUMN_TYPE_BASES = new Set([
|
||||
// BYTE is binary), so they only apply to Elasticsearch-compatible databases.
|
||||
const ELASTICSEARCH_ONLY_INTEGER_COLUMN_TYPE_BASES = new Set(["byte", "short", "long"]);
|
||||
|
||||
// Solr schema field types: point/trie numerics and dates carry a `p`/`t`
|
||||
// prefix, and analyzed text fields are named `text_*` — none of those names
|
||||
// exist in the SQL type palette, so they resolve only under a Solr connection.
|
||||
const SOLR_INTEGER_COLUMN_TYPE_BASES = new Set(["pint", "plong", "tint", "tlong", "sint", "slong"]);
|
||||
const SOLR_NUMERIC_COLUMN_TYPE_BASES = new Set(["pfloat", "pdouble", "tfloat", "tdouble", "sfloat", "sdouble", "pcurrency", "currency"]);
|
||||
const SOLR_TEMPORAL_COLUMN_TYPE_BASES = new Set(["pdate", "tdate"]);
|
||||
|
||||
const STRING_COLUMN_TYPE_BASES = new Set([
|
||||
"varchar",
|
||||
"varchar2",
|
||||
@@ -300,8 +307,14 @@ export function resolveDataGridTypeVisualKind(dataType: string | undefined, data
|
||||
if (array) return "structured";
|
||||
if (databaseType === "sqlserver" && (base === "timestamp" || base === "rowversion")) return "binary";
|
||||
if (databaseType === "postgres" && (base === "bit" || base === "bit varying")) return "binary";
|
||||
if (isElasticsearchCompatibleDatabaseType(databaseType) && ELASTICSEARCH_ONLY_INTEGER_COLUMN_TYPE_BASES.has(base)) return "integer";
|
||||
if ((isElasticsearchCompatibleDatabaseType(databaseType) || isSolrDatabaseType(databaseType)) && ELASTICSEARCH_ONLY_INTEGER_COLUMN_TYPE_BASES.has(base)) return "integer";
|
||||
if (INTEGER_COLUMN_TYPE_BASES.has(base)) return "integer";
|
||||
if (isSolrDatabaseType(databaseType)) {
|
||||
if (SOLR_INTEGER_COLUMN_TYPE_BASES.has(base)) return "integer";
|
||||
if (SOLR_NUMERIC_COLUMN_TYPE_BASES.has(base)) return "numeric";
|
||||
if (SOLR_TEMPORAL_COLUMN_TYPE_BASES.has(base)) return "temporal";
|
||||
if (base.startsWith("text")) return "string";
|
||||
}
|
||||
if (isNumericColumnType(dataType)) return "numeric";
|
||||
if (BOOLEAN_COLUMN_TYPE_BASES.has(base)) return "boolean";
|
||||
if (TEMPORAL_COLUMN_TYPE_BASES.has(base) || base.startsWith("timestamp_")) return "temporal";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { DatabaseType } from "@/types/database";
|
||||
|
||||
const NON_SQL_PREVIEW_DATABASE_TYPES: ReadonlySet<DatabaseType> = new Set<DatabaseType>(["mongodb", "elasticsearch", "easysearch", "meilisearch"]);
|
||||
const NON_SQL_PREVIEW_DATABASE_TYPES: ReadonlySet<DatabaseType> = new Set<DatabaseType>(["mongodb", "elasticsearch", "easysearch", "meilisearch", "solr"]);
|
||||
|
||||
export function dataGridPreviewLabelKey(databaseType?: DatabaseType): "toolbar.previewQuery" | "toolbar.previewSql" {
|
||||
return databaseType && NON_SQL_PREVIEW_DATABASE_TYPES.has(databaseType) ? "toolbar.previewQuery" : "toolbar.previewSql";
|
||||
|
||||
@@ -33,6 +33,7 @@ export const DATABASE_NAMESPACE_CREATION_MATRIX = {
|
||||
elasticsearch: { deferred: "index creation is not modeled as database creation" },
|
||||
easysearch: { deferred: "index creation is not modeled as database creation" },
|
||||
meilisearch: { deferred: "index creation is not modeled as database creation" },
|
||||
solr: { deferred: "core creation is not modeled as database creation" },
|
||||
hbase: { deferred: "namespace creation needs dedicated HBase namespace options" },
|
||||
qdrant: { deferred: "collection creation is separate from database creation" },
|
||||
milvus: { deferred: "collection/database lifecycle needs a dedicated vector workflow" },
|
||||
|
||||
@@ -33,6 +33,7 @@ export const DATABASE_PROPERTY_EDITING_MATRIX = {
|
||||
elasticsearch: { deferred: "index settings are not database properties" },
|
||||
easysearch: { deferred: "index settings are not database properties" },
|
||||
meilisearch: { deferred: "index settings are not database properties" },
|
||||
solr: { deferred: "core settings are not database properties" },
|
||||
hbase: { deferred: "namespace and table properties need a dedicated HBase workflow" },
|
||||
qdrant: { deferred: "collection settings are not database properties" },
|
||||
milvus: { deferred: "collection/database settings need a dedicated vector workflow" },
|
||||
|
||||
@@ -110,6 +110,7 @@ const SQL_EXECUTION_TARGET_REGISTRY: Partial<Record<DatabaseType, SqlExecutionTa
|
||||
elasticsearch: { scope: "connection" },
|
||||
easysearch: { scope: "connection" },
|
||||
meilisearch: { scope: "connection" },
|
||||
solr: { scope: "connection" },
|
||||
qdrant: { scope: "connection" },
|
||||
milvus: { scope: "connection" },
|
||||
weaviate: { scope: "connection" },
|
||||
|
||||
@@ -70,7 +70,7 @@ function decorationsForHints(hints: readonly InsertValueHint[]): DecorationSet {
|
||||
* keep this list in sync instead of re-inlining it at call sites.
|
||||
*/
|
||||
export function supportsInsertValueHints(databaseType: string | undefined | null): boolean {
|
||||
return databaseType !== "redis" && databaseType !== "mongodb" && databaseType !== "elasticsearch" && databaseType !== "easysearch" && databaseType !== "meilisearch" && databaseType !== "victoriametrics";
|
||||
return databaseType !== "redis" && databaseType !== "mongodb" && databaseType !== "elasticsearch" && databaseType !== "easysearch" && databaseType !== "meilisearch" && databaseType !== "solr" && databaseType !== "victoriametrics";
|
||||
}
|
||||
|
||||
export function buildInsertValueHintDecorations(hints: readonly InsertValueHint[]): DecorationSet {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { isElasticsearchCompatibleDatabaseType, type DatabaseType, type QueryResult } from "@/types/database";
|
||||
import { isElasticsearchCompatibleDatabaseType, isSolrDatabaseType, type DatabaseType, type QueryResult } from "@/types/database";
|
||||
import { stripLeadingElasticsearchComments } from "@/lib/sql/sqlStatementRanges";
|
||||
|
||||
export interface ElasticsearchJsonResponse {
|
||||
@@ -9,11 +9,12 @@ export interface ElasticsearchJsonResponse {
|
||||
const ELASTICSEARCH_REST_STATEMENT = /^(?:GET|POST|PUT|DELETE|HEAD)\s+\S+/i;
|
||||
|
||||
/**
|
||||
* Detect the raw HTTP result emitted for an Elasticsearch REST request.
|
||||
* DBX asks unformatted CAT requests for JSON so they use this response panel.
|
||||
* Detect the raw HTTP result emitted for a search-engine REST request.
|
||||
* Elasticsearch asks unformatted CAT requests for JSON; Solr REST passthrough
|
||||
* emits the same `status`/`response` shape, so both share this response panel.
|
||||
*/
|
||||
export function elasticsearchJsonResponseForResult(databaseType: DatabaseType | undefined, sourceStatement: string | undefined, result: QueryResult | undefined): ElasticsearchJsonResponse | undefined {
|
||||
if (!isElasticsearchCompatibleDatabaseType(databaseType) || !result || typeof sourceStatement !== "string") return undefined;
|
||||
if ((!isElasticsearchCompatibleDatabaseType(databaseType) && !isSolrDatabaseType(databaseType)) || !result || typeof sourceStatement !== "string") return undefined;
|
||||
if (!ELASTICSEARCH_REST_STATEMENT.test(stripLeadingElasticsearchComments(sourceStatement))) return undefined;
|
||||
if (result.columns.length !== 2 || result.columns[0] !== "status" || result.columns[1] !== "response" || result.rows.length !== 1) return undefined;
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@ function orderSidebarTreeChildrenForParent(parent: Pick<TreeNode, "type">, child
|
||||
const regularChildren = normalized.filter((child) => child.type !== "user-admin" && child.type !== "dameng-users" && child.type !== "dameng-roles" && child.type !== "saved-sql-root");
|
||||
const withConnectionUtilityOrder = (children: TreeNode[]) => [...savedSqlNodes, ...children, ...userAdminNodes];
|
||||
|
||||
if (databaseType === "mongodb" || databaseType === "elasticsearch" || databaseType === "easysearch" || databaseType === "meilisearch" || databaseType === "qdrant" || databaseType === "milvus" || databaseType === "weaviate" || databaseType === "chromadb") {
|
||||
if (databaseType === "mongodb" || databaseType === "elasticsearch" || databaseType === "easysearch" || databaseType === "meilisearch" || databaseType === "solr" || databaseType === "qdrant" || databaseType === "milvus" || databaseType === "weaviate" || databaseType === "chromadb") {
|
||||
const meilisearchSystem = regularChildren.filter((child) => child.type === "meilisearch-system");
|
||||
const databaseObjects = regularChildren.filter((child) => child.type !== "meilisearch-system");
|
||||
return withConnectionUtilityOrder([...sortByLabel(databaseObjects), ...meilisearchSystem]);
|
||||
|
||||
@@ -0,0 +1,572 @@
|
||||
import { executeQuery } from "@/lib/backend/api";
|
||||
import { parseJsonPreservingLargeNumbers } from "@/lib/common/safeJsonFormat";
|
||||
|
||||
/**
|
||||
* Solr Admin 面板的数据层:所有请求复用 REST 控制台链路
|
||||
* (execute_query → solr_driver::execute_rest_query),后端零新增命令。
|
||||
* 非表格化 JSON 响应由驱动包装为 [status, response] 单行返回;
|
||||
* /response/docs 形态走 elasticsearch_raw_body 透出原文。
|
||||
*/
|
||||
|
||||
export interface SolrAdminResponse {
|
||||
status: number;
|
||||
/** 解析后的 JSON;非 JSON 时为原始文本;空响应为 null。 */
|
||||
body: unknown;
|
||||
rawBody: string;
|
||||
elapsedMs: number;
|
||||
}
|
||||
|
||||
export type SolrAdminMethod = "GET" | "POST" | "PUT" | "DELETE" | "HEAD";
|
||||
|
||||
export async function solrAdminRequest(connectionId: string, method: SolrAdminMethod, path: string, body?: string): Promise<SolrAdminResponse> {
|
||||
const sql = body ? `${method} ${path}\n${body}` : `${method} ${path}`;
|
||||
const result = await executeQuery(connectionId, "", sql);
|
||||
const status = typeof result.rows?.[0]?.[0] === "number" ? result.rows[0][0] : 200;
|
||||
const rawBody = result.elasticsearch_raw_body ?? (typeof result.rows?.[0]?.[1] === "string" ? result.rows[0][1] : "");
|
||||
let parsed: unknown = null;
|
||||
if (rawBody) {
|
||||
try {
|
||||
parsed = parseJsonPreservingLargeNumbers(rawBody);
|
||||
} catch {
|
||||
parsed = rawBody;
|
||||
}
|
||||
}
|
||||
return { status, body: parsed, rawBody, elapsedMs: result.execution_time_ms };
|
||||
}
|
||||
|
||||
export function solrAdminGet(connectionId: string, path: string): Promise<SolrAdminResponse> {
|
||||
return solrAdminRequest(connectionId, "GET", path);
|
||||
}
|
||||
|
||||
/** Solr 错误体 {"error":{"msg","code"}} → 可读消息;用于写操作的状态判定。 */
|
||||
export function solrAdminErrorMessage(res: SolrAdminResponse): string | null {
|
||||
if (res.status < 400) return null;
|
||||
const body = res.body as Record<string, unknown> | null;
|
||||
const msg = body?.error && typeof body.error === "object" ? (body.error as Record<string, unknown>).msg : undefined;
|
||||
return `Solr error (${res.status}): ${typeof msg === "string" ? msg : res.rawBody.slice(0, 500)}`;
|
||||
}
|
||||
|
||||
/** 服务端点(server scope)与 core 端点(core scope,{core} 占位)。 */
|
||||
export interface SolrAdminViewDef {
|
||||
id: SolrAdminViewId;
|
||||
scope: "server" | "core";
|
||||
/** REST 路径模板;core scope 中 {core} 会被替换。 */
|
||||
path: string;
|
||||
/** 需要专用渲染而非通用 JSON 树。 */
|
||||
view: "generic" | "dashboard" | "properties" | "logging" | "coreAdmin" | "overview" | "ping" | "schema" | "analysis" | "segments" | "queryForm" | "replication" | "action";
|
||||
}
|
||||
|
||||
export type SolrAdminViewId = "dashboard" | "logging" | "javaProps" | "threads" | "coreAdmin" | "metrics" | "overview" | "analysis" | "documents" | "paramsets" | "files" | "ping" | "plugins" | "query" | "replication" | "schema" | "segments";
|
||||
|
||||
export const SOLR_ADMIN_SERVER_VIEWS: readonly SolrAdminViewDef[] = [
|
||||
{ id: "dashboard", scope: "server", path: "/admin/info/system", view: "dashboard" },
|
||||
{ id: "logging", scope: "server", path: "/admin/info/logging", view: "logging" },
|
||||
{ id: "javaProps", scope: "server", path: "/admin/info/properties", view: "properties" },
|
||||
{ id: "threads", scope: "server", path: "/admin/info/threads", view: "generic" },
|
||||
{ id: "coreAdmin", scope: "server", path: "/admin/cores?action=STATUS", view: "coreAdmin" },
|
||||
{ id: "metrics", scope: "server", path: "/admin/metrics", view: "generic" },
|
||||
];
|
||||
|
||||
export const SOLR_ADMIN_CORE_VIEWS: readonly SolrAdminViewDef[] = [
|
||||
{ id: "overview", scope: "core", path: "/{core}/admin/luke?show=index", view: "overview" },
|
||||
{ id: "analysis", scope: "core", path: "/{core}/analysis/field", view: "analysis" },
|
||||
{ id: "documents", scope: "core", path: "", view: "action" },
|
||||
{ id: "paramsets", scope: "core", path: "/{core}/config/params", view: "generic" },
|
||||
{ id: "files", scope: "core", path: "/{core}/admin/file", view: "generic" },
|
||||
{ id: "ping", scope: "core", path: "/{core}/admin/ping", view: "ping" },
|
||||
{ id: "plugins", scope: "core", path: "/{core}/admin/mbeans?stats=true", view: "generic" },
|
||||
{ id: "query", scope: "core", path: "", view: "queryForm" },
|
||||
{ id: "replication", scope: "core", path: "/{core}/replication?command=details", view: "replication" },
|
||||
{ id: "schema", scope: "core", path: "/{core}/schema", view: "schema" },
|
||||
{ id: "segments", scope: "core", path: "/{core}/admin/segments", view: "segments" },
|
||||
];
|
||||
|
||||
export function solrAdminViewById(id: SolrAdminViewId): SolrAdminViewDef | undefined {
|
||||
return [...SOLR_ADMIN_SERVER_VIEWS, ...SOLR_ADMIN_CORE_VIEWS].find((view) => view.id === id);
|
||||
}
|
||||
|
||||
export function solrAdminPath(view: SolrAdminViewDef, core: string): string {
|
||||
return view.path.replace("{core}", encodeURIComponent(core));
|
||||
}
|
||||
|
||||
/* ---------- Core Admin ---------- */
|
||||
|
||||
export interface SolrCoreStatus {
|
||||
name: string;
|
||||
instanceDir?: string;
|
||||
dataDir?: string;
|
||||
config?: string;
|
||||
schema?: string;
|
||||
startTime?: string;
|
||||
uptime?: number;
|
||||
numDocs?: number;
|
||||
maxDoc?: number;
|
||||
deletedDocs?: number;
|
||||
sizeInBytes?: number;
|
||||
version?: number;
|
||||
segmentCount?: number;
|
||||
}
|
||||
|
||||
export function parseCoreStatus(body: unknown): SolrCoreStatus[] {
|
||||
const status = (body as Record<string, unknown> | null)?.status;
|
||||
if (!status || typeof status !== "object" || Array.isArray(status)) return [];
|
||||
return Object.entries(status as Record<string, Record<string, unknown>>).map(([name, info]) => {
|
||||
const index = (info?.index ?? {}) as Record<string, unknown>;
|
||||
return {
|
||||
name: typeof info?.name === "string" ? info.name : name,
|
||||
instanceDir: info?.instanceDir as string | undefined,
|
||||
dataDir: info?.dataDir as string | undefined,
|
||||
config: info?.config as string | undefined,
|
||||
schema: info?.schema as string | undefined,
|
||||
startTime: info?.startTime as string | undefined,
|
||||
uptime: typeof info?.uptime === "number" ? info.uptime : undefined,
|
||||
numDocs: typeof index.numDocs === "number" ? index.numDocs : undefined,
|
||||
maxDoc: typeof index.maxDoc === "number" ? index.maxDoc : undefined,
|
||||
deletedDocs: typeof index.deletedDocs === "number" ? index.deletedDocs : undefined,
|
||||
sizeInBytes: typeof index.sizeInBytes === "number" ? index.sizeInBytes : typeof index.size === "number" ? index.size : undefined,
|
||||
version: typeof index.version === "number" ? index.version : undefined,
|
||||
segmentCount: typeof index.segmentCount === "number" ? index.segmentCount : undefined,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export type SolrCoreAction = "CREATE" | "RELOAD" | "UNLOAD" | "RENAME" | "SWAP";
|
||||
|
||||
/** CoreAdmin 写操作统一走 GET /admin/cores(Solr 官方形态),参数全部由 UI 收集。 */
|
||||
export function solrCoreAdminRequest(connectionId: string, params: Record<string, string>): Promise<SolrAdminResponse> {
|
||||
const query = new URLSearchParams(params).toString();
|
||||
return solrAdminRequest(connectionId, "GET", `/admin/cores?${query}`);
|
||||
}
|
||||
|
||||
/* ---------- Schema ---------- */
|
||||
|
||||
export interface SolrSchemaField {
|
||||
name: string;
|
||||
type?: string;
|
||||
stored?: boolean;
|
||||
indexed?: boolean;
|
||||
required?: boolean;
|
||||
multiValued?: boolean;
|
||||
docValues?: boolean;
|
||||
uninvertible?: boolean;
|
||||
}
|
||||
|
||||
export interface SolrSchemaInfo {
|
||||
name?: string;
|
||||
version?: string;
|
||||
uniqueKey?: string;
|
||||
fields: SolrSchemaField[];
|
||||
dynamicFields: SolrSchemaField[];
|
||||
copyFields: Array<{ source: string; dest: string; maxChars?: number }>;
|
||||
fieldTypeCount: number;
|
||||
}
|
||||
|
||||
export function parseSolrSchema(body: unknown): SolrSchemaInfo {
|
||||
const root = (body as Record<string, unknown> | null)?.schema ?? body;
|
||||
const schema = (root ?? {}) as Record<string, unknown>;
|
||||
const fields = Array.isArray(schema.fields) ? (schema.fields as SolrSchemaField[]) : [];
|
||||
const dynamicFields = Array.isArray(schema.dynamicFields) ? (schema.dynamicFields as SolrSchemaField[]) : [];
|
||||
const copyFields = Array.isArray(schema.copyFields) ? (schema.copyFields as Array<{ source: string; dest: string; maxChars?: number }>) : [];
|
||||
const fieldTypes = Array.isArray(schema.fieldTypes) ? schema.fieldTypes : [];
|
||||
return {
|
||||
name: schema.name as string | undefined,
|
||||
version: schema.version != null ? String(schema.version) : undefined,
|
||||
uniqueKey: schema.uniqueKey as string | undefined,
|
||||
fields,
|
||||
dynamicFields,
|
||||
copyFields,
|
||||
fieldTypeCount: fieldTypes.length,
|
||||
};
|
||||
}
|
||||
|
||||
/* ---------- Analysis ---------- */
|
||||
|
||||
export interface SolrAnalysisStage {
|
||||
/** 分析器类名,如 org.apache.lucene.analysis.standard.StandardTokenizer */
|
||||
name: string;
|
||||
tokens: string[];
|
||||
}
|
||||
|
||||
export interface SolrAnalysisResult {
|
||||
/** index 侧阶段链 */
|
||||
index: SolrAnalysisStage[];
|
||||
/** query 侧阶段链 */
|
||||
query: SolrAnalysisStage[];
|
||||
}
|
||||
|
||||
/**
|
||||
* /analysis/field 响应:analysis.field_names.{f}.index 是
|
||||
* [ [stageName, [tokenObj...]], ... ] 交替数组;query 侧结构相同。
|
||||
*/
|
||||
export function parseAnalysisResponse(body: unknown, fieldName: string): SolrAnalysisResult {
|
||||
const analysis = (body as Record<string, unknown> | null)?.analysis as Record<string, unknown> | undefined;
|
||||
const fieldNames = analysis?.field_names as Record<string, unknown> | undefined;
|
||||
const entry = (fieldName ? fieldNames?.[fieldName] : undefined) ?? (fieldNames ? Object.values(fieldNames)[0] : undefined);
|
||||
const stages = (entry ?? {}) as Record<string, unknown>;
|
||||
const parseStages = (raw: unknown): SolrAnalysisStage[] => {
|
||||
if (!Array.isArray(raw)) return [];
|
||||
const stages: SolrAnalysisStage[] = [];
|
||||
for (const item of raw) {
|
||||
if (!Array.isArray(item) || item.length < 2) continue;
|
||||
const name = typeof item[0] === "string" ? item[0] : String(item[0]);
|
||||
const tokenObjs = Array.isArray(item[1]) ? item[1] : [];
|
||||
const tokens = tokenObjs.map((token) => {
|
||||
if (token && typeof token === "object" && !Array.isArray(token)) {
|
||||
const text = (token as Record<string, unknown>).text;
|
||||
return text != null ? String(text) : JSON.stringify(token);
|
||||
}
|
||||
return String(token);
|
||||
});
|
||||
stages.push({ name, tokens });
|
||||
}
|
||||
return stages;
|
||||
};
|
||||
return { index: parseStages(stages.index), query: parseStages(stages.query) };
|
||||
}
|
||||
|
||||
export function solrAnalysisPath(core: string, fieldName: string, fieldValue: string, queryValue?: string): string {
|
||||
const params = new URLSearchParams();
|
||||
params.set("analysis.fieldname", fieldName);
|
||||
params.set("analysis.fieldvalue", fieldValue);
|
||||
if (queryValue) params.set("analysis.query", queryValue);
|
||||
return `/${encodeURIComponent(core)}/analysis/field?${params.toString()}`;
|
||||
}
|
||||
|
||||
/* ---------- Query Builder(对标官方 Admin UI Query 表单) ---------- */
|
||||
|
||||
export interface SolrQueryParam {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface SolrQueryFormState {
|
||||
/** Request-Handler,如 /select、/query */
|
||||
handler: string;
|
||||
q: string;
|
||||
qop: "" | "AND" | "OR";
|
||||
fq: string[];
|
||||
sort: string;
|
||||
start: string;
|
||||
rows: string;
|
||||
fl: string;
|
||||
df: string;
|
||||
/** paramset 名(useParams 参数,逗号拼接) */
|
||||
useParams: string[];
|
||||
wt: string;
|
||||
indent: boolean;
|
||||
debugQuery: boolean;
|
||||
defType: string;
|
||||
hl: { enabled: boolean; fl: string; snippets: string; fragsize: string; simplePre: string; simplePost: string; requireFieldMatch: boolean; mergeContiguous: boolean };
|
||||
facet: { enabled: boolean; queries: string[]; fields: string[]; prefix: string; sort: string; limit: string; offset: string; mincount: string; missing: boolean };
|
||||
spatial: { enabled: boolean; pt: string; sfield: string; d: string; geofilt: boolean; bbox: boolean };
|
||||
spellcheck: { enabled: boolean; q: string; build: boolean; collate: boolean; count: string; dictionary: string; onlyMorePopular: boolean };
|
||||
rawParams: SolrQueryParam[];
|
||||
/** 非空时改用 JSON Request DSL:POST {handler} + body,与参数表单互斥 */
|
||||
jsonQuery: string;
|
||||
}
|
||||
|
||||
export function defaultSolrQueryForm(): SolrQueryFormState {
|
||||
return {
|
||||
handler: "/select",
|
||||
q: "*:*",
|
||||
qop: "",
|
||||
fq: [],
|
||||
sort: "",
|
||||
start: "0",
|
||||
rows: "10",
|
||||
fl: "",
|
||||
df: "",
|
||||
useParams: [],
|
||||
wt: "json",
|
||||
indent: true,
|
||||
debugQuery: false,
|
||||
defType: "",
|
||||
hl: { enabled: false, fl: "", snippets: "", fragsize: "", simplePre: "", simplePost: "", requireFieldMatch: false, mergeContiguous: false },
|
||||
facet: { enabled: false, queries: [], fields: [], prefix: "", sort: "", limit: "", offset: "", mincount: "", missing: false },
|
||||
spatial: { enabled: false, pt: "", sfield: "", d: "", geofilt: false, bbox: false },
|
||||
spellcheck: { enabled: false, q: "", build: false, collate: false, count: "", dictionary: "", onlyMorePopular: false },
|
||||
rawParams: [],
|
||||
jsonQuery: "",
|
||||
};
|
||||
}
|
||||
|
||||
export interface SolrBuiltQuery {
|
||||
/** REST 控制台可执行文本:"GET /path?qs" 或 "POST /path\n{json}" */
|
||||
text: string;
|
||||
jsonMode: boolean;
|
||||
}
|
||||
|
||||
const nonEmpty = (v: string) => v.trim() !== "";
|
||||
|
||||
/** 表单 → REST 文本。重复参数(fq/facet.field…)经 URLSearchParams.append 保留多值。 */
|
||||
export function buildSolrQuery(core: string, state: SolrQueryFormState): SolrBuiltQuery {
|
||||
const handler = state.handler.trim() || "/select";
|
||||
const base = `/${encodeURIComponent(core)}${handler.startsWith("/") ? handler : `/${handler}`}`;
|
||||
|
||||
if (nonEmpty(state.jsonQuery)) {
|
||||
return { text: `POST ${base}\n${state.jsonQuery.trim()}`, jsonMode: true };
|
||||
}
|
||||
|
||||
const params = new URLSearchParams();
|
||||
const set = (key: string, value: string | undefined) => {
|
||||
if (value != null && nonEmpty(value)) params.set(key, value.trim());
|
||||
};
|
||||
const appendEach = (key: string, values: string[]) => {
|
||||
for (const v of values) if (nonEmpty(v)) params.append(key, v.trim());
|
||||
};
|
||||
|
||||
set("q", state.q || "*:*");
|
||||
set("q.op", state.qop);
|
||||
appendEach("fq", state.fq);
|
||||
set("sort", state.sort);
|
||||
set("start", state.start);
|
||||
set("rows", state.rows);
|
||||
set("fl", state.fl);
|
||||
set("df", state.df);
|
||||
if (state.useParams.length) params.set("useParams", state.useParams.join(","));
|
||||
set("wt", state.wt);
|
||||
if (state.indent) params.set("indent", "on");
|
||||
if (state.debugQuery) params.set("debugQuery", "true");
|
||||
set("defType", state.defType);
|
||||
|
||||
const hl = state.hl;
|
||||
if (hl.enabled) {
|
||||
params.set("hl", "true");
|
||||
set("hl.fl", hl.fl);
|
||||
set("hl.snippets", hl.snippets);
|
||||
set("hl.fragsize", hl.fragsize);
|
||||
set("hl.simple.pre", hl.simplePre);
|
||||
set("hl.simple.post", hl.simplePost);
|
||||
if (hl.requireFieldMatch) params.set("hl.requireFieldMatch", "true");
|
||||
if (hl.mergeContiguous) params.set("hl.mergeContiguous", "true");
|
||||
}
|
||||
|
||||
const facet = state.facet;
|
||||
if (facet.enabled) {
|
||||
params.set("facet", "true");
|
||||
appendEach("facet.query", facet.queries);
|
||||
appendEach("facet.field", facet.fields);
|
||||
set("facet.prefix", facet.prefix);
|
||||
set("facet.sort", facet.sort);
|
||||
set("facet.limit", facet.limit);
|
||||
set("facet.offset", facet.offset);
|
||||
set("facet.mincount", facet.mincount);
|
||||
if (facet.missing) params.set("facet.missing", "true");
|
||||
}
|
||||
|
||||
const spatial = state.spatial;
|
||||
if (spatial.enabled) {
|
||||
set("pt", spatial.pt);
|
||||
set("sfield", spatial.sfield);
|
||||
set("d", spatial.d);
|
||||
// 官方 UI 的 geofilt/bbox 以 fq 函数查询注入
|
||||
if (spatial.geofilt) params.append("fq", "{!geofilt}");
|
||||
if (spatial.bbox) params.append("fq", "{!bbox}");
|
||||
}
|
||||
|
||||
const sc = state.spellcheck;
|
||||
if (sc.enabled) {
|
||||
params.set("spellcheck", "true");
|
||||
set("spellcheck.q", sc.q);
|
||||
if (sc.build) params.set("spellcheck.build", "true");
|
||||
if (sc.collate) params.set("spellcheck.collate", "true");
|
||||
set("spellcheck.count", sc.count);
|
||||
set("spellcheck.dictionary", sc.dictionary);
|
||||
if (sc.onlyMorePopular) params.set("spellcheck.onlyMorePopular", "true");
|
||||
}
|
||||
|
||||
for (const rp of state.rawParams) {
|
||||
if (nonEmpty(rp.key) && nonEmpty(rp.value)) params.append(rp.key.trim(), rp.value.trim());
|
||||
}
|
||||
|
||||
return { text: `GET ${base}?${params.toString()}`, jsonMode: false };
|
||||
}
|
||||
|
||||
/** /{core}/config/params 响应 → paramset 名列表,供 useParams 选择。 */
|
||||
export function parseParamsetNames(body: unknown): string[] {
|
||||
const params = (body as Record<string, unknown> | null)?.response as Record<string, unknown> | undefined;
|
||||
const named = params?.params;
|
||||
if (!named || typeof named !== "object" || Array.isArray(named)) return [];
|
||||
return Object.keys(named as Record<string, unknown>).sort();
|
||||
}
|
||||
|
||||
/** select 响应体 → numFound;非 select/出错为 null。 */
|
||||
export function parseSolrNumFound(rawBody: string): number | null {
|
||||
try {
|
||||
const body = parseJsonPreservingLargeNumbers(rawBody) as Record<string, unknown> | null;
|
||||
const num = (body?.response as Record<string, unknown> | undefined)?.numFound;
|
||||
return typeof num === "number" ? num : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- Overview(对标官方 UI:Statistics / Instance / Replication / Healthcheck 组合) ---------- */
|
||||
|
||||
/** luke ?show=index 的 index 块 → Statistics 行 */
|
||||
export interface SolrIndexStats {
|
||||
lastModified?: string;
|
||||
numDocs?: number;
|
||||
maxDoc?: number;
|
||||
deletedDocs?: number;
|
||||
version?: number;
|
||||
segmentCount?: number;
|
||||
current?: boolean;
|
||||
/** Lucene Directory 实现类(org.apache.lucene.store.NRTCachingDirectory 等) */
|
||||
directoryImpl?: string;
|
||||
}
|
||||
|
||||
export function parseIndexStats(body: unknown): SolrIndexStats {
|
||||
const index = (body as Record<string, unknown> | null)?.index as Record<string, unknown> | undefined;
|
||||
if (!index) return {};
|
||||
const directory = typeof index.directory === "string" ? index.directory : undefined;
|
||||
return {
|
||||
lastModified: index.lastModified as string | undefined,
|
||||
numDocs: typeof index.numDocs === "number" ? index.numDocs : undefined,
|
||||
maxDoc: typeof index.maxDoc === "number" ? index.maxDoc : undefined,
|
||||
deletedDocs: typeof index.deletedDocs === "number" ? index.deletedDocs : undefined,
|
||||
version: typeof index.version === "number" ? index.version : undefined,
|
||||
segmentCount: typeof index.segmentCount === "number" ? index.segmentCount : undefined,
|
||||
current: typeof index.current === "boolean" ? index.current : undefined,
|
||||
directoryImpl: directory?.split(":")[0],
|
||||
};
|
||||
}
|
||||
|
||||
/** replication?command=details 的 details 块 */
|
||||
export interface SolrReplicationDetails {
|
||||
isLeader: boolean;
|
||||
isFollower: boolean;
|
||||
indexSize?: string;
|
||||
indexPath?: string;
|
||||
indexVersion?: string;
|
||||
generation?: number;
|
||||
replicableVersion?: number;
|
||||
replicableGeneration?: number;
|
||||
replicationEnabled?: boolean;
|
||||
replicateAfter: string[];
|
||||
/** follower 侧信息(isLeader=false 时展示) */
|
||||
follower?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export function parseReplicationDetails(body: unknown): SolrReplicationDetails | null {
|
||||
const details = (body as Record<string, unknown> | null)?.details as Record<string, unknown> | undefined;
|
||||
if (!details) return null;
|
||||
const leader = (details.leader ?? {}) as Record<string, unknown>;
|
||||
const follower = details.follower as Record<string, unknown> | undefined;
|
||||
const num = (v: unknown) => (typeof v === "number" ? v : v != null ? Number(v) : undefined);
|
||||
return {
|
||||
isLeader: details.isLeader === true || details.isLeader === "true",
|
||||
isFollower: details.isFollower === true || details.isFollower === "true",
|
||||
indexSize: details.indexSize as string | undefined,
|
||||
indexPath: details.indexPath as string | undefined,
|
||||
indexVersion: details.indexVersion != null ? String(details.indexVersion) : undefined,
|
||||
generation: num(details.generation),
|
||||
replicableVersion: num(leader.replicableVersion),
|
||||
replicableGeneration: num(leader.replicableGeneration),
|
||||
replicationEnabled: leader.replicationEnabled === true || leader.replicationEnabled === "true",
|
||||
replicateAfter: Array.isArray(leader.replicateAfter) ? leader.replicateAfter.map(String) : [],
|
||||
follower,
|
||||
};
|
||||
}
|
||||
|
||||
/** leader 侧启停复制:/{core}/replication?command=enablereplication|disablereplication */
|
||||
export function solrReplicationCommand(connectionId: string, core: string, command: "enablereplication" | "disablereplication"): Promise<SolrAdminResponse> {
|
||||
return solrAdminRequest(connectionId, "GET", `/${encodeURIComponent(core)}/replication?command=${command}`);
|
||||
}
|
||||
|
||||
/** /admin/info/properties 响应 → JVM 工作目录(官方 Instance 框的 CWD)。 */
|
||||
export function parseServerCwd(body: unknown): string | undefined {
|
||||
const props = (body as Record<string, unknown> | null)?.["system.properties"] as Record<string, unknown> | undefined;
|
||||
return typeof props?.["user.dir"] === "string" ? props["user.dir"] : undefined;
|
||||
}
|
||||
|
||||
/* ---------- Dashboard(官方布局:Instance / Versions / JVM + System 进度条) ---------- */
|
||||
|
||||
export interface SolrSystemInfo {
|
||||
mode?: string;
|
||||
solrHome?: string;
|
||||
coreRoot?: string;
|
||||
/** jvm.jmx.startTime(ISO) */
|
||||
startTime?: string;
|
||||
uptimeMs?: number;
|
||||
solrSpec?: string;
|
||||
solrImpl?: string;
|
||||
luceneSpec?: string;
|
||||
luceneImpl?: string;
|
||||
/** name + version 拼接,如 "Eclipse Adoptium OpenJDK 64-Bit Server VM 17.0.15 17.0.15+6" */
|
||||
jvmRuntime?: string;
|
||||
processors?: number;
|
||||
args: string[];
|
||||
jvmMemUsedPct?: number;
|
||||
jvmMemUsed?: string;
|
||||
jvmMemTotal?: string;
|
||||
/** 物理内存:bytes */
|
||||
physMemUsedPct?: number;
|
||||
physMemUsed?: number;
|
||||
physMemTotal?: number;
|
||||
swapUsedPct?: number;
|
||||
swapUsed?: number;
|
||||
swapTotal?: number;
|
||||
fdUsed?: number;
|
||||
fdMax?: number;
|
||||
}
|
||||
|
||||
export function parseSolrSystemInfo(body: unknown): SolrSystemInfo {
|
||||
const root = (body ?? {}) as Record<string, unknown>;
|
||||
const jvm = (root.jvm ?? {}) as Record<string, unknown>;
|
||||
const jmx = (jvm.jmx ?? {}) as Record<string, unknown>;
|
||||
const lucene = (root.lucene ?? {}) as Record<string, unknown>;
|
||||
const system = (root.system ?? {}) as Record<string, unknown>;
|
||||
const memory = (jvm.memory ?? {}) as Record<string, unknown>;
|
||||
const raw = (memory.raw ?? {}) as Record<string, unknown>;
|
||||
|
||||
const num = (v: unknown): number | undefined => (typeof v === "number" && Number.isFinite(v) ? v : undefined);
|
||||
const str = (v: unknown): string | undefined => (typeof v === "string" && v !== "" ? v : undefined);
|
||||
const pct = (used: number | undefined, total: number | undefined) => (used != null && total != null && total > 0 ? (used / total) * 100 : undefined);
|
||||
|
||||
const physTotal = num(system.totalPhysicalMemorySize);
|
||||
const physFree = num(system.freePhysicalMemorySize);
|
||||
const physUsed = physTotal != null && physFree != null ? physTotal - physFree : undefined;
|
||||
const swapTotal = num(system.totalSwapSpaceSize);
|
||||
const swapFree = num(system.freeSwapSpaceSize);
|
||||
const swapUsed = swapTotal != null && swapFree != null ? swapTotal - swapFree : undefined;
|
||||
|
||||
const jvmName = str(jvm.name);
|
||||
const jvmVersion = str(jvm.version);
|
||||
|
||||
return {
|
||||
mode: str(root.mode),
|
||||
solrHome: str(root.solr_home),
|
||||
coreRoot: str(root.core_root),
|
||||
startTime: str(jmx.startTime),
|
||||
uptimeMs: num(jmx.upTimeMS),
|
||||
solrSpec: str(lucene["solr-spec-version"]),
|
||||
solrImpl: str(lucene["solr-impl-version"]),
|
||||
luceneSpec: str(lucene["lucene-spec-version"]),
|
||||
luceneImpl: str(lucene["lucene-impl-version"]),
|
||||
jvmRuntime: [jvmName, jvmVersion].filter(Boolean).join(" ") || undefined,
|
||||
processors: num(jvm.processors),
|
||||
args: Array.isArray(jmx.commandLineArgs) ? jmx.commandLineArgs.map(String) : [],
|
||||
jvmMemUsedPct: num(raw["used%"]),
|
||||
jvmMemUsed: str(memory.used),
|
||||
jvmMemTotal: str(memory.total),
|
||||
physMemUsedPct: pct(physUsed, physTotal),
|
||||
physMemUsed: physUsed,
|
||||
physMemTotal: physTotal,
|
||||
swapUsedPct: pct(swapUsed, swapTotal),
|
||||
swapUsed,
|
||||
swapTotal,
|
||||
fdUsed: num(system.openFileDescriptorCount),
|
||||
fdMax: num(system.maxFileDescriptorCount),
|
||||
};
|
||||
}
|
||||
|
||||
/** solr-impl-version 里带构建日期("9.8.1 dab835... - houston - 2025-03-06 13:59:17"),>1 年提示升级。 */
|
||||
export function solrReleaseAgeDays(implVersion?: string): number | null {
|
||||
if (!implVersion) return null;
|
||||
const match = implVersion.match(/(\d{4})-(\d{2})-(\d{2})/);
|
||||
if (!match) return null;
|
||||
const ts = Date.parse(`${match[1]}-${match[2]}-${match[3]}T00:00:00Z`);
|
||||
if (!Number.isFinite(ts)) return null;
|
||||
return Math.floor((Date.now() - ts) / 86400000);
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
/**
|
||||
* Read/write classification for Apache Solr REST requests.
|
||||
*
|
||||
* Mirrors `classify_solr_query_risk` in `crates/dbx-sql/src/query_execution_sql.rs`
|
||||
* so the desktop guards (read-only unlock, production safety) agree with the
|
||||
* backend read-only gate instead of treating every request as unrecognized.
|
||||
*
|
||||
* Solr write traffic funnels through `/{core}/update*` (plus `commit`) — for
|
||||
* every HTTP method, since Solr update handlers also answer GET commits.
|
||||
* CoreAdmin/Collections actions and ReplicationHandler commands ride on GET
|
||||
* too, so `action=`/`command=` values outside the read whitelists are
|
||||
* dangerous. Read-only POST handlers are the query
|
||||
* endpoints (select/query/get/export/...). Any other mutating method/path —
|
||||
* `/admin/*`, `/schema`, `/config`, core management — is classified dangerous.
|
||||
*/
|
||||
|
||||
export type SolrRequestRisk = "read" | "write" | "dangerous";
|
||||
|
||||
const REQUEST_LINE = /^(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\s+(\S+)/i;
|
||||
const READ_ONLY_POST_ENDPOINTS = new Set(["select", "query", "get", "export", "terms", "suggest", "spell", "mlt", "sql", "graph", "clustering", "tvrh", "luke", "elevate", "browse", "debug"]);
|
||||
const READ_ONLY_REPLICATION_COMMANDS = new Set(["details", "restorestatus", "filelist", "filecontent", "filedownload", "filemtime", "indexversion", "showversion"]);
|
||||
const RISK_ORDER: Record<SolrRequestRisk, number> = { read: 0, write: 1, dangerous: 2 };
|
||||
|
||||
function parseRequestLine(line: string): { method: string; rawPath: string; endpoint: string; isUpdatePath: boolean; segments: string[] } | null {
|
||||
const match = line.trim().match(REQUEST_LINE);
|
||||
if (!match) return null;
|
||||
const rawPath = match[2];
|
||||
const path = (rawPath.split("?", 1)[0] ?? "").replace(/\/+$/, "");
|
||||
const segments = path.split("/").filter(Boolean);
|
||||
// Users may paste absolute Solr paths ("/solr/{core}/select") or relative
|
||||
// ones ("{core}/select"); drop the fixed /solr context segment if present.
|
||||
if (segments[0]?.toLowerCase() === "solr") segments.shift();
|
||||
// Update handler variants (/update/json, /update/json/docs, /update/csv)
|
||||
// nest below the update segment, so the last segment alone cannot spot them.
|
||||
const isUpdatePath = segments.some((segment) => segment.startsWith("update") || segment === "commit");
|
||||
return { method: match[1].toUpperCase(), rawPath, endpoint: segments[segments.length - 1]?.toLowerCase() ?? "", isUpdatePath, segments };
|
||||
}
|
||||
|
||||
/**
|
||||
* CoreAdmin/Collections endpoints accept GET-triggered mutations
|
||||
* (`GET /admin/cores?action=CREATE&name=x` really creates a core). Only the
|
||||
* explicitly read-only actions stay "read"; any other action is dangerous.
|
||||
*/
|
||||
function solrAdminRequestIsMutation(rawPath: string, segments: string[]): boolean {
|
||||
const isAdminTarget = segments.some((segment, index) => segment.toLowerCase() === "admin" && ["cores", "collections"].includes(segments[index + 1]?.toLowerCase() ?? ""));
|
||||
if (!isAdminTarget) return false;
|
||||
const query = rawPath.split("?")[1];
|
||||
if (!query) return false;
|
||||
const action = query
|
||||
.split("&")
|
||||
.map((param) => param.split("=", 2))
|
||||
.find(([key]) => key.toLowerCase() === "action")?.[1];
|
||||
if (action === undefined) return false;
|
||||
return !["STATUS", "REQUESTSTATUS", "LIST"].includes(action.toUpperCase());
|
||||
}
|
||||
|
||||
/**
|
||||
* The replication handler also takes GET-triggered mutations
|
||||
* (`GET /{core}/replication?command=disablereplication` really stops
|
||||
* replication). Only the explicitly read-only commands stay "read"; a missing
|
||||
* command falls back to the handler's default details response.
|
||||
*/
|
||||
function solrReplicationRequestIsMutation(rawPath: string, segments: string[]): boolean {
|
||||
if (segments[segments.length - 1]?.toLowerCase() !== "replication") return false;
|
||||
const query = rawPath.split("?")[1];
|
||||
if (!query) return false;
|
||||
const command = query
|
||||
.split("&")
|
||||
.map((param) => param.split("=", 2))
|
||||
.find(([key]) => key.toLowerCase() === "command")?.[1];
|
||||
if (command === undefined) return false;
|
||||
return !READ_ONLY_REPLICATION_COMMANDS.has(command.toLowerCase());
|
||||
}
|
||||
|
||||
function classifyParsedRequest({ method, rawPath, endpoint, isUpdatePath, segments }: { method: string; rawPath: string; endpoint: string; isUpdatePath: boolean; segments: string[] }): SolrRequestRisk {
|
||||
// Solr update handlers also answer GET (commit/optimize/stream.body all
|
||||
// mutate), so update paths count as writes for every method, not just POST.
|
||||
if (isUpdatePath) return "write";
|
||||
if (method === "GET" || method === "HEAD" || method === "OPTIONS") {
|
||||
return solrAdminRequestIsMutation(rawPath, segments) || solrReplicationRequestIsMutation(rawPath, segments) ? "dangerous" : "read";
|
||||
}
|
||||
if (method === "POST" || method === "PUT" || method === "PATCH" || method === "DELETE") {
|
||||
return method === "POST" && READ_ONLY_POST_ENDPOINTS.has(endpoint) ? "read" : "dangerous";
|
||||
}
|
||||
return "dangerous";
|
||||
}
|
||||
|
||||
/**
|
||||
* Classifies a single request: the request line is the first non-comment line,
|
||||
* everything after it is the JSON body. Returns null when the text is not a
|
||||
* REST request.
|
||||
*/
|
||||
export function classifySolrRequestRisk(request: string): SolrRequestRisk | null {
|
||||
const parsed = parseRequestLine(uncommentedLines(request)[0] ?? "");
|
||||
return parsed ? classifyParsedRequest(parsed) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Classifies every request in the editor text and returns the highest risk, so
|
||||
* a read request followed by a write is still guarded. Like the backend, the
|
||||
* text only counts as REST when its first non-comment line is a request line.
|
||||
*/
|
||||
export function classifySolrSourceRisk(source: string): SolrRequestRisk | null {
|
||||
const lines = uncommentedLines(source);
|
||||
if (!parseRequestLine(lines[0] ?? "")) return null;
|
||||
|
||||
let highest: SolrRequestRisk = "read";
|
||||
for (const line of lines) {
|
||||
const parsed = parseRequestLine(line);
|
||||
if (!parsed) continue;
|
||||
const risk = classifyParsedRequest(parsed);
|
||||
if (RISK_ORDER[risk] > RISK_ORDER[highest]) highest = risk;
|
||||
}
|
||||
return highest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a parsed request line targets a mutating path that is not a document
|
||||
* update — used by the dangerous-execution confirmation gate.
|
||||
*/
|
||||
export function isDangerousSolrRequest(method: string, path: string): boolean {
|
||||
const upperMethod = method.toUpperCase();
|
||||
const pathname = path.split("?", 1)[0].replace(/\/+$/, "");
|
||||
const segments = pathname.split("/").filter(Boolean);
|
||||
if (segments[0]?.toLowerCase() === "solr") segments.shift();
|
||||
const endpoint = segments[segments.length - 1]?.toLowerCase() ?? "";
|
||||
const isUpdatePath = segments.some((segment) => segment.startsWith("update") || segment === "commit");
|
||||
// Document writes are guarded by the write confirmation, not the dangerous
|
||||
// gate — for every method, including GET-driven commits.
|
||||
if (isUpdatePath) return false;
|
||||
if (upperMethod === "GET" || upperMethod === "HEAD" || upperMethod === "OPTIONS") {
|
||||
return solrAdminRequestIsMutation(path, segments) || solrReplicationRequestIsMutation(path, segments);
|
||||
}
|
||||
if (upperMethod === "POST" || upperMethod === "PUT" || upperMethod === "PATCH" || upperMethod === "DELETE") {
|
||||
return !(upperMethod === "POST" && READ_ONLY_POST_ENDPOINTS.has(endpoint));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns each line with its leading comments removed, dropping lines that hold
|
||||
* nothing else. Block comments spanning several lines are skipped as a whole so
|
||||
* a commented-out request line is never classified.
|
||||
*/
|
||||
function uncommentedLines(source: string): string[] {
|
||||
const lines: string[] = [];
|
||||
let inBlockComment = false;
|
||||
|
||||
for (const rawLine of source.split("\n")) {
|
||||
let offset = 0;
|
||||
while (offset < rawLine.length) {
|
||||
if (inBlockComment) {
|
||||
const close = rawLine.indexOf("*/", offset);
|
||||
if (close < 0) break;
|
||||
inBlockComment = false;
|
||||
offset = close + 2;
|
||||
continue;
|
||||
}
|
||||
while (offset < rawLine.length && /\s/.test(rawLine[offset] ?? "")) offset += 1;
|
||||
if (offset >= rawLine.length) break;
|
||||
if (rawLine.startsWith("/*", offset)) {
|
||||
inBlockComment = true;
|
||||
offset += 2;
|
||||
continue;
|
||||
}
|
||||
if (rawLine[offset] === "#" || rawLine.startsWith("//", offset)) break;
|
||||
lines.push(rawLine.slice(offset));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return lines;
|
||||
}
|
||||
@@ -461,6 +461,7 @@ export function shouldRunSqlSemanticDiagnostics(sql: string, cursor: number, opt
|
||||
options.databaseType === "elasticsearch" ||
|
||||
options.databaseType === "easysearch" ||
|
||||
options.databaseType === "meilisearch" ||
|
||||
options.databaseType === "solr" ||
|
||||
options.databaseType === "qdrant" ||
|
||||
options.databaseType === "milvus" ||
|
||||
options.databaseType === "weaviate" ||
|
||||
@@ -481,6 +482,7 @@ export function isSqlSemanticDiagnosticInputContext(sql: string, cursor: number,
|
||||
options.databaseType === "elasticsearch" ||
|
||||
options.databaseType === "easysearch" ||
|
||||
options.databaseType === "meilisearch" ||
|
||||
options.databaseType === "solr" ||
|
||||
options.databaseType === "qdrant" ||
|
||||
options.databaseType === "milvus" ||
|
||||
options.databaseType === "weaviate" ||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { classifyElasticsearchRequestRisk, classifyElasticsearchSourceRisk, type ElasticsearchRequestRisk } from "@/lib/elasticsearch/elasticsearchRequestRisk";
|
||||
import { classifySolrRequestRisk, classifySolrSourceRisk } from "@/lib/solr/solrRequestRisk";
|
||||
import { mongoAggregateWriteStage, splitMongoCommandRanges, type MongoCommand } from "@/lib/mongo/mongoShellCommand";
|
||||
import { isElasticsearchCompatibleDatabaseType, type DatabaseType } from "@/types/database";
|
||||
import { isElasticsearchCompatibleDatabaseType, isSolrDatabaseType, type DatabaseType } from "@/types/database";
|
||||
|
||||
export type SqlRiskLevel = "read" | "write" | "ddl" | "transaction" | "unknown";
|
||||
|
||||
@@ -54,7 +55,7 @@ export function classifySqlRisk(sql: string, options: SqlRiskOptions = {}): SqlR
|
||||
|
||||
// REST requests carry a JSON body that must not be split on semicolons, and
|
||||
// every request in the text is classified so the highest risk wins.
|
||||
const searchEngineRisk = searchEngineAssessment(sql, options.dialect, classifyElasticsearchSourceRisk);
|
||||
const searchEngineRisk = searchEngineAssessment(sql, options.dialect, { elasticsearch: classifyElasticsearchSourceRisk, solr: classifySolrSourceRisk });
|
||||
if (searchEngineRisk) return { ...searchEngineRisk, statements: [searchEngineRisk] };
|
||||
|
||||
const statements = splitSqlStatementsForSafety(sql).map((statement) => classifySqlStatementRisk(statement, options));
|
||||
@@ -69,7 +70,7 @@ export function classifySqlStatementRisk(sql: string, options: SqlRiskOptions =
|
||||
|
||||
const dynamodbRisk = classifyDynamoDbStatementRisk(sql, options.dialect);
|
||||
if (dynamodbRisk) return dynamodbRisk;
|
||||
const searchEngineRisk = searchEngineAssessment(sql, options.dialect, classifyElasticsearchRequestRisk);
|
||||
const searchEngineRisk = searchEngineAssessment(sql, options.dialect, { elasticsearch: classifyElasticsearchRequestRisk, solr: classifySolrRequestRisk });
|
||||
if (searchEngineRisk) return searchEngineRisk;
|
||||
return classifyTokens(tokenizeSqlForRisk(sql));
|
||||
}
|
||||
@@ -85,9 +86,11 @@ export function classifySqlStatementRisk(sql: string, options: SqlRiskOptions =
|
||||
* is a schema change, ...), which a request path does not carry. Reporting
|
||||
* `rest` keeps REST mutations as opaque as they were before this branch existed.
|
||||
*/
|
||||
function searchEngineAssessment(sql: string, dialect: DatabaseType | string | undefined, classify: (value: string) => ElasticsearchRequestRisk | null): SqlRiskStatementAssessment | null {
|
||||
if (!isElasticsearchCompatibleDatabaseType(dialect as DatabaseType | undefined)) return null;
|
||||
const risk = classify(sql);
|
||||
function searchEngineAssessment(sql: string, dialect: DatabaseType | string | undefined, classify: { elasticsearch: (value: string) => ElasticsearchRequestRisk | null; solr: (value: string) => ElasticsearchRequestRisk | null }): SqlRiskStatementAssessment | null {
|
||||
const databaseType = dialect as DatabaseType | undefined;
|
||||
const classifier = isSolrDatabaseType(databaseType) ? classify.solr : isElasticsearchCompatibleDatabaseType(databaseType) ? classify.elasticsearch : null;
|
||||
if (!classifier) return null;
|
||||
const risk = classifier(sql);
|
||||
if (!risk) return null;
|
||||
return { risk: risk === "read" ? "read" : risk === "write" ? "write" : "ddl", firstKeyword: "rest" };
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { cursorBelongsToTrailingStatementDelimiter } from "@/lib/sql/statementDe
|
||||
import { splitMongoCommandRanges } from "@/lib/mongo/mongoShellCommand";
|
||||
import { isRedisCommentLine } from "@/lib/redis/redisCommandTokenizer";
|
||||
import { readSqlBracedParameterAt, type SqlParameterOptions } from "@/lib/sql/sqlParameters";
|
||||
import { isElasticsearchCompatibleDatabaseType, isMeilisearchDatabaseType, type DatabaseType } from "@/types/database";
|
||||
import { isElasticsearchCompatibleDatabaseType, isMeilisearchDatabaseType, isSolrDatabaseType, type DatabaseType } from "@/types/database";
|
||||
|
||||
/**
|
||||
* A contiguous range of SQL text expressed as document offsets plus the
|
||||
@@ -18,7 +18,7 @@ export interface SqlTextRange {
|
||||
const ELASTICSEARCH_REST_REQUEST = /^(?:GET|POST|PUT|PATCH|DELETE|HEAD)\s+\S+/i;
|
||||
|
||||
function isHttpJsonRestDatabaseType(databaseType?: DatabaseType): boolean {
|
||||
return isElasticsearchCompatibleDatabaseType(databaseType) || isMeilisearchDatabaseType(databaseType);
|
||||
return isElasticsearchCompatibleDatabaseType(databaseType) || isMeilisearchDatabaseType(databaseType) || isSolrDatabaseType(databaseType);
|
||||
}
|
||||
|
||||
export function elasticsearchRestRequestRanges(sql: string, databaseType?: DatabaseType): SqlTextRange[] {
|
||||
@@ -27,7 +27,7 @@ export function elasticsearchRestRequestRanges(sql: string, databaseType?: Datab
|
||||
return requests.length > 0 && requests.every((request) => ELASTICSEARCH_REST_REQUEST.test(request.sql)) ? requests : [];
|
||||
}
|
||||
|
||||
const NON_SQL_EXECUTION_TARGET_TYPES: ReadonlySet<DatabaseType> = new Set(["mongodb", "elasticsearch", "easysearch", "meilisearch", "qdrant", "milvus", "weaviate", "chromadb", "etcd", "zookeeper", "consul", "mq", "neo4j", "victoriametrics"]);
|
||||
const NON_SQL_EXECUTION_TARGET_TYPES: ReadonlySet<DatabaseType> = new Set(["mongodb", "elasticsearch", "easysearch", "meilisearch", "solr", "qdrant", "milvus", "weaviate", "chromadb", "etcd", "zookeeper", "consul", "mq", "neo4j", "victoriametrics"]);
|
||||
|
||||
export function supportsExecutionTargetPicker(databaseType?: DatabaseType): boolean {
|
||||
return !!databaseType && (databaseType === "redis" || isHttpJsonRestDatabaseType(databaseType) || !NON_SQL_EXECUTION_TARGET_TYPES.has(databaseType));
|
||||
@@ -270,6 +270,7 @@ const DATABASE_SOFT_STATEMENT_KEYWORDS: Partial<Record<DatabaseType, readonly st
|
||||
mongodb: [],
|
||||
elasticsearch: [],
|
||||
easysearch: [],
|
||||
solr: [],
|
||||
qdrant: [],
|
||||
milvus: [],
|
||||
weaviate: [],
|
||||
|
||||
@@ -84,6 +84,12 @@ const capabilityByType: Partial<Record<DatabaseType, Partial<TableMetadataCapabi
|
||||
triggers: false,
|
||||
ddl: false,
|
||||
},
|
||||
solr: {
|
||||
indexes: false,
|
||||
foreignKeys: false,
|
||||
triggers: false,
|
||||
ddl: false,
|
||||
},
|
||||
hbase: {
|
||||
indexes: false,
|
||||
foreignKeys: false,
|
||||
|
||||
@@ -1506,6 +1506,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
elasticsearch: "Elasticsearch",
|
||||
easysearch: "Easysearch",
|
||||
meilisearch: "Meilisearch",
|
||||
solr: "Apache Solr",
|
||||
qdrant: "Qdrant",
|
||||
milvus: "Milvus",
|
||||
weaviate: "Weaviate",
|
||||
@@ -4165,8 +4166,8 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
await loadMongoDatabases(connectionId);
|
||||
} else if (config.db_type === "dynamodb") {
|
||||
await loadDynamoDbTables(connectionId);
|
||||
} else if (config.db_type === "elasticsearch" || config.db_type === "easysearch" || config.db_type === "meilisearch") {
|
||||
// Reload: list indices.
|
||||
} else if (config.db_type === "elasticsearch" || config.db_type === "easysearch" || config.db_type === "meilisearch" || config.db_type === "solr") {
|
||||
// Reload: list indices/cores.
|
||||
await loadElasticsearchIndices(connectionId);
|
||||
} else if (config.db_type === "milvus") {
|
||||
await loadMilvusDatabases(connectionId);
|
||||
@@ -5041,7 +5042,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
async function loadConnectedConnectionRootForSidebarSearch(connectionId: string) {
|
||||
if (!connectedIds.value.has(connectionId)) return;
|
||||
const config = getConfig(connectionId);
|
||||
if (!config || ["redis", "etcd", "zookeeper", "consul", "mongodb", "dynamodb", "elasticsearch", "easysearch", "meilisearch", "milvus", "qdrant", "weaviate", "chromadb", "mq", "nacos"].includes(config.db_type)) return;
|
||||
if (!config || ["redis", "etcd", "zookeeper", "consul", "mongodb", "dynamodb", "elasticsearch", "easysearch", "meilisearch", "solr", "milvus", "qdrant", "weaviate", "chromadb", "mq", "nacos"].includes(config.db_type)) return;
|
||||
const node = findConnectionNode(connectionId);
|
||||
if (!node || node.type !== "connection" || hasConnectionMetadataChildren(node.children)) return;
|
||||
const scope = { kind: "connection-databases" as const, connectionId, driverProfile: metadataDriverProfile(config) };
|
||||
@@ -5464,9 +5465,10 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
await ensureConnected(connectionId);
|
||||
load = reclaimTreeNodeLoad(load, node);
|
||||
const isMeilisearch = getConfig(connectionId)?.db_type === "meilisearch";
|
||||
const isSolr = getConfig(connectionId)?.db_type === "solr";
|
||||
const collections = isMeilisearch
|
||||
? sortSidebarNames(await withMetadataLoadTimeout(connectionId, api.meilisearchListIndexes(connectionId), "Meilisearch indexes")).map((name) => ({ name, aliases: [] as string[] }))
|
||||
: [...(await withMetadataLoadTimeout(connectionId, api.documentListCollections(connectionId, "default"), "Elasticsearch indices"))].sort((left, right) => compareSidebarNames(left.name, right.name));
|
||||
: [...(await withMetadataLoadTimeout(connectionId, api.documentListCollections(connectionId, "default"), isSolr ? "Solr cores" : "Elasticsearch indices"))].sort((left, right) => compareSidebarNames(left.name, right.name));
|
||||
const indexNodes = collections.map((collection) => {
|
||||
const aliases = collection.aliases?.filter((alias) => alias.trim());
|
||||
return {
|
||||
@@ -7214,7 +7216,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
await loadMongoDatabases(node.connectionId);
|
||||
} else if (config?.db_type === "dynamodb") {
|
||||
await loadDynamoDbTables(node.connectionId);
|
||||
} else if (config?.db_type === "elasticsearch" || config?.db_type === "easysearch" || config?.db_type === "meilisearch") {
|
||||
} else if (config?.db_type === "elasticsearch" || config?.db_type === "easysearch" || config?.db_type === "meilisearch" || config?.db_type === "solr") {
|
||||
await loadElasticsearchIndices(node.connectionId);
|
||||
} else if (config?.db_type === "milvus") {
|
||||
await loadMilvusDatabases(node.connectionId);
|
||||
|
||||
@@ -3278,6 +3278,29 @@ export const useQueryStore = defineStore("query", () => {
|
||||
return registerOpenTab(tab);
|
||||
}
|
||||
|
||||
function openSolrAdmin(connectionId: string) {
|
||||
const existing = tabs.value.find((tab) => tab.mode === "solr-admin" && tab.connectionId === connectionId);
|
||||
if (existing) {
|
||||
switchTab(existing.id);
|
||||
return existing.id;
|
||||
}
|
||||
|
||||
const conn = useConnectionStore().getConfig(connectionId);
|
||||
const id = uuid();
|
||||
const tab: QueryTab = {
|
||||
id,
|
||||
title: conn?.name ? `${conn.name} - ${t("solrAdmin.title")}` : t("solrAdmin.title"),
|
||||
connectionId,
|
||||
database: "",
|
||||
sql: "",
|
||||
isExecuting: false,
|
||||
isCancelling: false,
|
||||
isExplaining: false,
|
||||
mode: "solr-admin",
|
||||
};
|
||||
return registerOpenTab(tab);
|
||||
}
|
||||
|
||||
function openDamengJobAdmin(connectionId: string) {
|
||||
const existing = tabs.value.find((tab) => tab.mode === "dameng-jobs" && tab.connectionId === connectionId);
|
||||
if (existing) {
|
||||
@@ -8997,6 +9020,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
openPostgresDashboard,
|
||||
openXuguDashboard,
|
||||
openNacosDashboard,
|
||||
openSolrAdmin,
|
||||
openDamengUsers,
|
||||
openDamengRoles,
|
||||
openDamengJobAdmin,
|
||||
|
||||
@@ -13,6 +13,10 @@ export function isMeilisearchDatabaseType(dbType?: DatabaseType): boolean {
|
||||
return dbType === "meilisearch";
|
||||
}
|
||||
|
||||
export function isSolrDatabaseType(dbType?: DatabaseType): boolean {
|
||||
return dbType === "solr";
|
||||
}
|
||||
|
||||
export interface SqlSnippet {
|
||||
id: string;
|
||||
label: string;
|
||||
@@ -1796,6 +1800,7 @@ export interface QueryTab {
|
||||
| "mysql-dashboard"
|
||||
| "postgres-dashboard"
|
||||
| "xugu-dashboard"
|
||||
| "solr-admin"
|
||||
| "dolt-version-control"
|
||||
| "plugin-workbench"
|
||||
| "plugin-filesystem";
|
||||
|
||||
@@ -39,6 +39,7 @@ export const CONNECTION_PROFILES = {
|
||||
oracle: { type: "oracle", port: 1521, user: "system", label: "Oracle", icon: "oracle" },
|
||||
elasticsearch: { type: "elasticsearch", port: 9200, user: "", label: "Elasticsearch", icon: "elasticsearch" },
|
||||
easysearch: { type: "easysearch", port: 9200, user: "", label: "Easysearch", icon: "easysearch" },
|
||||
solr: { type: "solr", port: 8983, user: "", label: "Apache Solr", icon: "solr" },
|
||||
meilisearch: { type: "meilisearch", port: 7700, user: "", label: "Meilisearch", icon: "meilisearch" },
|
||||
hbase: { type: "hbase", port: 8080, user: "", label: "Apache HBase", icon: "hbase" },
|
||||
qdrant: { type: "qdrant", port: 6333, user: "", label: "Qdrant", icon: "qdrant" },
|
||||
@@ -146,6 +147,7 @@ export const CONNECTION_PROFILE_ICONS = {
|
||||
oracle: "oracle",
|
||||
elasticsearch: "elasticsearch",
|
||||
easysearch: "easysearch",
|
||||
solr: "solr",
|
||||
meilisearch: "meilisearch",
|
||||
hbase: "hbase",
|
||||
qdrant: "qdrant",
|
||||
@@ -252,6 +254,7 @@ export const CONNECTION_PICKER_OPTIONS = [
|
||||
{ value: "oracle", label: "Oracle", category: "sql" },
|
||||
{ value: "elasticsearch", label: "Elasticsearch", category: "document" },
|
||||
{ value: "easysearch", label: "Easysearch", category: "document" },
|
||||
{ value: "solr", label: "Apache Solr", category: "document" },
|
||||
{ value: "meilisearch", label: "Meilisearch", category: "document" },
|
||||
{ value: "hbase", label: "Apache HBase", category: "document" },
|
||||
{ value: "qdrant", label: "Qdrant", category: "graph_ai" },
|
||||
|
||||
@@ -16,6 +16,7 @@ export const DATABASE_TYPES = [
|
||||
"elasticsearch",
|
||||
"easysearch",
|
||||
"meilisearch",
|
||||
"solr",
|
||||
"hbase",
|
||||
"qdrant",
|
||||
"chromadb",
|
||||
|
||||
Reference in new issue
Block a user