Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions ui/chen/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,24 @@ export async function fetchChenProfile(chenToken: string) {
return readJson<ChenProfile>(response);
}

export async function uploadChenSqlFile(
chenToken: string,
file: File,
fetchImpl: typeof fetch = fetch
) {
const body = new FormData();
body.append("file", file);
const response = await fetchImpl(chenPath("/api/console/upload"), {
method: "POST",
credentials: "include",
headers: buildHeaders(chenToken, getWebApiMutationHeaders()),
body
});
const result = await readJson<{ path?: string }>(response);
if (!result.path?.trim()) throw new Error("Chen upload returned no SQL file path");
return { path: result.path };
}

export function sanitizeChenExportFileName(value: string, fallback = "chen-export") {
const withoutControls = Array.from(value)
.filter((character) => {
Expand Down
50 changes: 46 additions & 4 deletions ui/chen/components/QueryConsolePanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@ import { useChenSqlSnippets } from "~/chen/composables/useChenSqlSnippets";

const props = defineProps<{
tab: ChenQueryConsoleTab
contextLabel: string
dbType: string
canCopy: boolean
}>();

const emit = defineEmits<{
run: [tab: ChenQueryConsoleTab, selectedSql: string]
cancel: [tab: ChenQueryConsoleTab]
uploadSql: [tab: ChenQueryConsoleTab, file: File]
dataViewAction: [tab: ChenQueryConsoleTab, result: ChenQueryResultTab, action: ChenDataViewAction, data?: ChenDataViewActionData]
dismissMessage: [tab: ChenQueryConsoleTab]
updateStatement: [tab: ChenQueryConsoleTab, value: string]
Expand All @@ -26,6 +26,7 @@ const emit = defineEmits<{
}>();

const sqlEditor = ref<{ selectedText: () => string } | null>(null);
const sqlUploadInput = ref<HTMLInputElement | null>(null);
const hasSelection = ref(false);
const messageOpen = ref(false);
const saveSnippetDialogOpen = ref(false);
Expand Down Expand Up @@ -55,6 +56,32 @@ function requestErrorMessage(cause: unknown) {
return cause instanceof Error ? cause.message : String(cause);
}

function handleSqlFileChange(event: Event) {
const input = event.target as HTMLInputElement;
const file = input.files?.[0];
input.value = "";
if (!file) return;

if (!file.name.toLowerCase().endsWith(".sql")) {
toast.add({
title: "SQL upload failed",
description: "Choose a .sql file.",
color: "error"
});
return;
}
if (file.size === 0) {
toast.add({
title: "SQL upload failed",
description: "SQL file is empty.",
color: "error"
});
return;
}

emit("uploadSql", props.tab, file);
}

async function openSnippetDialog() {
selectSnippetDialogOpen.value = true;
try {
Expand Down Expand Up @@ -158,9 +185,6 @@ onBeforeUnmount(clearMessageTimer);
:disabled="!tab.state.canCancel"
@click="emit('cancel', tab)"
/>
<UBadge color="neutral" variant="subtle">
{{ tab.state.currentContext || contextLabel || 'Context' }}
</UBadge>
<UButton
icon="i-lucide-folder-open"
size="sm"
Expand All @@ -181,6 +205,24 @@ onBeforeUnmount(clearMessageTimer);
>
Save
</UButton>
<input
ref="sqlUploadInput"
type="file"
accept=".sql"
class="hidden"
@change="handleSqlFileChange"
>
<UButton
icon="i-lucide-upload"
size="sm"
color="neutral"
variant="soft"
:loading="tab.uploadingSql"
:disabled="queryBusy || tab.uploadingSql"
@click="sqlUploadInput?.click()"
>
Upload SQL
</UButton>
</div>
<div class="relative flex min-h-0 flex-1">
<ChenSqlEditor
Expand Down
10 changes: 10 additions & 0 deletions ui/chen/composables/useChenQueryConsole.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,15 @@ export function useChenQueryConsole(
});
}

function runQueryFile(tab: ChenQueryConsoleTab, path: string) {
if (tab.state.loading || tab.state.inQuery || !path.trim()) return;
dismissQueryMessage(tab);
sendConsoleAction(tab, "query_console_action", {
action: "run_sql_file",
data: path
});
}

function runConsoleTab(tab: ChenPromptConsoleTab) {
const sql = tab.pendingSql.trim();
if (!sql) return;
Expand Down Expand Up @@ -177,6 +186,7 @@ export function useChenQueryConsole(
handleQueryConsolePacket,
removeQueryResult,
runConsoleTab,
runQueryFile,
runQueryTab,
updateQueryResult
};
Expand Down
1 change: 1 addition & 0 deletions ui/chen/composables/useChenWorkspaceTabs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export function useChenWorkspaceTabs() {
kind: "query",
nodeKey,
statement: "",
uploadingSql: false,
state: {},
logs: [],
message: null,
Expand Down
1 change: 1 addition & 0 deletions ui/chen/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ export interface ChenConsoleHistoryEntry {
export interface ChenQueryConsoleTab extends ChenTabDefinition {
kind: "query"
statement: string
uploadingSql: boolean
state: ChenConsoleState
logs: string[]
message: ChenConsoleMessage | null
Expand Down
22 changes: 20 additions & 2 deletions ui/chen/workspaces/DatabaseSessionSurface.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import type {
} from "~/chen/types";
import type { WorkspaceSessionTab } from "~/composables/useWorkspaceTabs";

import { fetchChenActions, fetchChenExport } from "~/chen/api";
import { fetchChenActions, fetchChenExport, uploadChenSqlFile } from "~/chen/api";
import ChenSessionState from "~/chen/components/ChenSessionState.vue";
import ConsolePanel from "~/chen/components/ConsolePanel.vue";
import DataViewPanel from "~/chen/components/DataViewPanel.vue";
Expand Down Expand Up @@ -362,6 +362,24 @@ function runQueryTab(tab: ChenQueryLikeWorkspaceTab, selectedSql = "") {
if (tab.kind === "console") queryConsole.runConsoleTab(tab);
}

async function uploadQuerySql(tab: ChenQueryConsoleTab, file: File) {
if (tab.uploadingSql || tab.state.loading || tab.state.inQuery) return;
tab.uploadingSql = true;
try {
const result = await uploadChenSqlFile(auth.chenToken.value, file);
queryConsole.runQueryFile(tab, result.path);
toast.add({ title: "SQL file uploaded", description: file.name, color: "success" });
} catch (cause) {
toast.add({
title: "SQL upload failed",
description: cause instanceof Error ? cause.message : String(cause),
color: "error"
});
} finally {
tab.uploadingSql = false;
}
}

function cancelQueryLikeTab(tab: ChenQueryLikeWorkspaceTab) {
queryConsole.cancelQueryLikeTab(tab);
}
Expand Down Expand Up @@ -487,11 +505,11 @@ defineExpose({ focus });
<QueryConsolePanel
v-if="activeQueryTab"
:tab="activeQueryTab"
:context-label="currentContextLabel"
:db-type="auth.profile.value?.dbType || ''"
:can-copy="auth.profile.value?.canCopy === true"
@run="runQueryTab"
@cancel="cancelQueryLikeTab"
@upload-sql="uploadQuerySql"
@data-view-action="runQueryDataViewAction"
@dismiss-message="dismissQueryMessage"
@update-statement="updateQueryStatement"
Expand Down
Loading