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(
|
||||
|
||||
Reference in new issue
Block a user