fix(tabs): replace tab icon during query execution
This commit is contained in:
4 files changed
+74
-52
No files matched your search
@@ -703,26 +703,27 @@ function onOverflowItemKeydown(event: KeyboardEvent, tabId: string, kind: "regul
|
||||
@mousemove="handleTabDragTarget($event, tab)"
|
||||
@mouseleave="tabDrag.clearTarget(tab.id)"
|
||||
>
|
||||
<span class="shrink-0" :class="tabIconClass(tab)">
|
||||
<AlertTriangle v-if="tab.externalSqlFileMissing" class="h-3.5 w-3.5" />
|
||||
<Table2 v-else-if="tab.mode === 'data' || tab.mode === 'mongo' || tab.mode === 'redis' || tab.mode === 'hbase'" class="h-3.5 w-3.5" />
|
||||
<DatabaseIcon v-else-if="tab.mode === 'mq'" :db-type="tabDatabaseIconType(tab)" class="h-3.5 w-3.5" />
|
||||
<TableProperties v-else-if="tab.mode === 'vector'" class="h-3.5 w-3.5" />
|
||||
<KeyRound v-else-if="tab.mode === 'etcd' || tab.mode === 'zookeeper' || tab.mode === 'consul'" class="h-3.5 w-3.5" />
|
||||
<Gauge v-else-if="tab.mode === 'consul-overview'" class="h-3.5 w-3.5" />
|
||||
<Gauge v-else-if="tab.mode === 'etcd-dashboard'" class="h-3.5 w-3.5" />
|
||||
<ShieldCheck v-else-if="tab.mode === 'etcd-access-control'" class="h-3.5 w-3.5" />
|
||||
<Network v-else-if="tab.mode === 'nacos'" class="h-3.5 w-3.5" />
|
||||
<Database v-else-if="tab.mode === 'databases'" class="h-3.5 w-3.5" />
|
||||
<TableProperties v-else-if="tab.mode === 'objects'" class="h-3.5 w-3.5" />
|
||||
<PencilRuler v-else-if="tab.mode === 'structure'" class="h-3.5 w-3.5" />
|
||||
<CalendarClock v-else-if="tab.mode === 'dameng-jobs'" class="h-3.5 w-3.5" />
|
||||
<Activity v-else-if="tab.mode === 'processlist' || tab.mode === 'sqlserver-trace'" class="h-3.5 w-3.5" />
|
||||
<Gauge v-else-if="tab.mode === 'mysql-dashboard' || tab.mode === 'postgres-dashboard' || tab.mode === 'nacos-dashboard'" class="h-3.5 w-3.5" />
|
||||
<GitBranch v-else-if="tab.mode === 'dolt-version-control'" class="h-3.5 w-3.5" />
|
||||
<Code2 v-else class="h-3.5 w-3.5" />
|
||||
</span>
|
||||
<TabExecutionStatus :tab="tab" />
|
||||
<TabExecutionStatus :tab="tab">
|
||||
<span class="shrink-0" :class="tabIconClass(tab)">
|
||||
<AlertTriangle v-if="tab.externalSqlFileMissing" class="h-3.5 w-3.5" />
|
||||
<Table2 v-else-if="tab.mode === 'data' || tab.mode === 'mongo' || tab.mode === 'redis' || tab.mode === 'hbase'" class="h-3.5 w-3.5" />
|
||||
<DatabaseIcon v-else-if="tab.mode === 'mq'" :db-type="tabDatabaseIconType(tab)" class="h-3.5 w-3.5" />
|
||||
<TableProperties v-else-if="tab.mode === 'vector'" class="h-3.5 w-3.5" />
|
||||
<KeyRound v-else-if="tab.mode === 'etcd' || tab.mode === 'zookeeper' || tab.mode === 'consul'" class="h-3.5 w-3.5" />
|
||||
<Gauge v-else-if="tab.mode === 'consul-overview'" class="h-3.5 w-3.5" />
|
||||
<Gauge v-else-if="tab.mode === 'etcd-dashboard'" class="h-3.5 w-3.5" />
|
||||
<ShieldCheck v-else-if="tab.mode === 'etcd-access-control'" class="h-3.5 w-3.5" />
|
||||
<Network v-else-if="tab.mode === 'nacos'" class="h-3.5 w-3.5" />
|
||||
<Database v-else-if="tab.mode === 'databases'" class="h-3.5 w-3.5" />
|
||||
<TableProperties v-else-if="tab.mode === 'objects'" class="h-3.5 w-3.5" />
|
||||
<PencilRuler v-else-if="tab.mode === 'structure'" class="h-3.5 w-3.5" />
|
||||
<CalendarClock v-else-if="tab.mode === 'dameng-jobs'" class="h-3.5 w-3.5" />
|
||||
<Activity v-else-if="tab.mode === 'processlist' || tab.mode === 'sqlserver-trace'" class="h-3.5 w-3.5" />
|
||||
<Gauge v-else-if="tab.mode === 'mysql-dashboard' || tab.mode === 'postgres-dashboard' || tab.mode === 'nacos-dashboard'" class="h-3.5 w-3.5" />
|
||||
<GitBranch v-else-if="tab.mode === 'dolt-version-control'" class="h-3.5 w-3.5" />
|
||||
<Code2 v-else class="h-3.5 w-3.5" />
|
||||
</span>
|
||||
</TabExecutionStatus>
|
||||
<input
|
||||
v-if="editingTabId === tab.id"
|
||||
v-model="editingTitle"
|
||||
@@ -843,9 +844,10 @@ function onOverflowItemKeydown(event: KeyboardEvent, tabId: string, kind: "regul
|
||||
@contextmenu="onContextMenu"
|
||||
@keydown="onOverflowItemKeydown($event, tab.id, 'regular')"
|
||||
>
|
||||
<DatabaseIcon v-if="tab.mode === 'mq'" :db-type="tabDatabaseIconType(tab)" class="h-3.5 w-3.5 shrink-0" />
|
||||
<component :is="tabMenuIcon(tab)" v-else :class="['h-3.5 w-3.5 shrink-0', tabIconClass(tab)]" />
|
||||
<TabExecutionStatus :tab="tab" />
|
||||
<TabExecutionStatus :tab="tab">
|
||||
<DatabaseIcon v-if="tab.mode === 'mq'" :db-type="tabDatabaseIconType(tab)" class="h-3.5 w-3.5 shrink-0" />
|
||||
<component :is="tabMenuIcon(tab)" v-else :class="['h-3.5 w-3.5 shrink-0', tabIconClass(tab)]" />
|
||||
</TabExecutionStatus>
|
||||
<span class="inline-flex min-w-0 flex-1 items-center gap-0.5 overflow-hidden">
|
||||
<span v-if="isDirtyTab(tab)" aria-hidden="true" class="dirty-tab-marker">*</span>
|
||||
<span class="min-w-0 flex-1 truncate" :style="tabTitleStyle(tab)">{{ tabTitleText(tab) }}</span>
|
||||
@@ -911,26 +913,27 @@ function onOverflowItemKeydown(event: KeyboardEvent, tabId: string, kind: "regul
|
||||
@mousemove="handleTabDragTarget($event, tab)"
|
||||
@mouseleave="tabDrag.clearTarget(tab.id)"
|
||||
>
|
||||
<span class="shrink-0" :class="tabIconClass(tab)">
|
||||
<AlertTriangle v-if="tab.externalSqlFileMissing" class="h-3.5 w-3.5" />
|
||||
<Table2 v-else-if="tab.mode === 'data' || tab.mode === 'mongo' || tab.mode === 'redis' || tab.mode === 'hbase'" class="h-3.5 w-3.5" />
|
||||
<DatabaseIcon v-else-if="tab.mode === 'mq'" :db-type="tabDatabaseIconType(tab)" class="h-3.5 w-3.5" />
|
||||
<TableProperties v-else-if="tab.mode === 'vector'" class="h-3.5 w-3.5" />
|
||||
<KeyRound v-else-if="tab.mode === 'etcd' || tab.mode === 'zookeeper' || tab.mode === 'consul'" class="h-3.5 w-3.5" />
|
||||
<Gauge v-else-if="tab.mode === 'consul-overview'" class="h-3.5 w-3.5" />
|
||||
<Gauge v-else-if="tab.mode === 'etcd-dashboard'" class="h-3.5 w-3.5" />
|
||||
<ShieldCheck v-else-if="tab.mode === 'etcd-access-control'" class="h-3.5 w-3.5" />
|
||||
<Network v-else-if="tab.mode === 'nacos'" class="h-3.5 w-3.5" />
|
||||
<Database v-else-if="tab.mode === 'databases'" class="h-3.5 w-3.5" />
|
||||
<TableProperties v-else-if="tab.mode === 'objects'" class="h-3.5 w-3.5" />
|
||||
<PencilRuler v-else-if="tab.mode === 'structure'" class="h-3.5 w-3.5" />
|
||||
<CalendarClock v-else-if="tab.mode === 'dameng-jobs'" class="h-3.5 w-3.5" />
|
||||
<Activity v-else-if="tab.mode === 'processlist' || tab.mode === 'sqlserver-trace'" class="h-3.5 w-3.5" />
|
||||
<Gauge v-else-if="tab.mode === 'mysql-dashboard' || tab.mode === 'postgres-dashboard' || tab.mode === 'nacos-dashboard'" class="h-3.5 w-3.5" />
|
||||
<GitBranch v-else-if="tab.mode === 'dolt-version-control'" class="h-3.5 w-3.5" />
|
||||
<Code2 v-else class="h-3.5 w-3.5" />
|
||||
</span>
|
||||
<TabExecutionStatus :tab="tab" />
|
||||
<TabExecutionStatus :tab="tab">
|
||||
<span class="shrink-0" :class="tabIconClass(tab)">
|
||||
<AlertTriangle v-if="tab.externalSqlFileMissing" class="h-3.5 w-3.5" />
|
||||
<Table2 v-else-if="tab.mode === 'data' || tab.mode === 'mongo' || tab.mode === 'redis' || tab.mode === 'hbase'" class="h-3.5 w-3.5" />
|
||||
<DatabaseIcon v-else-if="tab.mode === 'mq'" :db-type="tabDatabaseIconType(tab)" class="h-3.5 w-3.5" />
|
||||
<TableProperties v-else-if="tab.mode === 'vector'" class="h-3.5 w-3.5" />
|
||||
<KeyRound v-else-if="tab.mode === 'etcd' || tab.mode === 'zookeeper' || tab.mode === 'consul'" class="h-3.5 w-3.5" />
|
||||
<Gauge v-else-if="tab.mode === 'consul-overview'" class="h-3.5 w-3.5" />
|
||||
<Gauge v-else-if="tab.mode === 'etcd-dashboard'" class="h-3.5 w-3.5" />
|
||||
<ShieldCheck v-else-if="tab.mode === 'etcd-access-control'" class="h-3.5 w-3.5" />
|
||||
<Network v-else-if="tab.mode === 'nacos'" class="h-3.5 w-3.5" />
|
||||
<Database v-else-if="tab.mode === 'databases'" class="h-3.5 w-3.5" />
|
||||
<TableProperties v-else-if="tab.mode === 'objects'" class="h-3.5 w-3.5" />
|
||||
<PencilRuler v-else-if="tab.mode === 'structure'" class="h-3.5 w-3.5" />
|
||||
<CalendarClock v-else-if="tab.mode === 'dameng-jobs'" class="h-3.5 w-3.5" />
|
||||
<Activity v-else-if="tab.mode === 'processlist' || tab.mode === 'sqlserver-trace'" class="h-3.5 w-3.5" />
|
||||
<Gauge v-else-if="tab.mode === 'mysql-dashboard' || tab.mode === 'postgres-dashboard' || tab.mode === 'nacos-dashboard'" class="h-3.5 w-3.5" />
|
||||
<GitBranch v-else-if="tab.mode === 'dolt-version-control'" class="h-3.5 w-3.5" />
|
||||
<Code2 v-else class="h-3.5 w-3.5" />
|
||||
</span>
|
||||
</TabExecutionStatus>
|
||||
<input
|
||||
v-if="editingTabId === tab.id"
|
||||
v-model="editingTitle"
|
||||
@@ -997,9 +1000,10 @@ function onOverflowItemKeydown(event: KeyboardEvent, tabId: string, kind: "regul
|
||||
@contextmenu="onContextMenu"
|
||||
@keydown="onOverflowItemKeydown($event, tab.id, 'fixed')"
|
||||
>
|
||||
<DatabaseIcon v-if="tab.mode === 'mq'" :db-type="tabDatabaseIconType(tab)" class="h-3.5 w-3.5 shrink-0" />
|
||||
<component :is="tabMenuIcon(tab)" v-else :class="['h-3.5 w-3.5 shrink-0', tabIconClass(tab)]" />
|
||||
<TabExecutionStatus :tab="tab" />
|
||||
<TabExecutionStatus :tab="tab">
|
||||
<DatabaseIcon v-if="tab.mode === 'mq'" :db-type="tabDatabaseIconType(tab)" class="h-3.5 w-3.5 shrink-0" />
|
||||
<component :is="tabMenuIcon(tab)" v-else :class="['h-3.5 w-3.5 shrink-0', tabIconClass(tab)]" />
|
||||
</TabExecutionStatus>
|
||||
<span class="inline-flex min-w-0 flex-1 items-center gap-0.5 overflow-hidden">
|
||||
<span v-if="isDirtyTab(tab)" aria-hidden="true" class="dirty-tab-marker">*</span>
|
||||
<span class="min-w-0 flex-1 truncate" :style="tabTitleStyle(tab)">{{ tabTitleText(tab) }}</span>
|
||||
|
||||
@@ -21,11 +21,12 @@ const status = computed(() => {
|
||||
v-if="status"
|
||||
data-tab-execution-status
|
||||
role="status"
|
||||
class="inline-flex h-4 w-4 shrink-0 items-center justify-center rounded-full bg-background/80"
|
||||
class="inline-flex h-3.5 w-3.5 shrink-0 items-center justify-center"
|
||||
:class="status === 'cancelling' ? 'text-amber-600 dark:text-amber-400' : 'text-blue-600 dark:text-blue-400'"
|
||||
:aria-label="t(queryExecutionLabelKey(tab))"
|
||||
:title="t(queryExecutionLabelKey(tab))"
|
||||
>
|
||||
<Loader2 aria-hidden="true" class="h-3 w-3 animate-spin motion-reduce:animate-none" />
|
||||
<Loader2 aria-hidden="true" class="h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
|
||||
</span>
|
||||
<slot v-else />
|
||||
</template>
|
||||
@@ -121,8 +121,9 @@ describe("AppTabBar overflow search", () => {
|
||||
});
|
||||
|
||||
describe("AppTabBar query execution status", () => {
|
||||
it("renders one shared status indicator in regular, pinned, and both overflow surfaces", () => {
|
||||
it("replaces the icon through one shared status component in every tab surface", () => {
|
||||
expect(tabBarSource).toContain('import TabExecutionStatus from "@/components/layout/TabExecutionStatus.vue";');
|
||||
expect(tabBarSource.match(/<TabExecutionStatus :tab="tab" \/>/g)).toHaveLength(4);
|
||||
expect(tabBarSource.match(/<TabExecutionStatus :tab="tab">/g)).toHaveLength(4);
|
||||
expect(tabBarSource.match(/<\/TabExecutionStatus>/g)).toHaveLength(4);
|
||||
});
|
||||
});
|
||||
@@ -10,12 +10,12 @@ vi.mock("vue-i18n", () => ({
|
||||
|
||||
const mountedApps: ReturnType<typeof createApp>[] = [];
|
||||
|
||||
function mountStatus(initial: { mode: "query" | "data"; isExecuting: boolean; isCancelling?: boolean }) {
|
||||
function mountStatus(initial: { mode: "query" | "data"; isExecuting: boolean; isCancelling?: boolean }, withFallback = false) {
|
||||
const state = reactive(initial);
|
||||
const root = document.createElement("div");
|
||||
document.body.appendChild(root);
|
||||
const app = createApp({
|
||||
setup: () => () => h(TabExecutionStatus, { tab: state }),
|
||||
setup: () => () => h(TabExecutionStatus, { tab: state }, withFallback ? { default: () => h("span", { "data-tab-icon": "" }) } : undefined),
|
||||
});
|
||||
mountedApps.push(app);
|
||||
app.mount(root);
|
||||
@@ -48,6 +48,22 @@ describe("TabExecutionStatus", () => {
|
||||
expect(status?.className).toContain("text-amber-600");
|
||||
});
|
||||
|
||||
it("replaces the tab icon while the query is executing", async () => {
|
||||
const { root, state } = mountStatus({ mode: "query", isExecuting: false }, true);
|
||||
await nextTick();
|
||||
expect(root.querySelector("[data-tab-icon]")).not.toBeNull();
|
||||
|
||||
state.isExecuting = true;
|
||||
await nextTick();
|
||||
expect(root.querySelector("[data-tab-icon]")).toBeNull();
|
||||
expect(root.querySelector("[data-tab-execution-status]")).not.toBeNull();
|
||||
|
||||
state.isExecuting = false;
|
||||
await nextTick();
|
||||
expect(root.querySelector("[data-tab-execution-status]")).toBeNull();
|
||||
expect(root.querySelector("[data-tab-icon]")).not.toBeNull();
|
||||
});
|
||||
|
||||
it.each(["success", "error", "cancelled"])("disappears as soon as %s completes", async () => {
|
||||
const { root, state } = mountStatus({ mode: "query", isExecuting: true, isCancelling: true });
|
||||
await nextTick();
|
||||
|
||||
Reference in new issue
Block a user