Files
gridline/desktop/src/components/editor/QueryToolbar.tsx
T
adrianbonpin 6854d889c5 refactor: convert to bun-workspaces monorepo (desktop/ + www/)
- Move the Tauri app (React frontend + Rust backend) into desktop/ via
  git mv — configs unchanged (relative paths: tauri.conf.json, vite.config.ts)
- Root package.json becomes a private bun workspace container with
  orchestration scripts; desktop package renamed gridline-desktop
- Scaffold www/ with Astro 7 + Tailwind v4 (hand-wired vite plugin,
  static output, ready for Dokploy)
- Update release.yml for the new layout: projectPath: desktop,
  desktop/src-tauri resource paths, rust-cache workspace path
- Update README + AGENTS.md structure trees and dev commands
- Verified: vitest (1074), cargo test (354), desktop build, tauri dev
  (window launches), tauri build (dmg + app bundles), Astro build
2026-08-16 19:31:53 +08:00

177 lines
5.9 KiB
TypeScript

import { useState } from "react";
import { Play, Square, Wand2, Save } from "lucide-react";
import { Tooltip } from "../ui/Tooltip";
import { QueryHistoryDropdown } from "./QueryHistoryDropdown";
import { SaveQueryDialog } from "./SaveQueryDialog";
import { useDbViewerStore } from "../../stores/dbViewerStore";
import type { DbType } from "../../lib/types";
const DB_TYPE_LABELS: Record<DbType, string> = {
postgresql: "PostgreSQL",
mysql: "MySQL",
sqlite: "SQLite",
redis: "Redis",
};
/**
* Resolve the run-query modifier key for the given platform string
* (from `navigator.platform`). Mac platforms show ⌘ + the return glyph,
* everything else shows Ctrl + Enter.
*/
export function queryShortcut(platform: string): {
mod: string;
enter: string;
} {
const isMac = /Mac|iPhone|iPad/.test(platform);
return isMac
? { mod: "⌘", enter: "⏎" }
: { mod: "Ctrl", enter: "Enter" };
}
interface QueryToolbarProps {
onRun: () => void;
onFormat: () => void;
connectionId: string;
onRestore: (sql: string) => void;
onRunFromHistory: (sql: string) => void;
dbType?: DbType;
readOnly?: boolean;
isRunning?: boolean;
onCancel?: () => void;
}
export function QueryToolbar({
onRun,
onFormat,
connectionId,
onRestore,
onRunFromHistory,
dbType,
readOnly = false,
isRunning: isRunningProp,
onCancel,
}: QueryToolbarProps) {
const tabs = useDbViewerStore((s) => s.tabs);
const activeTabId = useDbViewerStore((s) => s.activeTabId);
const isRunning =
isRunningProp ?? tabs.find((t) => t.id === activeTabId)?.loading ?? false;
const [saveDialogOpen, setSaveDialogOpen] = useState(false);
const activeTab = tabs.find((t) => t.id === activeTabId);
const currentQueryText = activeTab?.query ?? "";
const shortcut = queryShortcut(
typeof navigator !== "undefined" ? navigator.platform : "",
);
return (
<div className="relative flex items-center gap-2 border-b border-border px-3 py-1.5 text-xs text-text-muted">
{/* run pulse: mirrors the table toolbar refresh pulse, absolutely positioned so it never shifts layout */}
{isRunning && (
<div
data-testid="query-run-pulse"
aria-hidden="true"
className="absolute inset-0 pointer-events-none animate-toolbar-pulse bg-accent"
/>
)}
<div
className="flex items-center gap-1"
aria-label="Query toolbar actions"
>
{/* Run Query: outline play that fills on hover; tooltip (delayed) reveals the shortcut */}
<Tooltip
content={
<span className="inline-flex items-center gap-1">
<kbd className="rounded border border-border bg-surface px-1 font-mono text-[10px] leading-none">
{shortcut.mod}
</kbd>
<span className="text-text-muted">+</span>
<kbd className="rounded border border-border bg-surface px-1 font-mono text-[10px] leading-none">
{shortcut.enter}
</kbd>
</span>
}
side="bottom"
>
<button
type="button"
onClick={onRun}
disabled={readOnly}
className="group flex items-center gap-1.5 rounded-md bg-accent px-2.5 py-1 font-medium text-white transition-colors hover:bg-accent-hover cursor-pointer disabled:cursor-not-allowed disabled:opacity-50"
aria-label="Run query"
>
<Play className="h-3.5 w-3.5 transition-[fill] duration-150 group-hover:fill-current" />
<span>Run Query</span>
</button>
</Tooltip>
{/* Cancel Query */}
{isRunning && onCancel && (
<Tooltip content="Cancel running query" side="bottom">
<button
type="button"
onClick={onCancel}
className="flex items-center gap-1.5 rounded-md border border-red-500/50 bg-red-500/10 px-2.5 py-1 font-medium text-red-400 transition-colors hover:bg-red-500/20 cursor-pointer"
aria-label="Cancel query"
>
<Square className="h-3 w-3 fill-current" />
<span>Cancel</span>
</button>
</Tooltip>
)}
{/* History dropdown */}
<QueryHistoryDropdown
connectionId={connectionId}
onRestore={onRestore}
onRun={onRunFromHistory}
/>
{/* Auto format: icon only with tooltip */}
<Tooltip content="Auto format query" side="bottom">
<button
type="button"
onClick={onFormat}
disabled={readOnly}
className="flex items-center rounded px-2 py-1.5 hover:bg-surface-raised hover:text-text transition-colors cursor-pointer disabled:cursor-not-allowed disabled:opacity-50"
aria-label="Auto format query"
>
<Wand2 className="h-3.5 w-3.5" />
</button>
</Tooltip>
{/* Save Query icon */}
<Tooltip content="Save query" side="bottom">
<button
type="button"
aria-label="Save query"
onClick={() => {
if (currentQueryText.trim()) {
setSaveDialogOpen(true);
}
}}
disabled={readOnly || !currentQueryText.trim()}
className="flex items-center rounded px-2 py-1.5 hover:bg-surface-raised hover:text-text transition-colors cursor-pointer disabled:cursor-not-allowed disabled:opacity-50"
>
<Save className="h-3.5 w-3.5" />
</button>
</Tooltip>
</div>
{dbType && (
<div className="ml-auto">
<span className="rounded-md border border-border px-2 py-0.5 font-mono text-[10px] uppercase tracking-wide text-text-muted">
{DB_TYPE_LABELS[dbType]}
</span>
</div>
)}
<SaveQueryDialog
open={saveDialogOpen}
onClose={() => setSaveDialogOpen(false)}
connectionId={connectionId}
queryText={currentQueryText}
/>
</div>
);
}