feat(db): 支持 VictoriaMetrics 数据库
* feat(victoriametrics): support native MetricsQL queries * fix(victoriametrics): 完善指标浏览和结果导出 * fix(victoriametrics): 修复指标对象通用操作入口 * fix(victoriametrics): 修正指标注释和截断提示 * fix(i18n): 补齐 VictoriaMetrics 连接配置翻译 --------- Co-authored-by: zipg <4047349+zipg@users.noreply.github.com>
This commit is contained in:
1 parent
930aa48c20
commit
2ede64f1cb
64 files changed
+1293
-64
No files matched your search
@@ -16,6 +16,7 @@ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import type { ConnectionConfig, ConnectionTestResult, DatabaseConnectionInfo, DatabaseType, HttpTunnelConfig, IdentifierCase, JdbcDriverInfo, JdbcLocalBundleInfo, JdbcMavenBundleInfo, ProxyTunnelConfig, SshConfigHostEntry, SshTunnelConfig, TransportLayerConfig } from "@/types/database";
|
||||
import type { InfluxDbExternalConfig, InfluxDbVersion } from "@/types/influxdb";
|
||||
import type { VictoriaMetricsExternalConfig } from "@/types/victoriametrics";
|
||||
import type { MqAdminConfig, MqAuth, MqSystemKind } from "@/types/mq";
|
||||
import type { MqttConnectionConfig } from "@/types/mqtt";
|
||||
import type { NacosAdminConfig, NacosAuthConfig, NacosImplementation, NacosMetricsMode, NacosRNacosConsoleAuth, NacosVersionMode } from "@/types/nacos";
|
||||
@@ -1025,6 +1026,7 @@ const driverProfiles: Record<
|
||||
mqtt: { type: "mqtt", port: 1883, user: "", label: "MQTT", icon: "mqtt", host: "127.0.0.1" },
|
||||
iris: { type: "iris", port: 1972, user: "_SYSTEM", label: "IRIS", icon: "iris" },
|
||||
influxdb: { type: "influxdb", port: 8086, user: "", label: "InfluxDB", icon: "InfluxDB" },
|
||||
victoriametrics: { type: "victoriametrics", port: 8428, user: "", label: "VictoriaMetrics", icon: "victoriametrics" },
|
||||
custom_mysql: {
|
||||
type: "mysql",
|
||||
port: 3306,
|
||||
@@ -1291,6 +1293,8 @@ function buildMqttExternalConfig(): MqttConnectionConfig {
|
||||
|
||||
const influxDbVersion = ref<InfluxDbVersion>("1");
|
||||
const influxDbOrg = ref("");
|
||||
const victoriaMetricsApiPath = ref("/prometheus");
|
||||
const victoriaMetricsLookback = ref("1h");
|
||||
|
||||
function resetInfluxDbFields(config?: Partial<InfluxDbExternalConfig>) {
|
||||
influxDbVersion.value = config?.version === "2" ? "2" : "1";
|
||||
@@ -1328,6 +1332,33 @@ function buildInfluxDbExternalConfig(): InfluxDbExternalConfig {
|
||||
return { version: "2", org };
|
||||
}
|
||||
|
||||
function resetVictoriaMetricsFields(config?: Partial<VictoriaMetricsExternalConfig>) {
|
||||
victoriaMetricsApiPath.value = config?.apiPath?.trim() || "/prometheus";
|
||||
victoriaMetricsLookback.value = config?.lookback?.trim() || "1h";
|
||||
}
|
||||
|
||||
function hydrateVictoriaMetricsFields(value: unknown) {
|
||||
if (!value || typeof value !== "object") {
|
||||
resetVictoriaMetricsFields();
|
||||
return;
|
||||
}
|
||||
const config = value as Partial<VictoriaMetricsExternalConfig> & { api_path?: string };
|
||||
resetVictoriaMetricsFields({
|
||||
apiPath: config.apiPath || config.api_path,
|
||||
lookback: config.lookback,
|
||||
});
|
||||
}
|
||||
|
||||
function buildVictoriaMetricsExternalConfig(): VictoriaMetricsExternalConfig {
|
||||
const apiPath = victoriaMetricsApiPath.value.trim().replace(/\/+$/, "");
|
||||
if (apiPath && !apiPath.startsWith("/")) throw new Error(t("connection.victoriametricsInvalidApiPath"));
|
||||
const lookback = victoriaMetricsLookback.value.trim();
|
||||
if (!/^\d+(?:ms|[smhdwy])$/.test(lookback) || lookback.startsWith("0")) {
|
||||
throw new Error(t("connection.victoriametricsInvalidLookback"));
|
||||
}
|
||||
return { apiPath, lookback };
|
||||
}
|
||||
|
||||
watch(influxDbVersion, (version) => {
|
||||
if (form.value.db_type !== "influxdb") return;
|
||||
if (version === "2") {
|
||||
@@ -2070,6 +2101,13 @@ function applyProfile(val: string, preserveConnectionFields = false) {
|
||||
form.value.password = "";
|
||||
form.value.connection_string = undefined;
|
||||
}
|
||||
if (profile.type === "victoriametrics") {
|
||||
resetVictoriaMetricsFields();
|
||||
form.value.database = "metrics";
|
||||
form.value.password = "";
|
||||
form.value.connection_string = undefined;
|
||||
form.value.url_params = "";
|
||||
}
|
||||
resetHiveKerberosFields(profile.type === "hive" ? form.value : undefined);
|
||||
}
|
||||
}
|
||||
@@ -2177,6 +2215,11 @@ watch(
|
||||
} else {
|
||||
resetInfluxDbFields();
|
||||
}
|
||||
if (config.db_type === "victoriametrics") {
|
||||
hydrateVictoriaMetricsFields(config.external_config);
|
||||
} else {
|
||||
resetVictoriaMetricsFields();
|
||||
}
|
||||
resetElasticsearchProxyFields(config.db_type === "elasticsearch" ? config.external_config : undefined);
|
||||
resetHiveKerberosFields(config.db_type === "hive" ? config : undefined);
|
||||
resetDamengJvmOptions(config.db_type === "dameng" ? config : undefined);
|
||||
@@ -2436,6 +2479,7 @@ const iconTypeMap: Record<string, string> = {
|
||||
sundb: "sundb",
|
||||
oscar: "oscar",
|
||||
influxdb: "influxdb",
|
||||
victoriametrics: "victoriametrics",
|
||||
jdbc: "jdbc",
|
||||
custom_mysql: "mysql",
|
||||
custom_postgres: "postgres",
|
||||
@@ -2519,6 +2563,7 @@ const dbOptions: DbOption[] = [
|
||||
{ value: "mqtt", label: "MQTT" },
|
||||
{ value: "nacos", label: "Nacos" },
|
||||
{ value: "influxdb", label: "InfluxDB" },
|
||||
{ value: "victoriametrics", label: "VictoriaMetrics" },
|
||||
{ value: "iris", label: "IRIS" },
|
||||
{ value: "jdbcx", label: "JDBCX" },
|
||||
{ value: "manticoresearch", label: "Manticore Search" },
|
||||
@@ -2565,7 +2610,7 @@ const dbCategoryDefinitions: Array<{
|
||||
{
|
||||
key: "timeseries",
|
||||
titleKey: "connection.databaseCategoryTimeseries",
|
||||
optionValues: ["questdb", "tdengine", "iotdb", "influxdb"],
|
||||
optionValues: ["questdb", "tdengine", "iotdb", "influxdb", "victoriametrics"],
|
||||
},
|
||||
{
|
||||
key: "mq",
|
||||
@@ -2670,9 +2715,31 @@ const sqliteExtensionPaths = computed({
|
||||
form.value.url_params = setSqliteExtensionPaths(form.value.url_params, value);
|
||||
},
|
||||
});
|
||||
const tlsCapableDatabaseTypes = new Set<DatabaseType>(["mysql", "starrocks", "postgres", "redshift", "gaussdb", "kwdb", "opengauss", "questdb", "dameng", "redis", "etcd", "clickhouse", "elasticsearch", "easysearch", "hbase", "qdrant", "milvus", "weaviate", "chromadb", "influxdb"]);
|
||||
const tlsCapableDatabaseTypes = new Set<DatabaseType>([
|
||||
"mysql",
|
||||
"starrocks",
|
||||
"postgres",
|
||||
"redshift",
|
||||
"gaussdb",
|
||||
"kwdb",
|
||||
"opengauss",
|
||||
"questdb",
|
||||
"dameng",
|
||||
"redis",
|
||||
"etcd",
|
||||
"clickhouse",
|
||||
"elasticsearch",
|
||||
"easysearch",
|
||||
"hbase",
|
||||
"qdrant",
|
||||
"milvus",
|
||||
"weaviate",
|
||||
"chromadb",
|
||||
"influxdb",
|
||||
"victoriametrics",
|
||||
]);
|
||||
const supportsTlsToggle = computed(() => tlsCapableDatabaseTypes.has(form.value.db_type));
|
||||
const supportsCaCertificatePath = computed(() => form.value.db_type === "clickhouse");
|
||||
const supportsCaCertificatePath = computed(() => form.value.db_type === "clickhouse" || form.value.db_type === "victoriametrics");
|
||||
const supportsGenericUrlParams = computed(() => form.value.db_type !== "manticoresearch" && form.value.db_type !== "hbase");
|
||||
const bareMysqlProfiles = new Set(["doris", "selectdb", "oceanbase"]);
|
||||
const supportsMysqlTlsOptions = computed(() => form.value.db_type === "starrocks" || (form.value.db_type === "mysql" && !bareMysqlProfiles.has(selectedType.value)));
|
||||
@@ -3117,6 +3184,9 @@ function applyConnectionUrlToForm(input: string): boolean {
|
||||
|
||||
const parsed = parseConnectionUrl(input, selectedType.value);
|
||||
form.value = applyParsedConnectionUrl(form.value, parsed);
|
||||
if (form.value.db_type === "victoriametrics") {
|
||||
hydrateVictoriaMetricsFields(form.value.external_config);
|
||||
}
|
||||
oracleTnsAdminPath.value = parseOracleTnsConnectionString(parsed.connectionString)?.tnsAdmin || "";
|
||||
selectedType.value = parsed.driverProfile;
|
||||
customDriverName.value = isCustomCompatibleProfile() ? parsed.driverLabel : "";
|
||||
@@ -3405,6 +3475,11 @@ function connectionConfigForSubmit(id: string, generatedName = ""): ConnectionCo
|
||||
config.password = config.password.trim();
|
||||
config.database = config.database?.trim() || undefined;
|
||||
}
|
||||
} else if (config.db_type === "victoriametrics") {
|
||||
config.external_config = buildVictoriaMetricsExternalConfig();
|
||||
config.connection_string = undefined;
|
||||
config.database = "metrics";
|
||||
config.username = config.username.trim();
|
||||
} else if (config.db_type === "elasticsearch") {
|
||||
config.external_config = buildElasticsearchExternalConfig(elasticsearchConnectionMode.value, elasticsearchKibanaBasePath.value, elasticsearchConnectivityCheckPath.value);
|
||||
} else if (config.db_type === "sqlserver") {
|
||||
@@ -3518,7 +3593,7 @@ function connectionConfigForSubmit(id: string, generatedName = ""): ConnectionCo
|
||||
config.client_cert_path = undefined;
|
||||
config.client_key_path = undefined;
|
||||
}
|
||||
if (config.db_type !== "mysql" && config.db_type !== "clickhouse" && config.db_type !== "etcd" && config.db_type !== "starrocks" && config.db_type !== "mongodb") {
|
||||
if (config.db_type !== "mysql" && config.db_type !== "clickhouse" && config.db_type !== "etcd" && config.db_type !== "starrocks" && config.db_type !== "mongodb" && config.db_type !== "victoriametrics") {
|
||||
config.ca_cert_path = undefined;
|
||||
} else {
|
||||
config.ca_cert_path = config.ca_cert_path?.trim() || "";
|
||||
@@ -6062,6 +6137,43 @@ function openExternalUrl(url: string) {
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="form.db_type === 'victoriametrics'">
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelClass">{{ t("connection.host") }}</Label>
|
||||
<Input v-model="form.host" class="col-span-2" />
|
||||
<Input v-model.number="form.port" type="number" class="col-span-1" />
|
||||
</div>
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<span />
|
||||
<label class="col-span-3 flex items-center gap-2 text-sm">
|
||||
<input type="checkbox" v-model="form.ssl" />
|
||||
<span>{{ t("connection.sslEnable") }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelClass">{{ t("connection.user") }}</Label>
|
||||
<Input v-model="form.username" class="col-span-3" autocomplete="username" />
|
||||
</div>
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelClass">{{ t("connection.password") }}</Label>
|
||||
<PasswordInput v-model="form.password" class="col-span-3" />
|
||||
</div>
|
||||
<div class="grid grid-cols-4 items-start gap-4">
|
||||
<Label :class="connectionLabelSmallClass">{{ t("connection.victoriametricsApiPath") }}</Label>
|
||||
<div class="col-span-3 space-y-1.5">
|
||||
<Input v-model="victoriaMetricsApiPath" placeholder="/prometheus" />
|
||||
<p class="text-xs leading-5 text-muted-foreground">{{ t("connection.victoriametricsApiPathHint") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-4 items-start gap-4">
|
||||
<Label :class="connectionLabelSmallClass">{{ t("connection.victoriametricsLookback") }}</Label>
|
||||
<div class="col-span-3 space-y-1.5">
|
||||
<Input v-model="victoriaMetricsLookback" class="w-28" placeholder="1h" />
|
||||
<p class="text-xs leading-5 text-muted-foreground">{{ t("connection.victoriametricsLookbackHint") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- InfluxDB: v1 username/password or v2 token/org/bucket -->
|
||||
<template v-else-if="form.db_type === 'influxdb'">
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
|
||||
@@ -1489,7 +1489,7 @@ function runKeymapExtension(codeMirrorKeymap: (typeof import("@codemirror/view")
|
||||
|
||||
function extendQueryEditorSelectionForView(currentView: EditorViewType): boolean {
|
||||
const databaseType = props.databaseType;
|
||||
const language = databaseType === "redis" || databaseType === "mongodb" || databaseType === "elasticsearch" ? "text" : "sql";
|
||||
const language = databaseType === "redis" || databaseType === "mongodb" || databaseType === "elasticsearch" || databaseType === "victoriametrics" ? "text" : "sql";
|
||||
return extendQueryEditorSelection(currentView, {
|
||||
databaseType,
|
||||
dialect: props.syntaxDialect ?? props.dialect,
|
||||
@@ -1688,7 +1688,7 @@ function getInsertValueHintTableColumns(table: string, schema?: string, database
|
||||
|
||||
function requestInsertValueHintTableColumns(table: string, schema?: string, database?: string) {
|
||||
if (!props.connectionId || props.database == null) return;
|
||||
if (props.databaseType === "redis" || props.databaseType === "mongodb" || props.databaseType === "elasticsearch" || props.databaseType === "easysearch") return;
|
||||
if (props.databaseType === "redis" || props.databaseType === "mongodb" || props.databaseType === "elasticsearch" || props.databaseType === "easysearch" || props.databaseType === "victoriametrics") return;
|
||||
const cacheKey = insertHintCacheKey({ name: table, schema, database });
|
||||
const hasCachedColumns = props.databaseType === "sqlserver" ? cachedInsertValueHintColumnsByTable.has(cacheKey) : cachedColumnsByTable.has(cacheKey);
|
||||
if (hasCachedColumns || pendingInsertValueHintColumnLoads.has(cacheKey)) return;
|
||||
@@ -2122,7 +2122,7 @@ function invalidateSemanticDiagnosticsForDocumentChange() {
|
||||
}
|
||||
|
||||
function shouldSkipSqlSemanticDiagnostics() {
|
||||
return props.databaseType !== "redis" && !settingsStore.editorSettings.sqlSemanticDiagnosticsEnabled;
|
||||
return props.databaseType === "victoriametrics" || (props.databaseType !== "redis" && !settingsStore.editorSettings.sqlSemanticDiagnosticsEnabled);
|
||||
}
|
||||
|
||||
function rangesOverlap(left: { from: number; to: number }, right: { from: number; to: number }): boolean {
|
||||
@@ -2248,7 +2248,7 @@ async function refreshSemanticDiagnostics(options: { preserveOutsideRanges?: boo
|
||||
setSemanticDiagnostics([]);
|
||||
return;
|
||||
}
|
||||
if (props.databaseType === "mongodb" || props.databaseType === "elasticsearch" || props.databaseType === "easysearch") {
|
||||
if (props.databaseType === "mongodb" || props.databaseType === "elasticsearch" || props.databaseType === "easysearch" || props.databaseType === "victoriametrics") {
|
||||
setSemanticDiagnostics([]);
|
||||
return;
|
||||
}
|
||||
@@ -2363,6 +2363,7 @@ function scheduleSemanticDiagnostics(delay = 500, options: { preserveOutsideRang
|
||||
|
||||
async function formatCurrentSql() {
|
||||
if (props.readOnly) return;
|
||||
if (props.databaseType === "victoriametrics") return;
|
||||
const currentView = view.value;
|
||||
if (!currentView) return;
|
||||
|
||||
@@ -2581,7 +2582,7 @@ function localCompletionSchemasForDatabaseDisambiguation(completionContext: Retu
|
||||
}
|
||||
|
||||
function shouldInsertSqlCompletionSpace(): boolean {
|
||||
return props.databaseType !== "mongodb" && props.databaseType !== "redis" && props.databaseType !== "elasticsearch" && props.databaseType !== "easysearch";
|
||||
return props.databaseType !== "mongodb" && props.databaseType !== "redis" && props.databaseType !== "elasticsearch" && props.databaseType !== "easysearch" && props.databaseType !== "victoriametrics";
|
||||
}
|
||||
|
||||
function completionOptionForItem(item: QueryCompletionItem) {
|
||||
@@ -2764,6 +2765,7 @@ async function provideSqlCompletions(context: CompletionContext) {
|
||||
if (props.databaseType === "redis") {
|
||||
return provideRedisCompletions(currentState, position, explicit);
|
||||
}
|
||||
if (props.databaseType === "victoriametrics") return null;
|
||||
const hasDatabase = props.database != null;
|
||||
|
||||
const epoch = ++completionEpoch;
|
||||
@@ -2971,6 +2973,7 @@ function shouldStartSqlCompletionAfterInput(insertedText: string, removedText: s
|
||||
if (props.databaseType === "mongodb") {
|
||||
return !!(insertedText || removedText) && shouldAutoOpenMongoCompletion(fullDoc, position);
|
||||
}
|
||||
if (props.databaseType === "victoriametrics") return false;
|
||||
if (!insertedText && removedText) {
|
||||
const completionContext = getSqlCompletionContext(fullDoc, position, sqlCompletionDialectOptions());
|
||||
return isTableNameCompletionContext(completionContext) && shouldAutoOpenSqlCompletion(fullDoc, position, sqlCompletionDialectOptions());
|
||||
@@ -4010,7 +4013,7 @@ onMounted(async () => {
|
||||
const endLine = view.state.doc.lineAt(Math.max(range.from, frameTo - 1));
|
||||
let insertValueHints: Array<{ from: number; column: string }> = [];
|
||||
try {
|
||||
if (settingsStore.editorSettings.showInsertValueHints && props.databaseType !== "redis" && props.databaseType !== "mongodb" && props.databaseType !== "elasticsearch" && props.databaseType !== "easysearch") {
|
||||
if (settingsStore.editorSettings.showInsertValueHints && props.databaseType !== "redis" && props.databaseType !== "mongodb" && props.databaseType !== "elasticsearch" && props.databaseType !== "easysearch" && props.databaseType !== "victoriametrics") {
|
||||
const statementSql = view.state.doc.sliceString(range.from, range.to);
|
||||
if (/\binsert\b/i.test(statementSql)) {
|
||||
insertValueHints = parseInsertValueHints(statementSql, { resolveTableColumns: getInsertValueHintTableColumns }).map((hint) => ({
|
||||
@@ -4163,7 +4166,7 @@ onMounted(async () => {
|
||||
sqlSignatureComp.of(buildSqlSignatureExtension()),
|
||||
diagnosticComp.of(buildSqlDiagnosticExtension()),
|
||||
createInsertValueHintsExtension({
|
||||
isEnabled: () => settingsStore.editorSettings.showInsertValueHints && props.databaseType !== "redis" && props.databaseType !== "mongodb" && props.databaseType !== "elasticsearch" && props.databaseType !== "easysearch",
|
||||
isEnabled: () => settingsStore.editorSettings.showInsertValueHints && props.databaseType !== "redis" && props.databaseType !== "mongodb" && props.databaseType !== "elasticsearch" && props.databaseType !== "easysearch" && props.databaseType !== "victoriametrics",
|
||||
getTableColumns: getInsertValueHintTableColumns,
|
||||
requestTableColumns: requestInsertValueHintTableColumns,
|
||||
}),
|
||||
@@ -4746,7 +4749,7 @@ watch(
|
||||
watch(
|
||||
() => settingsStore.editorSettings.sqlSemanticDiagnosticsEnabled,
|
||||
(enabled) => {
|
||||
if (props.databaseType === "redis") return;
|
||||
if (props.databaseType === "redis" || props.databaseType === "victoriametrics") return;
|
||||
if (!shouldSkipSqlSemanticDiagnostics() && enabled) {
|
||||
scheduleSemanticDiagnostics(0);
|
||||
return;
|
||||
|
||||
@@ -139,7 +139,7 @@ import { BOOLEAN_CELL_EDITOR_VALUES, booleanCellEditorValue, isBooleanCellValue,
|
||||
import { resolveDataGridColumnsByResultIndex } from "@/lib/dataGrid/dataGridColumnMetadata";
|
||||
import { isCancelSearchShortcut, isCopyCurrentRowShortcut, isDeleteCurrentRowShortcut, isFocusSearchShortcut, isModRShortcut, isSaveShortcut, isToggleTransposeShortcut } from "@/lib/editor/keyboardShortcuts";
|
||||
import { dataGridHeaderContentWidth, scrollbarGutterWidth } from "@/lib/dataGrid/dataGridScrollGutter";
|
||||
import { canFetchNextDataGridSegment, canGoNextDataGridPage, dataGridTotalRowCountLabelKey, hasCompleteLocalDataGridResult, resolveDataGridPaginationTotal, type DataGridInexactTotalRowCountMode } from "@/lib/dataGrid/dataGridPagination";
|
||||
import { canFetchNextDataGridSegment, canGoNextDataGridPage, dataGridTotalRowCountLabelKey, dataGridTruncationHintKey, hasCompleteLocalDataGridResult, resolveDataGridPaginationTotal, type DataGridInexactTotalRowCountMode } from "@/lib/dataGrid/dataGridPagination";
|
||||
import { dataGridCountQueryOptions } from "@/lib/dataGrid/dataGridQueryOptions";
|
||||
import { dataGridBottomScrollTop, dataGridScrollPosition, isDataGridAtScrollBottom, isDataGridNearScrollBottom, isDataGridPrefixAppend, shouldCheckInfiniteScrollAfterScroll, type DataGridScrollPosition } from "@/lib/dataGrid/dataGridInfiniteScroll";
|
||||
import { CANVAS_DATA_GRID_ROW_HEIGHT, canvasDataGridActionOverlayWidth, canvasDataGridActionReservedWidth, dataGridSearchMatchKey, drawCanvasDataGrid, type CanvasDevicePixelSize } from "@/lib/dataGrid/canvasDataGridRenderer";
|
||||
@@ -2484,6 +2484,7 @@ watch(
|
||||
const manualTotalRowCount = ref<number | undefined>(undefined);
|
||||
const manualTotalRowCountLoading = ref(false);
|
||||
const showTruncationWarning = computed(() => props.result.truncated === true && typeof props.pageLimit !== "number" && props.result.has_more !== true);
|
||||
const truncationHintKey = computed(() => dataGridTruncationHintKey(resolvedDatabaseType.value));
|
||||
const isResultsContext = computed(() => props.context === "results");
|
||||
// affected_rows reported by the backend can be larger than the rows we
|
||||
// actually have in memory — e.g. ES auto-pages SELECT * on a big index and
|
||||
@@ -2613,8 +2614,8 @@ watch(
|
||||
const showQueryEditReadOnlyBadge = computed(() => isResultsContext.value && hasData.value && !props.editable && !!props.queryEditabilityReason);
|
||||
const queryEditReadOnlyReason = computed(() => (props.queryEditabilityReason ? t(`grid.queryEditUnsupported.${props.queryEditabilityReason}`) : ""));
|
||||
const showKeylessEditWarning = computed(() => !!props.editable && !!props.tableMeta && canUseKeylessRowPredicate(props.databaseType, props.tableMeta.primaryKeys ?? []));
|
||||
const canShowWhereSearch = computed(() => !!props.onExecuteSql && !isResultsContext.value);
|
||||
const canUseWhereSearch = computed(() => !!props.tableMeta && !!props.onExecuteSql && !isResultsContext.value);
|
||||
const canShowWhereSearch = computed(() => !!props.onExecuteSql && !isResultsContext.value && resolvedDatabaseType.value !== "victoriametrics");
|
||||
const canUseWhereSearch = computed(() => !!props.tableMeta && canShowWhereSearch.value);
|
||||
const canUseServerColumnFilter = computed(() => canUseWhereSearch.value && !!props.connectionId && !!props.tableMeta);
|
||||
type DataGridTableMeta = NonNullable<typeof props.tableMeta>;
|
||||
const hiveTableTransactional = ref<boolean | undefined>(undefined);
|
||||
@@ -8635,7 +8636,7 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
|
||||
</div>
|
||||
<!-- Truncation warning banner -->
|
||||
<div v-if="showTruncationWarning" class="shrink-0 px-3 py-1 bg-amber-500/10 border-b border-amber-500/20 text-xs text-amber-600 dark:text-amber-400 flex items-center gap-1.5">
|
||||
<span>{{ t("grid.truncatedHint", { count: result.rows.length }) }}</span>
|
||||
<span>{{ t(truncationHintKey, { count: result.rows.length }) }}</span>
|
||||
</div>
|
||||
<!-- Content area: table + side/bottom detail panes -->
|
||||
<div class="flex-1 grid min-h-0 overflow-hidden" :style="contentGridStyle">
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { dataGridTotalRowCountLabelKey } from "@/lib/dataGrid/dataGridPagination";
|
||||
import { dataGridTotalRowCountLabelKey, dataGridTruncationHintKey } from "@/lib/dataGrid/dataGridPagination";
|
||||
import DataGrid from "../DataGrid.vue";
|
||||
|
||||
type VuePropDefinition = { default?: unknown };
|
||||
type VueComponentWithProps = { props?: Record<string, VuePropDefinition> };
|
||||
|
||||
describe("DataGrid total row count exactness", () => {
|
||||
it("uses a VictoriaMetrics-specific truncation explanation", () => {
|
||||
expect(dataGridTruncationHintKey("victoriametrics")).toBe("grid.victoriaMetricsTruncatedHint");
|
||||
expect(dataGridTruncationHintKey("mysql")).toBe("grid.truncatedHint");
|
||||
expect(dataGridTruncationHintKey()).toBe("grid.truncatedHint");
|
||||
});
|
||||
|
||||
it("treats totals as exact unless a caller explicitly marks them as a lower bound", () => {
|
||||
const component = DataGrid as unknown as VueComponentWithProps;
|
||||
expect(component.props?.totalRowCountIsExact?.default).toBe(true);
|
||||
|
||||
@@ -100,6 +100,7 @@ const assetIcons: Record<string, string> = {
|
||||
nacos: "nacos.png",
|
||||
iris: "iris",
|
||||
influxdb: "influxdb",
|
||||
victoriametrics: "victoriametrics.png",
|
||||
zookeeper: "zookeeper",
|
||||
oscar: "oscar.png",
|
||||
jdbcx: "jdbcx",
|
||||
|
||||
@@ -96,7 +96,21 @@ const activeConnectionValue = computed(() => props.activeConnection?.id || "");
|
||||
const activeSchemaValue = computed(() => props.activeTab.schema || "");
|
||||
const supportsExplain = computed(() => {
|
||||
const dbType = props.activeConnection?.db_type;
|
||||
return dbType !== "redis" && dbType !== "mongodb" && dbType !== "elasticsearch" && dbType !== "easysearch" && dbType !== "qdrant" && dbType !== "milvus" && dbType !== "weaviate" && dbType !== "chromadb" && dbType !== "etcd" && dbType !== "zookeeper" && dbType !== "mq" && dbType !== "nacos";
|
||||
return (
|
||||
dbType !== "redis" &&
|
||||
dbType !== "mongodb" &&
|
||||
dbType !== "elasticsearch" &&
|
||||
dbType !== "easysearch" &&
|
||||
dbType !== "qdrant" &&
|
||||
dbType !== "milvus" &&
|
||||
dbType !== "weaviate" &&
|
||||
dbType !== "chromadb" &&
|
||||
dbType !== "etcd" &&
|
||||
dbType !== "zookeeper" &&
|
||||
dbType !== "mq" &&
|
||||
dbType !== "nacos" &&
|
||||
dbType !== "victoriametrics"
|
||||
);
|
||||
});
|
||||
const isSingleDb = computed(() => isSingleDatabase(props.activeConnection?.db_type));
|
||||
const supportsExPaste = computed(() => supportsSqlInListPaste(props.activeConnection?.db_type));
|
||||
|
||||
@@ -198,6 +198,8 @@ const isResizingSidePanel = ref(false);
|
||||
const sidePanelGuard = createSidePanelRequestGuard();
|
||||
const tableMetadataCapabilities = computed<TableMetadataCapabilities>(() => getTableMetadataCapabilities(effectiveDatabaseType.value));
|
||||
const effectiveDatabaseType = computed(() => effectiveDatabaseTypeForConnection(props.connection) ?? props.connection.db_type);
|
||||
const isVictoriaMetrics = computed(() => effectiveDatabaseType.value === "victoriametrics");
|
||||
const objectRowsLabel = computed(() => t(isVictoriaMetrics.value ? "objects.series" : "objects.rows"));
|
||||
const tableStructureDatabaseType = computed(() => tableStructureDatabaseTypeForConnection(props.connection) ?? props.connection.db_type);
|
||||
const sourceEditableText = ref("");
|
||||
const sourceDraft = ref("");
|
||||
@@ -438,7 +440,8 @@ function toggleCheckboxColumn() {
|
||||
const objectBrowserColumns = computed<ObjectBrowserColumnKey[]>(() => {
|
||||
const columns: ObjectBrowserColumnKey[] = [];
|
||||
if (showCheckboxColumn.value) columns.push("select");
|
||||
columns.push("name", "type", "estimatedRows", "totalBytes");
|
||||
columns.push("name", "type", "estimatedRows");
|
||||
if (!isVictoriaMetrics.value) columns.push("totalBytes");
|
||||
if (hasCreatedAt.value) columns.push("created_at");
|
||||
if (hasUpdatedAt.value) columns.push("updated_at");
|
||||
columns.push("comment");
|
||||
@@ -600,7 +603,8 @@ function toggleSort(key: ObjectBrowserSortKey) {
|
||||
}
|
||||
|
||||
const sortKeyOptions = computed<ObjectBrowserSortKey[]>(() => {
|
||||
const options: ObjectBrowserSortKey[] = ["name", "type", "estimatedRows", "totalBytes"];
|
||||
const options: ObjectBrowserSortKey[] = ["name", "type", "estimatedRows"];
|
||||
if (!isVictoriaMetrics.value) options.push("totalBytes");
|
||||
if (hasCreatedAt.value) options.push("created_at");
|
||||
if (hasUpdatedAt.value) options.push("updated_at");
|
||||
options.push("comment");
|
||||
@@ -617,7 +621,7 @@ watch(sortKeyOptions, (options) => {
|
||||
function sortKeyLabel(key: ObjectBrowserSortKey): string {
|
||||
if (key === "name") return t("objects.name");
|
||||
if (key === "type") return t("objects.type");
|
||||
if (key === "estimatedRows") return t("objects.rows");
|
||||
if (key === "estimatedRows") return objectRowsLabel.value;
|
||||
if (key === "totalBytes") return t("objects.size");
|
||||
if (key === "created_at") return t("objects.createdAt");
|
||||
if (key === "updated_at") return t("objects.updatedAt");
|
||||
@@ -1856,6 +1860,22 @@ async function exportTableData(row: ObjectBrowserRow, format: "csv" | "xlsx" | "
|
||||
|
||||
let task: ExportTask | null = null;
|
||||
try {
|
||||
if (isVictoriaMetrics.value) {
|
||||
const result = await fetchTableDataForExport({
|
||||
databaseType: effectiveDatabaseType.value,
|
||||
schema,
|
||||
tableName: row.name,
|
||||
executePage: (sql) => api.executeQuery(props.connection.id, props.database, sql),
|
||||
});
|
||||
if (format === "csv") {
|
||||
await api.exportQueryResultCsv(filePath, result.columns, result.rows);
|
||||
} else {
|
||||
const comments = useCommentHeader ? result.columns.map((name) => columnInfos?.find((column) => column.name.toLocaleLowerCase() === name.toLocaleLowerCase())?.comment ?? null) : undefined;
|
||||
await api.exportQueryResultXlsx(filePath, row.name, result.columns, result.column_types ?? result.columns.map(() => ""), comments, result.rows);
|
||||
}
|
||||
toast(t("grid.exported"));
|
||||
return;
|
||||
}
|
||||
let columns: string[] | undefined;
|
||||
let columnComments: (string | null)[] | undefined;
|
||||
|
||||
@@ -1969,7 +1989,7 @@ function copySelectedTablesToClipboard() {
|
||||
}
|
||||
|
||||
function canPasteTableClipboard(): boolean {
|
||||
return tableClipboardMatchesTarget(normalizedObjectBrowserTableClipboardEntries(), pasteTableTargetContext());
|
||||
return !isVictoriaMetrics.value && tableClipboardMatchesTarget(normalizedObjectBrowserTableClipboardEntries(), pasteTableTargetContext());
|
||||
}
|
||||
|
||||
function normalizedObjectBrowserTableClipboardEntries() {
|
||||
@@ -1982,6 +2002,7 @@ function normalizedObjectBrowserTableClipboardEntries() {
|
||||
}
|
||||
|
||||
function canTransferTableClipboard(): boolean {
|
||||
if (isVictoriaMetrics.value) return false;
|
||||
const entries = normalizedObjectBrowserTableClipboardEntries();
|
||||
const target = pasteTableTargetContext();
|
||||
if (entries.length === 0 || props.connection.read_only) return false;
|
||||
@@ -2548,15 +2569,16 @@ watch(
|
||||
// ---- CustomContextMenu helpers ----
|
||||
|
||||
function exportDataSubmenu(item: ObjectBrowserRow): ContextMenuItem {
|
||||
const formats: ContextMenuItem[] = [
|
||||
{ label: "CSV", action: () => exportData(item, "csv") },
|
||||
{ label: "JSON", action: () => exportData(item, "json") },
|
||||
];
|
||||
if (!isVictoriaMetrics.value) formats.push({ label: "SQL INSERT", action: () => exportData(item, "sql") });
|
||||
formats.push({ label: "XLSX", action: () => exportDataXlsx(item) });
|
||||
return {
|
||||
label: t("contextMenu.exportData"),
|
||||
icon: Upload,
|
||||
children: [
|
||||
{ label: "CSV", action: () => exportData(item, "csv") },
|
||||
{ label: "JSON", action: () => exportData(item, "json") },
|
||||
{ label: "SQL INSERT", action: () => exportData(item, "sql") },
|
||||
{ label: "XLSX", action: () => exportDataXlsx(item) },
|
||||
],
|
||||
children: formats,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2574,6 +2596,7 @@ function objectBrowserTableClipboardMenuState(item: ObjectBrowserRow) {
|
||||
}
|
||||
|
||||
function tableClipboardMenuItems(item: ObjectBrowserRow): ContextMenuItem[] {
|
||||
if (isVictoriaMetrics.value) return [];
|
||||
const copyItem: ContextMenuItem = { label: t("contextMenu.copyTable"), action: () => copySingleTableToClipboard(item), icon: Copy };
|
||||
const state = objectBrowserTableClipboardMenuState(item);
|
||||
if (state === "copy") return [copyItem];
|
||||
@@ -2590,6 +2613,16 @@ function selectedBatchTableCountLabel(key: "batchDrop" | "batchTruncate" | "batc
|
||||
}
|
||||
|
||||
function getTableMenuItems(item: ObjectBrowserRow): ContextMenuItem[] {
|
||||
if (isVictoriaMetrics.value) {
|
||||
return [
|
||||
{ label: t("contextMenu.viewData"), action: () => openViewData(item), icon: Table2 },
|
||||
{ label: t("contextMenu.newQuery"), action: () => openNewQuery(item), icon: TerminalSquare },
|
||||
{ label: "", separator: true },
|
||||
exportDataSubmenu(item),
|
||||
{ label: "", separator: true },
|
||||
{ label: t("contextMenu.copyName"), action: () => copyName(item), icon: Copy },
|
||||
];
|
||||
}
|
||||
const useBatchActions = isSelectedBatchTableContext(item);
|
||||
return [
|
||||
{ label: t("contextMenu.viewData"), action: () => openViewData(item), icon: Table2 },
|
||||
@@ -2789,23 +2822,23 @@ function getObjectBrowserMenuItems(item: ObjectBrowserRow): ContextMenuItem[] {
|
||||
<div class="min-w-0 flex-1 truncate text-muted-foreground">
|
||||
{{ t("objects.selectedTables", { count: selectedTableCount }) }}
|
||||
</div>
|
||||
<Button variant="ghost" size="sm" class="h-7 px-2 text-xs" @click="openBatchDatabaseExport">
|
||||
<Button v-if="!isVictoriaMetrics" variant="ghost" size="sm" class="h-7 px-2 text-xs" @click="openBatchDatabaseExport">
|
||||
<Upload class="mr-1.5 h-3.5 w-3.5" />
|
||||
{{ t("objects.exportSelected") }}
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" class="h-7 px-2 text-xs" @click="copySelectedTablesToClipboard">
|
||||
<Button v-if="!isVictoriaMetrics" variant="ghost" size="sm" class="h-7 px-2 text-xs" @click="copySelectedTablesToClipboard">
|
||||
<Clipboard class="mr-1.5 h-3.5 w-3.5" />
|
||||
{{ t("objects.copyTableSelected") }}
|
||||
</Button>
|
||||
<Button v-if="supportsTruncateTable" variant="ghost" size="sm" class="h-7 px-2 text-xs text-destructive" @click="requestBatchTruncateTables">
|
||||
<Button v-if="!isVictoriaMetrics && supportsTruncateTable" variant="ghost" size="sm" class="h-7 px-2 text-xs text-destructive" @click="requestBatchTruncateTables">
|
||||
<Scissors class="mr-1.5 h-3.5 w-3.5" />
|
||||
{{ t("objects.truncateSelected") }}
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" class="h-7 px-2 text-xs text-destructive" @click="requestBatchEmptyTables">
|
||||
<Button v-if="!isVictoriaMetrics" variant="ghost" size="sm" class="h-7 px-2 text-xs text-destructive" @click="requestBatchEmptyTables">
|
||||
<Eraser class="mr-1.5 h-3.5 w-3.5" />
|
||||
{{ t("contextMenu.batchEmpty", { count: selectedTableCount }) }}
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" class="h-7 px-2 text-xs text-destructive" @click="requestBatchDropTables">
|
||||
<Button v-if="!isVictoriaMetrics" variant="ghost" size="sm" class="h-7 px-2 text-xs text-destructive" @click="requestBatchDropTables">
|
||||
<Trash2 class="mr-1.5 h-3.5 w-3.5" />
|
||||
{{ t("objects.dropSelected") }}
|
||||
</Button>
|
||||
@@ -2858,7 +2891,7 @@ function getObjectBrowserMenuItems(item: ObjectBrowserRow): ContextMenuItem[] {
|
||||
</div>
|
||||
<div class="relative flex min-w-0 items-center">
|
||||
<button class="flex min-w-0 items-center gap-1 truncate pr-4 text-left" type="button" :title="t('objects.statisticsHint')" @click="toggleSort('estimatedRows')">
|
||||
<span class="truncate">{{ t("objects.rows") }}</span>
|
||||
<span class="truncate">{{ objectRowsLabel }}</span>
|
||||
<component :is="sortIconFor('estimatedRows')" v-if="sortIconFor('estimatedRows')" class="h-3 w-3 shrink-0" />
|
||||
</button>
|
||||
<div
|
||||
@@ -2869,7 +2902,7 @@ function getObjectBrowserMenuItems(item: ObjectBrowserRow): ContextMenuItem[] {
|
||||
<GripVertical class="h-3 w-3" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative flex min-w-0 items-center">
|
||||
<div v-if="!isVictoriaMetrics" class="relative flex min-w-0 items-center">
|
||||
<button class="flex min-w-0 items-center gap-1 truncate pr-4 text-left" type="button" :title="t('objects.statisticsHint')" @click="toggleSort('totalBytes')">
|
||||
<span class="truncate">{{ t("objects.size") }}</span>
|
||||
<component :is="sortIconFor('totalBytes')" v-if="sortIconFor('totalBytes')" class="h-3 w-3 shrink-0" />
|
||||
@@ -2962,7 +2995,7 @@ function getObjectBrowserMenuItems(item: ObjectBrowserRow): ContextMenuItem[] {
|
||||
<div class="truncate text-xs tabular-nums text-muted-foreground" :title="item.estimatedRows == null ? '' : formatObjectBrowserCount(item.estimatedRows)">
|
||||
{{ formatObjectBrowserCount(item.estimatedRows) }}
|
||||
</div>
|
||||
<div class="truncate text-xs tabular-nums text-muted-foreground" :title="item.totalBytes == null ? '' : formatObjectBrowserBytes(item.totalBytes)">
|
||||
<div v-if="!isVictoriaMetrics" class="truncate text-xs tabular-nums text-muted-foreground" :title="item.totalBytes == null ? '' : formatObjectBrowserBytes(item.totalBytes)">
|
||||
{{ formatObjectBrowserBytes(item.totalBytes) }}
|
||||
</div>
|
||||
<div v-if="hasCreatedAt" class="truncate text-xs tabular-nums text-muted-foreground" :title="formatObjectBrowserTimestamp(item.created_at)">
|
||||
@@ -3008,7 +3041,9 @@ function getObjectBrowserMenuItems(item: ObjectBrowserRow): ContextMenuItem[] {
|
||||
<span v-if="item.estimatedRows != null && item.estimatedRows > 0" class="object-browser-stat-badge object-browser-stat-badge-rows rounded-full bg-primary/10 px-1.5 py-0.5 text-[10px] font-medium tabular-nums text-primary">{{
|
||||
formatObjectBrowserCount(item.estimatedRows)
|
||||
}}</span>
|
||||
<span v-if="item.totalBytes != null && item.totalBytes > 0" class="object-browser-stat-badge object-browser-stat-badge-bytes rounded-full bg-muted px-1.5 py-0.5 text-[10px] font-medium tabular-nums text-muted-foreground">{{ formatObjectBrowserBytes(item.totalBytes) }}</span>
|
||||
<span v-if="!isVictoriaMetrics && item.totalBytes != null && item.totalBytes > 0" class="object-browser-stat-badge object-browser-stat-badge-bytes rounded-full bg-muted px-1.5 py-0.5 text-[10px] font-medium tabular-nums text-muted-foreground">{{
|
||||
formatObjectBrowserBytes(item.totalBytes)
|
||||
}}</span>
|
||||
</div>
|
||||
<!-- Always reserve timestamp/comment slots when the dataset has them so every card shares one height. -->
|
||||
<div v-if="hasCreatedAt || hasUpdatedAt" class="flex min-h-[15px] items-center gap-1 text-[10px] leading-[15px] text-muted-foreground/70">
|
||||
@@ -3034,7 +3069,7 @@ function getObjectBrowserMenuItems(item: ObjectBrowserRow): ContextMenuItem[] {
|
||||
<div class="flex items-center gap-2 px-3 py-1.5 border-b shrink-0 bg-muted/20 h-9">
|
||||
<TableProperties class="w-3.5 h-3.5 text-muted-foreground" />
|
||||
<span class="text-xs font-medium flex-1 min-w-0 truncate">{{ sidePanelRow?.name }}</span>
|
||||
<div v-if="tableInfoTab === 'ddl'" class="table-info-actions flex min-w-0 shrink-0 items-center gap-1">
|
||||
<div v-if="tableInfoTab === 'ddl' && tableMetadataCapabilities.ddl" class="table-info-actions flex min-w-0 shrink-0 items-center gap-1">
|
||||
<Button variant="ghost" size="sm" class="table-info-action-button h-6 px-2 text-xs" :title="t('grid.copyDdl')" :aria-label="t('grid.copyDdl')" @click="copyTableDdl">
|
||||
<Copy class="w-3 h-3" />
|
||||
<span class="table-info-action-label">{{ t("grid.copyDdl") }}</span>
|
||||
|
||||
@@ -891,7 +891,7 @@ function normalizedTreeClipboardTableEntries(): TableClipboardTableContext[] {
|
||||
}
|
||||
|
||||
function canPasteTreeClipboardToCurrentNode(): boolean {
|
||||
return tableClipboardMatchesTarget(normalizedTreeClipboardTableEntries(), pasteTableTargetContext());
|
||||
return currentDatabaseType() !== "victoriametrics" && tableClipboardMatchesTarget(normalizedTreeClipboardTableEntries(), pasteTableTargetContext());
|
||||
}
|
||||
|
||||
function canTransferTreeClipboardToCurrentNode(): boolean {
|
||||
@@ -924,6 +924,7 @@ function openTransferFromTreeClipboard(): boolean {
|
||||
}
|
||||
|
||||
function requestPasteTreeClipboard(): boolean {
|
||||
if (currentDatabaseType() === "victoriametrics") return false;
|
||||
claimTreeItemDialogOwnership();
|
||||
ensureDangerDialogRouting();
|
||||
routeTreeItemDialogController();
|
||||
@@ -1376,6 +1377,7 @@ async function newDeleteTemplate() {
|
||||
}
|
||||
|
||||
async function generateDdlTemplate() {
|
||||
if (currentDatabaseType() === "victoriametrics") return;
|
||||
const targets = selectedDdlTargets();
|
||||
if (!targets.length) return;
|
||||
const tabTarget = targets.find((target) => target.id === activeNode.value.id) ?? targets[0]!;
|
||||
@@ -1411,6 +1413,7 @@ function selectedDdlTargets() {
|
||||
}
|
||||
|
||||
async function openDdl() {
|
||||
if (currentDatabaseType() === "victoriametrics") return;
|
||||
const targets = selectedDdlTargets();
|
||||
if (!targets.length) return;
|
||||
if (targets.length > 1) {
|
||||
@@ -1471,6 +1474,10 @@ function updateTreeClipboardForNodes(nodes: TreeNode[]) {
|
||||
connectionStore.treeClipboard = null;
|
||||
return;
|
||||
}
|
||||
if (tableNodes.some((node) => databaseTypeForNode(node) === "victoriametrics")) {
|
||||
connectionStore.treeClipboard = null;
|
||||
return;
|
||||
}
|
||||
connectionStore.treeClipboard = {
|
||||
kind: "table-copy",
|
||||
tables: tableNodes.map((node) => ({
|
||||
@@ -3083,6 +3090,7 @@ async function confirmDropSchema() {
|
||||
|
||||
function duplicateStructure(source: TreeNode = activeNode.value) {
|
||||
if (!isDuplicateStructureSource(source)) return;
|
||||
if (databaseTypeForNode(source) === "victoriametrics") return;
|
||||
duplicateStructureSource.value = source;
|
||||
duplicateTableName.value = `${source.label}_copy`;
|
||||
showDuplicateDialog.value = true;
|
||||
@@ -3096,6 +3104,7 @@ async function confirmDuplicateStructure() {
|
||||
const node = duplicateStructureSource.value || (isDuplicateStructureSource(activeNode.value) ? activeNode.value : null);
|
||||
const newName = duplicateTableName.value.trim();
|
||||
if (!newName || !node) return;
|
||||
if (databaseTypeForNode(node) === "victoriametrics") return;
|
||||
showDuplicateDialog.value = false;
|
||||
try {
|
||||
await connectionStore.ensureConnected(node.connectionId);
|
||||
@@ -3840,16 +3849,17 @@ const shortcutRefresh = "F5";
|
||||
|
||||
const shortcutDelete = "Delete";
|
||||
|
||||
function exportDataSubmenu(): ContextMenuItem {
|
||||
function exportDataSubmenu(includeSqlInsert = true): ContextMenuItem {
|
||||
const children: ContextMenuItem[] = [
|
||||
{ label: "CSV", action: () => exportData("csv") },
|
||||
{ label: "JSON", action: () => exportData("json") },
|
||||
];
|
||||
if (includeSqlInsert) children.push({ label: "SQL INSERT", action: () => exportData("sql") });
|
||||
children.push({ label: "XLSX", action: () => exportDataXlsx() });
|
||||
return {
|
||||
label: t("contextMenu.exportData"),
|
||||
icon: Upload,
|
||||
children: [
|
||||
{ label: "CSV", action: () => exportData("csv") },
|
||||
{ label: "JSON", action: () => exportData("json") },
|
||||
{ label: "SQL INSERT", action: () => exportData("sql") },
|
||||
{ label: "XLSX", action: () => exportDataXlsx() },
|
||||
],
|
||||
children,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4349,6 +4359,17 @@ function buildObjectSidebarMenu(context: SidebarMenuFactoryContext): boolean {
|
||||
const { node, items, deleteMenuLabel, deleteMenuAction, truncateMenuLabel, truncateMenuAction, emptyMenuLabel, emptyMenuAction } = context;
|
||||
// 6. Table / View / Materialized View
|
||||
if (node.type === "table" || node.type === "view" || node.type === "materialized_view") {
|
||||
if (currentDatabaseType() === "victoriametrics" && node.type === "table") {
|
||||
items.push({ label: t("contextMenu.copyName"), action: copyName, icon: Copy, shortcut: shortcutCopyName.value });
|
||||
items.push({ label: "", separator: true });
|
||||
items.push({ label: t("contextMenu.viewData"), action: openDataImmediately, icon: TableProperties });
|
||||
items.push({ label: t("contextMenu.openInNewDataTab"), action: openDataInNewTabImmediately, icon: CopyPlus });
|
||||
items.push({ label: t("contextMenu.newQuery"), action: newQuery, icon: TerminalSquare });
|
||||
items.push({ label: "", separator: true });
|
||||
items.push(exportDataSubmenu(false));
|
||||
items.push({ label: t("contextMenu.refreshChildren"), action: refresh, icon: RefreshCw, shortcut: shortcutRefresh });
|
||||
return true;
|
||||
}
|
||||
const destructiveActions: ContextMenuItem[] = [];
|
||||
items.push({ label: t("contextMenu.copyName"), action: copyName, icon: Copy, shortcut: shortcutCopyName.value });
|
||||
items.push({ label: "", separator: true });
|
||||
@@ -4568,6 +4589,7 @@ function buildObjectSidebarMenu(context: SidebarMenuFactoryContext): boolean {
|
||||
}
|
||||
|
||||
function treeTableClipboardMenuItems(node: TreeNode): ContextMenuItem[] {
|
||||
if (currentDatabaseType() === "victoriametrics") return [];
|
||||
const copyItem: ContextMenuItem = { label: t("contextMenu.copyTable"), action: copySelectedNames, icon: Copy };
|
||||
if (!node.connectionId || !node.database) return [copyItem];
|
||||
const state = tableClipboardMenuState(
|
||||
|
||||
@@ -18,7 +18,7 @@ const QUICK_OPEN_MAX_RESULTS = 200;
|
||||
const INITIAL_SQL_LIBRARY_LIMIT = 20;
|
||||
const INITIAL_SQL_FILE_LIMIT = 20;
|
||||
|
||||
const REMOTE_SEARCH_UNSUPPORTED_TYPES = new Set<ConnectionConfig["db_type"]>(["redis", "mongodb", "elasticsearch", "easysearch", "qdrant", "milvus", "weaviate", "chromadb", "neo4j", "influxdb", "etcd", "zookeeper", "mq", "nacos"]);
|
||||
const REMOTE_SEARCH_UNSUPPORTED_TYPES = new Set<ConnectionConfig["db_type"]>(["redis", "mongodb", "elasticsearch", "easysearch", "qdrant", "milvus", "weaviate", "chromadb", "neo4j", "influxdb", "victoriametrics", "etcd", "zookeeper", "mq", "nacos"]);
|
||||
|
||||
export interface QuickOpenItem {
|
||||
id: string;
|
||||
|
||||
@@ -258,6 +258,26 @@ export function useSidebarTreeExportRuntime(options: SidebarTreeExportRuntimeOpt
|
||||
await connectionStore.ensureConnected(connectionId);
|
||||
task = addExportTask(node.label, format, outputPath);
|
||||
const currentTask = task;
|
||||
const effectiveDbType = effectiveDatabaseTypeForConnection(config);
|
||||
if (effectiveDbType === "victoriametrics") {
|
||||
const result = await fetchTableDataForExport({
|
||||
databaseType: effectiveDbType,
|
||||
schema: node.schema,
|
||||
tableName: node.label,
|
||||
tableType: node.tableType,
|
||||
executePage: (sql) => api.executeQuery(connectionId, database, sql),
|
||||
});
|
||||
if (format === "csv") {
|
||||
await api.exportQueryResultCsv(outputPath, result.columns, result.rows);
|
||||
} else {
|
||||
await api.exportQueryResultXlsx(outputPath, node.label, result.columns, result.column_types ?? result.columns.map(() => ""), undefined, result.rows);
|
||||
}
|
||||
currentTask.status = "Done";
|
||||
currentTask.rowsExported = result.rows.length;
|
||||
currentTask.totalRows = result.rows.length;
|
||||
toast(t("grid.exported"));
|
||||
return;
|
||||
}
|
||||
const queryColumns = config.db_type === "neo4j" ? (await api.getColumns(connectionId, database, node.schema || database, node.label)).map((column) => column.name) : undefined;
|
||||
const rowLimit = settingsStore.editorSettings.exportRowLimitEnabled ? settingsStore.editorSettings.exportRowLimit : null;
|
||||
const request: api.TableExportRequest = {
|
||||
|
||||
@@ -366,7 +366,7 @@ export function useSqlExecution(deps: {
|
||||
export function supportsSqlTemplateParameters(connection: Pick<ConnectionConfig, "db_type"> | undefined, sql = ""): boolean {
|
||||
if (!connection) return false;
|
||||
if (connection.db_type === "elasticsearch" || connection.db_type === "easysearch") return !isElasticsearchRestRequestText(sql);
|
||||
return connection.db_type !== "redis" && connection.db_type !== "mongodb";
|
||||
return connection.db_type !== "redis" && connection.db_type !== "mongodb" && connection.db_type !== "victoriametrics";
|
||||
}
|
||||
|
||||
export function requiresDatabaseSelection(tab: QueryTab, connection: ConnectionConfig | undefined, _sql = ""): boolean {
|
||||
|
||||
@@ -243,6 +243,12 @@ export default {
|
||||
elasticsearchConnectivityCheckPath: "Connectivity Path",
|
||||
elasticsearchConnectivityCheckPathPlaceholder: "/ or /my-index/_search",
|
||||
version: "Version",
|
||||
victoriametricsApiPath: "API Path",
|
||||
victoriametricsApiPathHint: "Use /prometheus for single-node deployments, or /select/tenantID/prometheus for clusters.",
|
||||
victoriametricsLookback: "Metadata Lookback",
|
||||
victoriametricsLookbackHint: "Used for metric discovery and label columns, for example 1h or 7d. Metric previews use 1h and remain editable.",
|
||||
victoriametricsInvalidApiPath: "The VictoriaMetrics API path must be empty or start with /.",
|
||||
victoriametricsInvalidLookback: "Invalid VictoriaMetrics lookback. Use a value such as 15m, 1h, or 7d.",
|
||||
driverInstallHintPrefix: "Install the required driver from ",
|
||||
driverInstallHintSuffix: " in the top toolbar before connecting.",
|
||||
driverName: "Driver Name",
|
||||
@@ -1562,6 +1568,7 @@ export default {
|
||||
},
|
||||
sortUnsupported: "This SQL does not support full-result sorting. Try again with a single SELECT query.",
|
||||
truncatedHint: "Results were truncated after loading {count} rows. Use the footer pagination to browse loaded data; exporting the full result reruns the database query.",
|
||||
victoriaMetricsTruncatedHint: "Results exceeded the loading limit, so only the first {count} rows were loaded. Rows per page only paginates loaded data; shorten the time range or add filters to retrieve a complete result.",
|
||||
},
|
||||
exportProgress: {
|
||||
streamingUnsupported: "Streaming export is unsupported for this query. Simplify it or use a supported driver.",
|
||||
@@ -2719,6 +2726,7 @@ export default {
|
||||
name: "Name",
|
||||
type: "Type",
|
||||
rows: "Rows",
|
||||
series: "Series",
|
||||
size: "Size",
|
||||
statisticsHint: "Estimated from database statistics; no COUNT(*) is executed.",
|
||||
createdAt: "Created",
|
||||
|
||||
@@ -239,6 +239,12 @@ export default withEnglishFallback({
|
||||
oracleTnsAdminRequired: "El modo Oracle TNS requiere el directorio TNS_ADMIN que contiene tnsnames.ora.",
|
||||
oracleTnsTransportUnsupported: "Oracle TNS no se puede combinar con túneles SSH, proxy o HTTP. Elimine la capa de transporte o use Servicio/SID.",
|
||||
version: "Versión",
|
||||
victoriametricsApiPath: "Ruta de la API",
|
||||
victoriametricsApiPathHint: "Use /prometheus para implementaciones de nodo único o /select/tenantID/prometheus para clústeres.",
|
||||
victoriametricsLookback: "Intervalo retrospectivo de metadatos",
|
||||
victoriametricsLookbackHint: "Se usa para descubrir métricas y columnas de etiquetas, por ejemplo, 1h o 7d. Las vistas previas de métricas usan 1h y se pueden editar.",
|
||||
victoriametricsInvalidApiPath: "La ruta de la API de VictoriaMetrics debe estar vacía o comenzar por /.",
|
||||
victoriametricsInvalidLookback: "El intervalo retrospectivo de VictoriaMetrics no es válido. Use un valor como 15m, 1h o 7d.",
|
||||
driverInstallHintPrefix: "Instala el driver requerido desde ",
|
||||
driverInstallHintSuffix: " en la barra superior antes de conectar.",
|
||||
driverName: "Nombre del driver",
|
||||
@@ -1440,6 +1446,7 @@ export default withEnglishFallback({
|
||||
},
|
||||
sortUnsupported: "Este SQL no admite ordenamiento del resultado completo. Intenta con una consulta SELECT simple.",
|
||||
truncatedHint: "Los resultados se truncaron después de cargar {count} filas. Usa la paginación inferior para explorar los datos cargados; al exportar el resultado completo se vuelve a consultar la base de datos.",
|
||||
victoriaMetricsTruncatedHint: "Los resultados superaron el límite de carga, por lo que solo se cargaron las primeras {count} filas. Las filas por página solo paginan los datos cargados; reduce el intervalo de tiempo o añade filtros para obtener el resultado completo.",
|
||||
filterBuilderSearchColumns: "Buscar columnas...",
|
||||
filterBuilderNoMatchingColumns: "Sin columnas coincidentes",
|
||||
copyExtractorSmart: "Copia inteligente",
|
||||
@@ -2571,6 +2578,7 @@ export default withEnglishFallback({
|
||||
name: "Nombre",
|
||||
type: "Tipo",
|
||||
rows: "Filas",
|
||||
series: "Series",
|
||||
size: "Tamaño",
|
||||
statisticsHint: "Estimado desde estadísticas de la base de datos; no se ejecuta COUNT(*).",
|
||||
createdAt: "Creado",
|
||||
|
||||
@@ -238,6 +238,12 @@ export default withEnglishFallback({
|
||||
oracleTnsAdminRequired: "La modalità Oracle TNS richiede la directory TNS_ADMIN contenente tnsnames.ora.",
|
||||
oracleTnsTransportUnsupported: "Oracle TNS non può usare tunnel SSH, proxy o HTTP. Rimuovi il livello di trasporto o usa Servizio/SID.",
|
||||
version: "Versione",
|
||||
victoriametricsApiPath: "Percorso API",
|
||||
victoriametricsApiPathHint: "Usa /prometheus per le installazioni a nodo singolo oppure /select/tenantID/prometheus per i cluster.",
|
||||
victoriametricsLookback: "Intervallo metadati",
|
||||
victoriametricsLookbackHint: "Usato per rilevare metriche e colonne delle etichette, ad esempio 1h o 7d. Le anteprime delle metriche usano 1h e restano modificabili.",
|
||||
victoriametricsInvalidApiPath: "Il percorso API di VictoriaMetrics deve essere vuoto o iniziare con /.",
|
||||
victoriametricsInvalidLookback: "Intervallo VictoriaMetrics non valido. Usa un valore come 15m, 1h o 7d.",
|
||||
driverInstallHintPrefix: "Installa il driver richiesto da ",
|
||||
driverInstallHintSuffix: " nella barra degli strumenti superiore prima di connetterti.",
|
||||
driverName: "Nome Driver",
|
||||
@@ -1438,6 +1444,7 @@ export default withEnglishFallback({
|
||||
},
|
||||
sortUnsupported: "Questo SQL non supporta l'ordinamento sull'intero risultato. Riprova con una query SELECT semplice.",
|
||||
truncatedHint: "I risultati sono stati troncati dopo il caricamento di {count} righe. Usa la paginazione in basso per consultare i dati caricati; l'esportazione del risultato completo riesegue la query sul database.",
|
||||
victoriaMetricsTruncatedHint: "I risultati hanno superato il limite di caricamento, quindi sono state caricate solo le prime {count} righe. Le righe per pagina suddividono solo i dati caricati; riduci l'intervallo di tempo o aggiungi filtri per ottenere il risultato completo.",
|
||||
filterBuilderSearchColumns: "Cerca campi...",
|
||||
filterBuilderNoMatchingColumns: "Nessun campo corrispondente",
|
||||
copyExtractorSmart: "Copia intelligente",
|
||||
@@ -2569,6 +2576,7 @@ export default withEnglishFallback({
|
||||
name: "Nome",
|
||||
type: "Tipo",
|
||||
rows: "Righe",
|
||||
series: "Serie",
|
||||
size: "Dimensione",
|
||||
statisticsHint: "Stimato dalle statistiche del database; COUNT(*) non viene eseguito.",
|
||||
createdAt: "Creato",
|
||||
|
||||
@@ -238,6 +238,12 @@ export default withEnglishFallback({
|
||||
oracleTnsAdminRequired: "Oracle TNS モードでは tnsnames.ora を含む TNS_ADMIN ディレクトリが必要です。",
|
||||
oracleTnsTransportUnsupported: "Oracle TNS 接続は SSH、プロキシ、HTTP トンネルと併用できません。トランスポート層を削除するか、サービス名/SID モードを使用してください。",
|
||||
version: "バージョン",
|
||||
victoriametricsApiPath: "APIパス",
|
||||
victoriametricsApiPathHint: "単一ノード構成では /prometheus、クラスター構成では /select/tenantID/prometheus を使用します。",
|
||||
victoriametricsLookback: "メタデータ参照範囲",
|
||||
victoriametricsLookbackHint: "メトリクスとラベル列の検出に使用します(例: 1h、7d)。メトリクスのプレビューには 1h を使用し、クエリは編集できます。",
|
||||
victoriametricsInvalidApiPath: "VictoriaMetrics の API パスは空にするか、/ で始める必要があります。",
|
||||
victoriametricsInvalidLookback: "VictoriaMetrics の参照範囲が無効です。15m、1h、7d などの値を使用してください。",
|
||||
driverInstallHintPrefix: "接続前に上部ツールバーの",
|
||||
driverInstallHintSuffix: "から必要なドライバーをインストールしてください。",
|
||||
driverName: "ドライバー名",
|
||||
@@ -1454,6 +1460,7 @@ export default withEnglishFallback({
|
||||
},
|
||||
sortUnsupported: "このSQLは完全な結果の並び替えをサポートしていません。単一のSELECTクエリで再試行してください。",
|
||||
truncatedHint: "結果は{count}行を読み込んだ時点で切り詰められました。フッターのページネーションで読み込み済みデータを参照できます。完全な結果のエクスポート時はデータベースへ再問い合わせします。",
|
||||
victoriaMetricsTruncatedHint: "結果が読み込み上限を超えたため、先頭の{count}行のみ読み込みました。1ページあたりの行数は読み込み済みデータだけをページ分割します。完全な結果を取得するには、時間範囲を短くするかフィルターを追加してください。",
|
||||
loadingMore: "さらにデータを読み込み中...",
|
||||
allLoaded: "すべて読み込み済み",
|
||||
previewSqlEmpty: "プレビューする保留中のSQL変更はありません",
|
||||
@@ -2630,6 +2637,7 @@ export default withEnglishFallback({
|
||||
name: "名前",
|
||||
type: "タイプ",
|
||||
rows: "行数",
|
||||
series: "系列数",
|
||||
size: "サイズ",
|
||||
statisticsHint: "データベース統計からの推定値です。COUNT(*) は実行しません。",
|
||||
createdAt: "作成日時",
|
||||
|
||||
@@ -239,6 +239,12 @@ export default withEnglishFallback({
|
||||
elasticsearchConnectivityCheckPath: "연결 확인 경로",
|
||||
elasticsearchConnectivityCheckPathPlaceholder: "/ 또는 /my-index/_search",
|
||||
version: "버전",
|
||||
victoriametricsApiPath: "API 경로",
|
||||
victoriametricsApiPathHint: "단일 노드 배포에는 /prometheus를, 클러스터에는 /select/tenantID/prometheus를 사용하세요.",
|
||||
victoriametricsLookback: "메타데이터 조회 범위",
|
||||
victoriametricsLookbackHint: "메트릭 및 레이블 열 검색에 사용합니다(예: 1h 또는 7d). 메트릭 미리보기는 1h를 사용하며 쿼리를 편집할 수 있습니다.",
|
||||
victoriametricsInvalidApiPath: "VictoriaMetrics API 경로는 비워 두거나 /로 시작해야 합니다.",
|
||||
victoriametricsInvalidLookback: "VictoriaMetrics 조회 범위가 올바르지 않습니다. 15m, 1h 또는 7d와 같은 값을 사용하세요.",
|
||||
driverInstallHintPrefix: "연결 전에 상단 도구 모음의 ",
|
||||
driverInstallHintSuffix: "에서 필수 드라이버를 설치하세요.",
|
||||
driverName: "드라이버 이름",
|
||||
@@ -1457,6 +1463,7 @@ export default withEnglishFallback({
|
||||
},
|
||||
sortUnsupported: "이 SQL은 전체 결과 정렬을 지원하지 않습니다. 단일 SELECT 쿼리로 다시 시도하세요.",
|
||||
truncatedHint: "{count}행 로드 후 결과가 잘렸습니다. 로드된 데이터를 탐색하려면 푸터 페이지 매김을 사용하세요. 전체 결과를 내보내면 데이터베이스 쿼리를 다시 실행합니다.",
|
||||
victoriaMetricsTruncatedHint: "결과가 로드 한도를 초과하여 처음 {count}행만 로드했습니다. 페이지당 행 수는 로드된 데이터만 나눠 표시합니다. 전체 결과를 가져오려면 시간 범위를 줄이거나 필터를 추가하세요.",
|
||||
},
|
||||
exportProgress: {
|
||||
streamingUnsupported: "이 쿼리는 스트리밍 내보내기를 지원하지 않습니다. 쿼리를 단순화하거나 지원되는 드라이버를 사용하세요.",
|
||||
@@ -2600,6 +2607,7 @@ export default withEnglishFallback({
|
||||
name: "이름",
|
||||
type: "유형",
|
||||
rows: "행",
|
||||
series: "시계열 수",
|
||||
size: "크기",
|
||||
statisticsHint: "데이터베이스 통계에서 추정한 값이며, COUNT(*)는 실행되지 않습니다.",
|
||||
createdAt: "생성일",
|
||||
|
||||
@@ -239,6 +239,12 @@ export default withEnglishFallback({
|
||||
oracleTnsAdminRequired: "O modo Oracle TNS requer o diretório TNS_ADMIN que contém tnsnames.ora.",
|
||||
oracleTnsTransportUnsupported: "Oracle TNS não pode usar túneis SSH, proxy ou HTTP. Remova a camada de transporte ou use Serviço/SID.",
|
||||
version: "Versão",
|
||||
victoriametricsApiPath: "Caminho da API",
|
||||
victoriametricsApiPathHint: "Use /prometheus para implantações de nó único ou /select/tenantID/prometheus para clusters.",
|
||||
victoriametricsLookback: "Intervalo de consulta de metadados",
|
||||
victoriametricsLookbackHint: "Usado para descobrir métricas e colunas de rótulos, por exemplo, 1h ou 7d. As visualizações de métricas usam 1h e podem ser editadas.",
|
||||
victoriametricsInvalidApiPath: "O caminho da API do VictoriaMetrics deve estar vazio ou começar com /.",
|
||||
victoriametricsInvalidLookback: "O intervalo de consulta do VictoriaMetrics é inválido. Use um valor como 15m, 1h ou 7d.",
|
||||
driverInstallHintPrefix: "Instale o driver necessário em ",
|
||||
driverInstallHintSuffix: " na barra de ferramentas superior antes de conectar.",
|
||||
driverName: "Nome do Driver",
|
||||
@@ -1440,6 +1446,7 @@ export default withEnglishFallback({
|
||||
},
|
||||
sortUnsupported: "Este SQL não suporta a ordenação do resultado completo. Tente novamente com uma única consulta SELECT.",
|
||||
truncatedHint: "Os resultados foram truncados após carregar {count} linhas. Use a paginação no rodapé para navegar pelos dados carregados; exportar o resultado completo executa novamente a consulta no banco de dados.",
|
||||
victoriaMetricsTruncatedHint: "Os resultados excederam o limite de carregamento, então apenas as primeiras {count} linhas foram carregadas. As linhas por página paginam somente os dados carregados; reduza o intervalo de tempo ou adicione filtros para obter o resultado completo.",
|
||||
filterBuilderSearchColumns: "Pesquisar campos...",
|
||||
filterBuilderNoMatchingColumns: "Nenhum campo correspondente",
|
||||
copyExtractorSmart: "Cópia inteligente",
|
||||
@@ -2571,6 +2578,7 @@ export default withEnglishFallback({
|
||||
name: "Nome",
|
||||
type: "Tipo",
|
||||
rows: "Linhas",
|
||||
series: "Séries",
|
||||
size: "Tamanho",
|
||||
statisticsHint: "Estimado pelas estatísticas do banco de dados; COUNT(*) não é executado.",
|
||||
createdAt: "Criado em",
|
||||
|
||||
@@ -245,6 +245,12 @@ export default withEnglishFallback({
|
||||
elasticsearchConnectivityCheckPath: "连通性检查路径",
|
||||
elasticsearchConnectivityCheckPathPlaceholder: "/ 或 /my-index/_search",
|
||||
version: "版本",
|
||||
victoriametricsApiPath: "API 路径",
|
||||
victoriametricsApiPathHint: "单机版通常为 /prometheus;集群版可填写 /select/租户ID/prometheus。",
|
||||
victoriametricsLookback: "元数据回看范围",
|
||||
victoriametricsLookbackHint: "用于指标列表和标签列,例如 1h、7d。双击指标默认预览 1h,并可直接修改查询。",
|
||||
victoriametricsInvalidApiPath: "VictoriaMetrics API 路径必须为空或以 / 开头。",
|
||||
victoriametricsInvalidLookback: "VictoriaMetrics 回看范围格式无效,请使用 15m、1h、7d 等格式。",
|
||||
driverInstallHintPrefix: "需要在顶部导航栏「",
|
||||
driverInstallHintSuffix: "」中安装对应的驱动才能连接。",
|
||||
driverName: "驱动名称",
|
||||
@@ -1563,6 +1569,7 @@ export default withEnglishFallback({
|
||||
},
|
||||
sortUnsupported: "当前 SQL 不支持全量排序,请改为单条 SELECT 查询后再尝试。",
|
||||
truncatedHint: "结果已截断,已加载前 {count} 行。可通过底部分页浏览已加载数据;导出完整结果时会重新查询数据库。",
|
||||
victoriaMetricsTruncatedHint: "结果超过加载上限,仅加载前 {count} 行。右下角每页行数只调整已加载数据的分页显示;请缩短查询时间范围或增加筛选条件以获取完整结果。",
|
||||
},
|
||||
exportProgress: {
|
||||
streamingUnsupported: "当前查询暂不支持流式导出,请简化查询或使用受支持的驱动。",
|
||||
@@ -2719,6 +2726,7 @@ export default withEnglishFallback({
|
||||
name: "名称",
|
||||
type: "类型",
|
||||
rows: "行数",
|
||||
series: "序列数",
|
||||
size: "大小",
|
||||
statisticsHint: "来自数据库统计信息的估算值,不执行 COUNT(*)。",
|
||||
createdAt: "创建时间",
|
||||
|
||||
@@ -239,6 +239,12 @@ export default withEnglishFallback({
|
||||
oracleTnsAdminRequired: "Oracle TNS 模式需要填寫包含 tnsnames.ora 的 TNS_ADMIN 目錄。",
|
||||
oracleTnsTransportUnsupported: "Oracle TNS 連線不能與 SSH、代理或 HTTP Tunnel 組合使用。請移除傳輸層,或改用服務名稱/SID 模式。",
|
||||
version: "版本",
|
||||
victoriametricsApiPath: "API 路徑",
|
||||
victoriametricsApiPathHint: "單機版通常使用 /prometheus;叢集版可填寫 /select/tenantID/prometheus。",
|
||||
victoriametricsLookback: "中繼資料回看範圍",
|
||||
victoriametricsLookbackHint: "用於探索指標與標籤欄位,例如 1h 或 7d。指標預覽使用 1h,且查詢內容可直接修改。",
|
||||
victoriametricsInvalidApiPath: "VictoriaMetrics API 路徑必須為空或以 / 開頭。",
|
||||
victoriametricsInvalidLookback: "VictoriaMetrics 回看範圍格式無效,請使用 15m、1h 或 7d 等格式。",
|
||||
driverInstallHintPrefix: "需要在上方導覽列「",
|
||||
driverInstallHintSuffix: "」中安裝對應的驅動程式才能連線。",
|
||||
driverName: "驅動程式名稱",
|
||||
@@ -1439,6 +1445,7 @@ export default withEnglishFallback({
|
||||
},
|
||||
sortUnsupported: "目前 SQL 不支援完整排序,請改為單條 SELECT 查詢後再嘗試。",
|
||||
truncatedHint: "結果已截斷,已載入前 {count} 筆。可透過底部分頁瀏覽已載入資料;匯出完整結果時會重新查詢資料庫。",
|
||||
victoriaMetricsTruncatedHint: "結果超過載入上限,僅載入前 {count} 筆。右下角每頁筆數只調整已載入資料的分頁顯示;請縮短查詢時間範圍或增加篩選條件以取得完整結果。",
|
||||
filterBuilderSearchColumns: "搜尋欄位...",
|
||||
filterBuilderNoMatchingColumns: "沒有匹配的欄位",
|
||||
copyExtractorSmart: "智慧複製",
|
||||
@@ -2440,6 +2447,7 @@ export default withEnglishFallback({
|
||||
name: "名稱",
|
||||
type: "類型",
|
||||
rows: "列數",
|
||||
series: "序列數",
|
||||
size: "大小",
|
||||
statisticsHint: "來自資料庫統計資訊的估算值,不執行 COUNT(*)。",
|
||||
createdAt: "建立時間",
|
||||
|
||||
+2
-1
@@ -2,10 +2,11 @@ import { readFileSync } from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const dialogSource = readFileSync(new URL("../../../components/connection/ConnectionDialog.vue", import.meta.url), "utf8");
|
||||
const tlsCapableDatabaseTypesSource = dialogSource.match(/const tlsCapableDatabaseTypes = new Set<DatabaseType>\(\[([\s\S]*?)\]\);/)?.[1] ?? "";
|
||||
|
||||
describe("Dameng SSL connection form", () => {
|
||||
it("shows Dameng in the TLS tab with certificate directory and password fields", () => {
|
||||
expect(dialogSource).toContain('"questdb", "dameng", "redis"');
|
||||
expect(tlsCapableDatabaseTypesSource).toContain('"dameng"');
|
||||
expect(dialogSource).toContain("<template v-if=\"form.db_type === 'dameng'\">");
|
||||
expect(dialogSource).toContain('v-model="damengSslFilesPath"');
|
||||
expect(dialogSource).toContain('v-model="damengSslKeystorePassword"');
|
||||
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { applyParsedConnectionUrl, parseConnectionUrl } from "@/lib/connection/connectionUrl";
|
||||
import type { ConnectionConfig } from "@/types/database";
|
||||
|
||||
describe("VictoriaMetrics connection URL", () => {
|
||||
it("parses single-node HTTP URLs with Basic Auth", () => {
|
||||
expect(parseConnectionUrl("http://vm_user:secret@vm.example.com:8428/prometheus", "victoriametrics")).toMatchObject({
|
||||
dbType: "victoriametrics",
|
||||
host: "vm.example.com",
|
||||
port: 8428,
|
||||
username: "vm_user",
|
||||
password: "secret",
|
||||
database: "metrics",
|
||||
apiPath: "/prometheus",
|
||||
ssl: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves cluster tenant API paths in external config", () => {
|
||||
const parsed = parseConnectionUrl("https://vm.example.com/select/42/prometheus", "victoriametrics");
|
||||
const config = applyParsedConnectionUrl(
|
||||
{
|
||||
db_type: "victoriametrics",
|
||||
name: "VictoriaMetrics",
|
||||
host: "",
|
||||
port: 8428,
|
||||
username: "",
|
||||
password: "",
|
||||
ssl: false,
|
||||
external_config: { lookback: "7d" },
|
||||
} as Omit<ConnectionConfig, "id">,
|
||||
parsed,
|
||||
);
|
||||
|
||||
expect(config.ssl).toBe(true);
|
||||
expect(config.external_config).toEqual({ lookback: "7d", apiPath: "/select/42/prometheus" });
|
||||
});
|
||||
});
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { filterModeIsSupportedForDatabase } from "@/lib/dataGrid/dataGridColumnFilter";
|
||||
import type { DataGridContextFilterMode } from "@/lib/dataGrid/dataGridSql";
|
||||
|
||||
describe("VictoriaMetrics data grid filters", () => {
|
||||
it("does not expose SQL filter modes for MetricsQL results", () => {
|
||||
const modes: DataGridContextFilterMode[] = ["equals", "not-equals", "is-null", "is-not-null", "like", "not-like", "less-than", "greater-than", "in", "not-in", "between", "not-between"];
|
||||
|
||||
expect(modes.every((mode) => !filterModeIsSupportedForDatabase(mode, "victoriametrics"))).toBe(true);
|
||||
expect(filterModeIsSupportedForDatabase("equals", "postgres")).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -169,6 +169,10 @@ describe("resolveNewQueryTable", () => {
|
||||
});
|
||||
|
||||
describe("buildSelectAllSql", () => {
|
||||
it("builds a MetricsQL range query for VictoriaMetrics metrics", () => {
|
||||
expect(buildSelectAllSql("victoriametrics", { tableName: "flag" })).toBe('{__name__="flag"}[1h]');
|
||||
});
|
||||
|
||||
it("quotes a MySQL table with backticks", () => {
|
||||
expect(buildSelectAllSql("mysql", { tableName: "users" })).toBe("SELECT * FROM `users`");
|
||||
});
|
||||
|
||||
@@ -1276,6 +1276,7 @@ describe("supportsExecutionTargetPicker", () => {
|
||||
expect(supportsExecutionTargetPicker("zookeeper")).toBe(false);
|
||||
expect(supportsExecutionTargetPicker("mq")).toBe(false);
|
||||
expect(supportsExecutionTargetPicker("neo4j")).toBe(false);
|
||||
expect(supportsExecutionTargetPicker("victoriametrics")).toBe(false);
|
||||
expect(supportsExecutionTargetPicker(undefined)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -236,6 +236,9 @@ export function connectionUrlPlaceholder(dbType: DatabaseType): string {
|
||||
case "influxdb":
|
||||
return "influxdb://user:password@host:port/database";
|
||||
|
||||
case "victoriametrics":
|
||||
return "http://user:password@host:port/prometheus";
|
||||
|
||||
case "jdbc":
|
||||
return "jdbc:mysql://host:3306/database";
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ export interface ParsedConnectionUrl {
|
||||
oracleConnectionType?: "service_name" | "sid";
|
||||
useMongoUrl?: boolean;
|
||||
portExplicit?: boolean;
|
||||
apiPath?: string;
|
||||
}
|
||||
|
||||
export type ConnectionProfile = {
|
||||
@@ -68,6 +69,7 @@ const SCHEME_PROFILES: Record<string, ConnectionProfile> = {
|
||||
xugu: { type: "xugu", profile: "xugu", label: "XuguDB", defaultPort: 5138 },
|
||||
iotdb: { type: "iotdb", profile: "iotdb", label: "Apache IoTDB", defaultPort: 6667 },
|
||||
iris: { type: "iris", profile: "iris", label: "IRIS", defaultPort: 1972 },
|
||||
victoriametrics: { type: "victoriametrics", profile: "victoriametrics", label: "VictoriaMetrics", defaultPort: 8428 },
|
||||
};
|
||||
|
||||
const HTTP_SELECTED_PROFILES: Record<string, ConnectionProfile> = {
|
||||
@@ -78,6 +80,7 @@ const HTTP_SELECTED_PROFILES: Record<string, ConnectionProfile> = {
|
||||
milvus: SCHEME_PROFILES.milvus,
|
||||
weaviate: SCHEME_PROFILES.weaviate,
|
||||
chromadb: SCHEME_PROFILES.chromadb,
|
||||
victoriametrics: SCHEME_PROFILES.victoriametrics,
|
||||
};
|
||||
|
||||
function decodeUrlPart(value: string): string {
|
||||
@@ -640,9 +643,10 @@ export function parseConnectionUrl(value: string, preferredProfile?: string): Pa
|
||||
...(profile.type === "sqlserver" && parsed.port ? { portExplicit: true } : {}),
|
||||
username: mysqlCredentials?.username ?? decodeUrlPart(parsed.username),
|
||||
password: mysqlCredentials?.password ?? decodeUrlPart(parsed.password),
|
||||
database: databaseFromPath(parsed.pathname),
|
||||
database: profile.type === "victoriametrics" ? "metrics" : databaseFromPath(parsed.pathname),
|
||||
urlParams: effectiveUrlParams,
|
||||
ssl: scheme === "rediss" || scheme === "https" || urlParamsRequireTls(profile.type, effectiveUrlParams) || (profile.type === "mysql" && isTidbCloudHost(parsed.hostname)),
|
||||
...(profile.type === "victoriametrics" ? { apiPath: parsed.pathname.replace(/\/+$/, "") } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -678,7 +682,12 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return !!value && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function sqlServerExternalConfig(existing: unknown, parsed: ParsedConnectionUrl): unknown {
|
||||
function parsedExternalConfig(existing: unknown, parsed: ParsedConnectionUrl): unknown {
|
||||
if (parsed.dbType === "victoriametrics") {
|
||||
const next = isRecord(existing) ? { ...existing } : {};
|
||||
next.apiPath = parsed.apiPath || "/prometheus";
|
||||
return next;
|
||||
}
|
||||
if (parsed.dbType !== "sqlserver") return existing;
|
||||
|
||||
const next = isRecord(existing) ? { ...existing } : {};
|
||||
@@ -707,6 +716,6 @@ export function applyParsedConnectionUrl(config: Omit<ConnectionConfig, "id">, p
|
||||
ssl: parsed.ssl,
|
||||
connection_string: parsed.connectionString,
|
||||
oracle_connection_type: parsed.oracleConnectionType,
|
||||
external_config: sqlServerExternalConfig(config.external_config, parsed),
|
||||
external_config: parsedExternalConfig(config.external_config, parsed),
|
||||
};
|
||||
}
|
||||
@@ -184,6 +184,7 @@ export function filterModeUsesRange(mode: DataGridContextFilterMode): boolean {
|
||||
}
|
||||
|
||||
export function filterModeIsSupportedForDatabase(mode: DataGridContextFilterMode, databaseType?: DatabaseType): boolean {
|
||||
if (databaseType === "victoriametrics") return false;
|
||||
if (!filterModeUsesList(mode) && !filterModeUsesRange(mode)) return true;
|
||||
// These targets do not support all four new SQL predicates reliably.
|
||||
return databaseType !== "cassandra" && databaseType !== "influxdb" && databaseType !== "jdbc";
|
||||
|
||||
@@ -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", "qdrant", "milvus", "weaviate", "chromadb", "etcd", "zookeeper", "mq", "nacos"]);
|
||||
const NON_SQL_DATABASE_TYPES = new Set<DatabaseType>(["redis", "mongodb", "elasticsearch", "easysearch", "qdrant", "milvus", "weaviate", "chromadb", "etcd", "zookeeper", "mq", "nacos", "victoriametrics"]);
|
||||
// quoteTableIdentifier 对 jdbc/iotdb 原样返回(无引用字符可用)。
|
||||
const UNQUOTABLE_DATABASE_TYPES = new Set<DatabaseType>(["jdbc", "iotdb"]);
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { DatabaseType } from "@/types/database";
|
||||
|
||||
export interface CanGoNextDataGridPageOptions {
|
||||
hasMore?: boolean;
|
||||
rowCount: number;
|
||||
@@ -31,6 +33,10 @@ export interface CanFetchNextDataGridSegmentOptions {
|
||||
|
||||
export type DataGridInexactTotalRowCountMode = "at-least" | "estimated";
|
||||
|
||||
export function dataGridTruncationHintKey(databaseType?: DatabaseType): "grid.truncatedHint" | "grid.victoriaMetricsTruncatedHint" {
|
||||
return databaseType === "victoriametrics" ? "grid.victoriaMetricsTruncatedHint" : "grid.truncatedHint";
|
||||
}
|
||||
|
||||
export function dataGridTotalRowCountLabelKey(totalRowCountIsExact: boolean, inexactMode: DataGridInexactTotalRowCountMode): "grid.totalRowCount" | "grid.totalRowCountAtLeast" | "grid.totalRowCountEstimated" {
|
||||
if (totalRowCountIsExact) return "grid.totalRowCount";
|
||||
return inexactMode === "estimated" ? "grid.totalRowCountEstimated" : "grid.totalRowCountAtLeast";
|
||||
|
||||
@@ -43,7 +43,7 @@ export const SCHEMA_AWARE_TYPES = new Set<DatabaseType>([
|
||||
// query another database through a three-part name on the same connection.
|
||||
export const DATABASE_SCHEMA_QUALIFIED_TYPES = new Set<DatabaseType>(["sqlserver", "trino", "prestosql"]);
|
||||
|
||||
export const SINGLE_DATABASE_TYPES = new Set<DatabaseType>(["oracle", "dameng", "firebird", "oceanbase-oracle", "access", "questdb"]);
|
||||
export const SINGLE_DATABASE_TYPES = new Set<DatabaseType>(["oracle", "dameng", "firebird", "oceanbase-oracle", "access", "questdb", "victoriametrics"]);
|
||||
|
||||
export const CLEARABLE_QUERY_SCHEMA_TYPES = new Set<DatabaseType>(["oracle", "dameng", "gaussdb", "oceanbase-oracle"]);
|
||||
|
||||
|
||||
@@ -140,7 +140,7 @@ export function supportsObjectBrowserTreeNode(dbType: DatabaseType | undefined,
|
||||
}
|
||||
|
||||
export function supportsTableTruncate(dbType?: DatabaseType): boolean {
|
||||
return !!dbType && dbType !== "sqlite" && dbType !== "rqlite" && dbType !== "turso" && dbType !== "cloudflare-d1" && dbType !== "duckdb" && dbType !== "influxdb" && dbType !== "manticoresearch";
|
||||
return !!dbType && dbType !== "sqlite" && dbType !== "rqlite" && dbType !== "turso" && dbType !== "cloudflare-d1" && dbType !== "duckdb" && dbType !== "influxdb" && dbType !== "victoriametrics" && dbType !== "manticoresearch";
|
||||
}
|
||||
|
||||
export function usesPostgresLikeStructureCopy(dbType?: DatabaseType): boolean {
|
||||
|
||||
@@ -81,6 +81,7 @@ export const DATABASE_NAMESPACE_CREATION_MATRIX = {
|
||||
zookeeper: { deferred: "key-value namespaces are not databases" },
|
||||
iris: { database: "schema" },
|
||||
influxdb: { connection: "database" },
|
||||
victoriametrics: { deferred: "metric namespaces are managed by VictoriaMetrics deployment configuration" },
|
||||
jdbc: { deferred: "generic JDBC does not expose a reliable dialect-specific create target" },
|
||||
mq: { deferred: "message queue namespaces are handled by MQ admin panels" },
|
||||
nacos: { deferred: "Nacos namespace creation already uses the Nacos admin flow" },
|
||||
|
||||
@@ -62,6 +62,7 @@ const DATABASE_TYPE_OBJECTS = new Map<DatabaseType, SidebarObjectKind[]>([
|
||||
["neo4j", TABLE_VIEW_OBJECTS],
|
||||
// others
|
||||
["influxdb", ["TABLE"]],
|
||||
["victoriametrics", ["TABLE"]],
|
||||
["hbase", ["TABLE"]],
|
||||
["questdb", ["TABLE", "VIEW", "MATERIALIZED_VIEW"]],
|
||||
["manticoresearch", ["TABLE", "FUNCTION"]],
|
||||
|
||||
@@ -81,6 +81,7 @@ export const DATABASE_PROPERTY_EDITING_MATRIX = {
|
||||
zookeeper: { deferred: "key-value namespaces are not databases" },
|
||||
iris: { deferred: "schema properties need product-specific handling" },
|
||||
influxdb: { deferred: "database retention policies need a dedicated workflow" },
|
||||
victoriametrics: { deferred: "metric and retention settings are managed by VictoriaMetrics deployment configuration" },
|
||||
jdbc: { deferred: "generic JDBC does not expose reliable dialect-specific properties" },
|
||||
mq: { deferred: "message queue namespaces are handled by MQ admin panels" },
|
||||
nacos: { deferred: "Nacos namespace editing already uses the Nacos admin flow" },
|
||||
|
||||
@@ -176,6 +176,18 @@ const DATABASE_CAPABILITY_OVERRIDES: Partial<Record<DatabaseType, Partial<Databa
|
||||
readonly: true,
|
||||
},
|
||||
},
|
||||
victoriametrics: {
|
||||
tableData: {
|
||||
insert: false,
|
||||
updateRequiresPrimaryKey: false,
|
||||
deleteRequiresPrimaryKey: true,
|
||||
keylessRowPredicate: false,
|
||||
requiresTransactionalTableForExistingRows: false,
|
||||
existingRowsReadonly: true,
|
||||
transaction: false,
|
||||
readonly: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
function defaultTableDataCapability(dbType?: DatabaseType): TableDataCapability {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { resolveDefaultDatabase } from "@/lib/database/defaultDatabase";
|
||||
import { normalizeSqliteNamespace } from "@/lib/database/sqliteNamespace";
|
||||
import { qualifiedTableName } from "@/lib/table/tableSelectSql";
|
||||
import { metricRangeQuery, qualifiedTableName } from "@/lib/table/tableSelectSql";
|
||||
import type { ConnectionConfig, DatabaseType, QueryTab, TreeNode } from "@/types/database";
|
||||
|
||||
export interface NewQueryTarget {
|
||||
@@ -145,6 +145,7 @@ export function resolveNewQueryTable(input: ResolveNewQueryTableInput): NewQuery
|
||||
* by the table-data view.
|
||||
*/
|
||||
export function buildSelectAllSql(databaseType: DatabaseType | undefined, table: Pick<NewQueryTable, "schema" | "catalog" | "tableName"> & Partial<Pick<NewQueryTable, "database">>): string {
|
||||
if (databaseType === "victoriametrics") return metricRangeQuery(table.tableName);
|
||||
const ref = qualifiedTableName({ databaseType, database: table.database, schema: table.schema, catalog: table.catalog, tableName: table.tableName });
|
||||
return `SELECT * FROM ${ref}`;
|
||||
}
|
||||
|
||||
@@ -22,7 +22,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", "qdrant", "milvus", "weaviate", "chromadb", "etcd", "zookeeper", "mq", "neo4j"]);
|
||||
const NON_SQL_EXECUTION_TARGET_TYPES: ReadonlySet<DatabaseType> = new Set(["mongodb", "elasticsearch", "easysearch", "qdrant", "milvus", "weaviate", "chromadb", "etcd", "zookeeper", "mq", "neo4j", "victoriametrics"]);
|
||||
|
||||
export function supportsExecutionTargetPicker(databaseType?: DatabaseType): boolean {
|
||||
return !!databaseType && (databaseType === "redis" || isElasticsearchCompatibleDatabaseType(databaseType) || !NON_SQL_EXECUTION_TARGET_TYPES.has(databaseType));
|
||||
|
||||
@@ -56,7 +56,7 @@ export function tableClipboardMenuState(entries: TableClipboardTableContext[], t
|
||||
}
|
||||
|
||||
export function supportsWholeRowTableDataCopy(databaseType: DatabaseType | undefined): boolean {
|
||||
return !!databaseType;
|
||||
return !!databaseType && databaseType !== "victoriametrics";
|
||||
}
|
||||
|
||||
export function defaultPasteTableMode(databaseType: DatabaseType | undefined): PasteTableMode {
|
||||
|
||||
@@ -17,6 +17,18 @@ export interface FetchTableDataForExportOptions {
|
||||
|
||||
export async function fetchTableDataForExport(options: FetchTableDataForExportOptions): Promise<QueryResult> {
|
||||
const pageSize = Math.max(1, options.pageSize ?? TABLE_DATA_EXPORT_PAGE_SIZE);
|
||||
if (options.databaseType === "victoriametrics") {
|
||||
const query = await (options.buildPageSql ?? buildTableSelectSql)({
|
||||
databaseType: options.databaseType,
|
||||
schema: options.schema,
|
||||
tableName: options.tableName,
|
||||
tableType: options.tableType,
|
||||
columns: options.columns,
|
||||
limit: pageSize,
|
||||
offset: 0,
|
||||
});
|
||||
return options.executePage(query);
|
||||
}
|
||||
let offset = 0;
|
||||
const rows: QueryResult["rows"] = [];
|
||||
let columns: string[] = [];
|
||||
|
||||
@@ -79,6 +79,12 @@ const capabilityByType: Partial<Record<DatabaseType, Partial<TableMetadataCapabi
|
||||
triggers: false,
|
||||
ddl: false,
|
||||
},
|
||||
victoriametrics: {
|
||||
indexes: false,
|
||||
foreignKeys: false,
|
||||
triggers: false,
|
||||
ddl: false,
|
||||
},
|
||||
questdb: {
|
||||
indexes: true,
|
||||
foreignKeys: false,
|
||||
|
||||
@@ -96,11 +96,21 @@ export function qualifiedTableName(options: Pick<BuildTableSelectSqlOptions, "da
|
||||
return quoteTableIdentifier(databaseType, tableName);
|
||||
}
|
||||
|
||||
export function metricSelector(metricName: string): string {
|
||||
const escaped = metricName.replaceAll("\\", "\\\\").replaceAll('"', '\\"').replaceAll("\n", "\\n");
|
||||
return `{__name__="${escaped}"}`;
|
||||
}
|
||||
|
||||
export function metricRangeQuery(metricName: string, lookback = "1h"): string {
|
||||
return `${metricSelector(metricName)}[${lookback}]`;
|
||||
}
|
||||
|
||||
export function normalizeWhereInput(whereInput?: string): string {
|
||||
const withoutSemicolon = whereInput?.trim().replace(/;+$/, "").trim() ?? "";
|
||||
return withoutSemicolon.replace(/^where\b/i, "").trim();
|
||||
}
|
||||
|
||||
export async function buildTableSelectSql(options: BuildTableSelectSqlOptions): Promise<string> {
|
||||
if (options.databaseType === "victoriametrics") return metricRangeQuery(options.tableName);
|
||||
return api.buildTableSelectSql(options);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ColumnInfo, DatabaseType } from "@/types/database";
|
||||
import { qualifiedTableName, quoteTableDataIdentifier } from "@/lib/table/tableSelectSql";
|
||||
import { metricRangeQuery, qualifiedTableName, quoteTableDataIdentifier } from "@/lib/table/tableSelectSql";
|
||||
|
||||
export interface TableSqlTemplateOptions {
|
||||
databaseType?: DatabaseType;
|
||||
@@ -13,6 +13,7 @@ export interface TableSqlTemplateOptions {
|
||||
}
|
||||
|
||||
export function buildTableSelectTemplate(options: TableSqlTemplateOptions): string {
|
||||
if (options.databaseType === "victoriametrics") return metricRangeQuery(options.tableName);
|
||||
const tableName = templateTableName(options);
|
||||
const columns = options.columns ?? [];
|
||||
if (!columns.length) {
|
||||
|
||||
@@ -1011,6 +1011,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
sundb: "科蓝 SUNDB",
|
||||
oscar: "神通 OSCAR",
|
||||
influxdb: "InfluxDB",
|
||||
victoriametrics: "VictoriaMetrics",
|
||||
};
|
||||
|
||||
const profile = config.driver_profile || config.db_type;
|
||||
|
||||
@@ -66,6 +66,7 @@ export type DatabaseType =
|
||||
| "zookeeper"
|
||||
| "iris"
|
||||
| "influxdb"
|
||||
| "victoriametrics"
|
||||
| "jdbc"
|
||||
| "mq"
|
||||
| "mqtt"
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface VictoriaMetricsExternalConfig {
|
||||
apiPath: string;
|
||||
lookback: string;
|
||||
}
|
||||
Reference in new issue
Block a user