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
28 changes: 28 additions & 0 deletions ui/chen/components/ChenSessionState.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<script setup lang="ts">
defineProps<{
icon?: string
loading?: boolean
message: string
actionLabel?: string
}>();

defineEmits<{
action: []
}>();
</script>

<template>
<div class="grid h-full place-items-center text-sm text-muted">
<div class="flex max-w-md flex-col items-center gap-3 text-center">
<UIcon
:name="icon || (loading ? 'i-lucide-loader-circle' : 'i-lucide-database-zap')"
class="size-6"
:class="loading ? 'animate-spin' : ''"
/>
<p>{{ message }}</p>
<UButton v-if="actionLabel" size="sm" variant="soft" @click="$emit('action')">
{{ actionLabel }}
</UButton>
</div>
</div>
</template>
81 changes: 81 additions & 0 deletions ui/chen/components/ConsolePanel.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<script setup lang="ts">
import type { ChenPromptConsoleTab, ChenQueryLikeWorkspaceTab, ChenQueryResultTab } from "~/chen/types";

import QueryResultTabs from "~/chen/components/QueryResultTabs.vue";

const props = defineProps<{
tab: ChenPromptConsoleTab
contextLabel: string
promptLabel: string
}>();

const emit = defineEmits<{
run: [tab: ChenPromptConsoleTab]
cancel: [tab: ChenQueryLikeWorkspaceTab]
download: [result: ChenQueryResultTab]
updatePendingSql: [tab: ChenPromptConsoleTab, value: string]
activateResult: [tab: ChenPromptConsoleTab, id: string]
}>();

// TODO(chen-native): Confirm whether this console-like prompt should remain
// as an independent entry or fold back into QueryConsolePanel.
const pendingSqlValue = computed({
get: () => props.tab.pendingSql,
set: (value: string) => emit("updatePendingSql", props.tab, value)
});
</script>

<template>
<div class="grid h-full min-h-0 grid-rows-[minmax(0,1fr)_minmax(16rem,42%)]">
<div class="flex min-h-0 flex-col border-b border-default">
<div class="flex items-center gap-2 border-b border-default px-3 py-2">
<UButton icon="i-lucide-play" size="sm" @click="emit('run', tab)" />
<UButton icon="i-lucide-square" size="sm" color="neutral" variant="soft" @click="emit('cancel', tab)" />
<UBadge color="neutral" variant="subtle">
{{ tab.state.currentContext || contextLabel || 'Console' }}
</UBadge>
</div>

<div class="min-h-0 flex-1 overflow-auto px-3 py-3 font-ui-mono text-sm">
<div v-if="!tab.historyEntries.length && !tab.logs.length" class="grid h-full place-items-center text-center text-muted">
<div>
<UIcon name="i-lucide-square-terminal" class="mx-auto mb-2 size-5" />
<p>Run SQL here like a `mysql` or `psql` console.</p>
</div>
</div>

<div v-for="entry in tab.historyEntries" :key="entry.id" class="mb-3">
<div class="mb-1 flex items-start gap-2">
<span class="shrink-0 text-primary">{{ promptLabel }}</span>
<pre class="whitespace-pre-wrap break-words text-[var(--app-fg)]">{{ entry.sql }}</pre>
</div>
</div>

<div v-if="tab.logs.length" class="rounded-md border border-default bg-[var(--workspace-surface-sub-panel)] px-3 py-2 text-xs text-muted">
<pre class="whitespace-pre-wrap">{{ tab.logs.join('\n') }}</pre>
</div>
</div>

<div class="border-t border-default px-3 py-3">
<div class="flex items-start gap-2 rounded-md border border-default bg-[var(--workspace-surface-sub-panel)] px-3 py-2">
<span class="pt-2 font-ui-mono text-sm text-primary">{{ promptLabel }}</span>
<textarea
v-model="pendingSqlValue"
class="min-h-20 flex-1 resize-none bg-transparent font-ui-mono text-sm text-[var(--app-fg)] outline-none placeholder:text-[var(--app-muted)]"
placeholder="Type SQL and press Cmd/Ctrl+Enter to run"
@keydown.enter.meta.prevent="emit('run', tab)"
@keydown.enter.ctrl.prevent="emit('run', tab)"
/>
</div>
</div>
</div>

<QueryResultTabs
:result-tabs="tab.resultTabs"
:active-result-tab-id="tab.activeResultTabId"
empty-message="Query results will open here."
@update:active-result-tab-id="emit('activateResult', tab, $event)"
@download="emit('download', $event)"
/>
</div>
</template>
248 changes: 248 additions & 0 deletions ui/chen/components/DataViewPanel.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
<script setup lang="ts">
import type { ChenDataViewConsoleTab, ChenDataViewPropertyTab } from "~/chen/types";

import ChenDataGrid from "~/chen/components/DataGrid.client.vue";
import { useChenDataViewDerivedMeta } from "~/chen/composables/useChenDataViewDerivedMeta";

const props = defineProps<{
tab: ChenDataViewConsoleTab
dbType?: string
protocol?: string
}>();

const emit = defineEmits<{
download: [tab: ChenDataViewConsoleTab]
updatePanel: [tab: ChenDataViewConsoleTab, panel: "data" | "properties"]
updatePropertyTab: [tab: ChenDataViewConsoleTab, propertyTab: ChenDataViewPropertyTab]
}>();

const dbTypeRef = computed(() => props.dbType);
const protocolRef = computed(() => props.protocol);
const {
dataViewBasicInfo,
dataViewColumns,
dataViewConstraints,
dataViewDDL,
dataViewForeignKeys,
dataViewIndexes,
dataViewPropertyTabs
} = useChenDataViewDerivedMeta(dbTypeRef, protocolRef);
</script>

<template>
<div class="flex h-full min-h-0 flex-col">
<div class="flex items-center justify-between gap-2 border-b border-default px-2 py-1">
<div class="flex min-w-0 items-center gap-1 overflow-x-auto">
<button
class="rounded-md px-2 py-1 text-xs"
:class="tab.activePanel === 'data' ? 'bg-accented' : 'text-muted'"
@click="emit('updatePanel', tab, 'data')"
>
Data
</button>
<button
class="rounded-md px-2 py-1 text-xs"
:class="tab.activePanel === 'properties' ? 'bg-accented' : 'text-muted'"
@click="emit('updatePanel', tab, 'properties')"
>
Properties
</button>

<template v-if="tab.activePanel === 'properties'">
<button
v-for="propertyTab in dataViewPropertyTabs"
:key="propertyTab.id"
class="rounded-md px-2 py-1 text-xs"
:class="tab.activePropertyTab === propertyTab.id ? 'bg-accented' : 'text-muted'"
@click="emit('updatePropertyTab', tab, propertyTab.id)"
>
{{ propertyTab.label }}
</button>
</template>
</div>

<UButton
size="xs"
icon="i-lucide-download"
color="neutral"
variant="soft"
@click="emit('download', tab)"
/>
</div>

<div v-if="tab.activePanel === 'data'" class="min-h-0 flex-1 overflow-auto">
<ChenDataGrid
:key="`${tab.id}:${tab.data?.fields?.map(field => field.name).join(',') || ''}:${tab.data?.data?.length || 0}`"
:dataset="tab.data"
/>
</div>

<div v-else class="flex min-h-0 flex-1 flex-col overflow-hidden">
<div v-if="tab.activePropertyTab === 'basic'" class="grid min-h-0 flex-1 gap-3 overflow-auto p-4 md:grid-cols-2">
<div
v-for="item in dataViewBasicInfo(tab)"
:key="item.label"
class="rounded-lg border border-default bg-[var(--workspace-surface-sub-panel)] px-3 py-2"
>
<div class="mb-1 text-[11px] uppercase tracking-wide text-muted">
{{ item.label }}
</div>
<div class="text-sm">
{{ item.value }}
</div>
</div>
</div>

<div v-else-if="tab.activePropertyTab === 'columns'" class="min-h-0 flex-1 overflow-auto p-3">
<div class="overflow-hidden rounded-lg border border-default">
<table class="w-full text-left text-sm">
<thead class="bg-[var(--workspace-surface-sub-panel)] text-muted">
<tr>
<th class="px-3 py-2 font-medium">
Name
</th>
<th class="px-3 py-2 font-medium">
Type
</th>
<th class="px-3 py-2 font-medium">
Nullable
</th>
<th class="px-3 py-2 font-medium">
Key
</th>
</tr>
</thead>
<tbody>
<tr v-for="column in dataViewColumns(tab)" :key="column.name" class="border-t border-default">
<td class="px-3 py-2">
{{ column.name }}
</td>
<td class="px-3 py-2 text-muted">
{{ column.type }}
</td>
<td class="px-3 py-2 text-muted">
{{ column.nullable }}
</td>
<td class="px-3 py-2 text-muted">
{{ column.key }}
</td>
</tr>
</tbody>
</table>
</div>
</div>

<div v-else-if="tab.activePropertyTab === 'indexes'" class="min-h-0 flex-1 overflow-auto p-3">
<div class="overflow-hidden rounded-lg border border-default">
<table class="w-full text-left text-sm">
<thead class="bg-[var(--workspace-surface-sub-panel)] text-muted">
<tr>
<th class="px-3 py-2 font-medium">
Name
</th>
<th class="px-3 py-2 font-medium">
Columns
</th>
<th class="px-3 py-2 font-medium">
Unique
</th>
<th class="px-3 py-2 font-medium">
Method
</th>
</tr>
</thead>
<tbody>
<tr v-for="index in dataViewIndexes(tab)" :key="index.name" class="border-t border-default">
<td class="px-3 py-2">
{{ index.name }}
</td>
<td class="px-3 py-2 text-muted">
{{ index.columns }}
</td>
<td class="px-3 py-2 text-muted">
{{ index.unique }}
</td>
<td class="px-3 py-2 text-muted">
{{ index.method }}
</td>
</tr>
</tbody>
</table>
</div>
</div>

<div v-else-if="tab.activePropertyTab === 'foreignKeys'" class="min-h-0 flex-1 overflow-auto p-3">
<div v-if="dataViewForeignKeys(tab).length" class="overflow-hidden rounded-lg border border-default">
<table class="w-full text-left text-sm">
<thead class="bg-[var(--workspace-surface-sub-panel)] text-muted">
<tr>
<th class="px-3 py-2 font-medium">
Name
</th>
<th class="px-3 py-2 font-medium">
Column
</th>
<th class="px-3 py-2 font-medium">
References
</th>
</tr>
</thead>
<tbody>
<tr v-for="foreignKey in dataViewForeignKeys(tab)" :key="foreignKey.name" class="border-t border-default">
<td class="px-3 py-2">
{{ foreignKey.name }}
</td>
<td class="px-3 py-2 text-muted">
{{ foreignKey.column }}
</td>
<td class="px-3 py-2 text-muted">
{{ foreignKey.references }}
</td>
</tr>
</tbody>
</table>
</div>
<div v-else class="grid h-full place-items-center text-sm text-muted">
No foreign keys in preview.
</div>
</div>

<div v-else-if="tab.activePropertyTab === 'constraints'" class="min-h-0 flex-1 overflow-auto p-3">
<div class="overflow-hidden rounded-lg border border-default">
<table class="w-full text-left text-sm">
<thead class="bg-[var(--workspace-surface-sub-panel)] text-muted">
<tr>
<th class="px-3 py-2 font-medium">
Name
</th>
<th class="px-3 py-2 font-medium">
Type
</th>
<th class="px-3 py-2 font-medium">
Definition
</th>
</tr>
</thead>
<tbody>
<tr v-for="constraint in dataViewConstraints(tab)" :key="constraint.name" class="border-t border-default">
<td class="px-3 py-2">
{{ constraint.name }}
</td>
<td class="px-3 py-2 text-muted">
{{ constraint.type }}
</td>
<td class="px-3 py-2 font-ui-mono text-xs text-muted">
{{ constraint.definition }}
</td>
</tr>
</tbody>
</table>
</div>
</div>

<div v-else class="min-h-0 flex-1 overflow-auto p-3">
<pre class="rounded-lg border border-default bg-[var(--workspace-surface-sub-panel)] p-3 font-ui-mono text-xs text-[var(--app-fg)]">{{ dataViewDDL(tab) }}</pre>
</div>
</div>
</div>
</template>
Loading
Loading