feat(xugu): discover and manage scheduler jobs

This commit is contained in:
Elias authored and GitHub committed 2026-08-31 14:03:34 +08:00
1 parent 1c0e97b52a
commit a849979dd1
28 files changed
+731 -42

No files matched your search

@@ -335,9 +335,23 @@ watch([deferredSearchQuery, regexMode], ([newQuery, isRegexMode], [oldQuery, was
});
});
const searchableObjectGroupTypes = new Set<TreeNodeType>(["group-tables", "group-dolt-system-tables", "group-views", "group-materialized-views", "group-procedures", "group-functions", "group-triggers", "group-events", "group-sequences", "group-synonyms", "group-packages", "group-types"]);
const searchableObjectGroupTypes = new Set<TreeNodeType>([
"group-tables",
"group-dolt-system-tables",
"group-views",
"group-materialized-views",
"group-procedures",
"group-functions",
"group-triggers",
"group-events",
"group-sequences",
"group-synonyms",
"group-jobs",
"group-packages",
"group-types",
]);
const simpleObjectParentTypes = new Set<TreeNodeType>(["database", "schema", "linked-server-schema"]);
const simpleObjectChildTypes = new Set<TreeNodeType>(["table", "view", "materialized_view", "procedure", "function", "trigger", "event", "sequence", "synonym", "package", "package-body", "type", "type-body", "load-more"]);
const simpleObjectChildTypes = new Set<TreeNodeType>(["table", "view", "materialized_view", "procedure", "function", "trigger", "event", "sequence", "synonym", "job", "package", "package-body", "type", "type-body", "load-more"]);
function isSimpleObjectSearchParent(node: TreeNode): boolean {
return settingsStore.editorSettings.sidebarObjectDisplay === "simple" && simpleObjectParentTypes.has(node.type) && node.isExpanded === true && (!!node.children?.some((child) => simpleObjectChildTypes.has(child.type)) || !!store.sidebarTableSearchQueries[node.id]?.trim());
@@ -80,7 +80,8 @@ import { connectionGroupDestinationRows } from "@/lib/sidebar/sidebarLayout";
import { objectTypesForGroupNode } from "@/lib/table/tableTree";
import { loadSidebarObjectGroup } from "@/lib/sidebar/sidebarObjectGroupRouting";
import { isXuguTypeMemberContainer } from "@/lib/sidebar/xuguTypeMembers";
import { isXuguPublicSynonymTreeNode } from "@/lib/sidebar/xuguPublicSynonyms";
import { isXuguSyntheticTreeNode } from "@/lib/sidebar/xuguPublicSynonyms";
import { buildXuguSchedulerJobSql, type XuguSchedulerJobAction } from "@/lib/database/xuguSchedulerJobSql";
import { mysqlObjectTemplateForGroup } from "@/lib/sidebar/mysqlObjectTemplates";
import { buildTableDeleteTemplate, buildTableInsertTemplate, buildTableSelectTemplate, buildTableUpdateTemplate } from "@/lib/table/tableSqlTemplates";
import { qualifiedTableName } from "@/lib/table/tableSelectSql";
@@ -649,6 +650,7 @@ const groupTypes: Set<TreeNodeType> = new Set([
"group-functions",
"group-sequences",
"group-synonyms",
"group-jobs",
"group-packages",
"group-types",
"group-partitions",
@@ -2218,7 +2220,7 @@ function openObjectSourceDialog(initialEditing: boolean, viewPackageBody = false
databaseType,
signature: sourceNode.signature,
});
const sourceIsEditable = raw.editable !== false && !["SEQUENCE", "TRIGGER", "TYPE", "TYPE_BODY"].includes(resolvedType);
const sourceIsEditable = raw.editable !== false && !["SEQUENCE", "TRIGGER", "TYPE", "TYPE_BODY", "JOB"].includes(resolvedType);
if (sourceIsEditable) {
queryStore.openObjectSourceTab({
connectionId,
@@ -2276,6 +2278,26 @@ async function compileXuguObject() {
}
}
async function executeXuguSchedulerJobAction(action: XuguSchedulerJobAction) {
const node = activeNode.value;
if (currentDatabaseType() !== "xugu" || node.type !== "job" || !node.connectionId || !node.database) return;
const sql = buildXuguSchedulerJobSql(action, node.objectName || node.label);
if (!sql) return;
if (action === "drop" && !window.confirm(t("xuguSchedulerJob.confirmDrop", { name: node.label }))) return;
try {
await connectionStore.ensureConnected(node.connectionId);
const executed = await executeTreeNodeSqlWithProductionGuard(node, sql, { database: node.database, schema: node.schema });
if (!executed) return;
toast(t("xuguSchedulerJob.actionSuccess", { action: t(`xuguSchedulerJob.${action}`), name: node.label }), 3000);
// A job belongs to the synthetic scheduler-job group. `refreshTreeNode`
// already walks a child back to that group, so this refreshes the list
// after a destructive action without retaining a stale job node.
await connectionStore.refreshTreeNode(node);
} catch (e: any) {
toast(t("contextMenu.tableOperationFailed", { message: e?.message || String(e) }), 5000);
}
}
function requestDropObject() {
void refreshDropObjectPreviewSql();
showDropObjectConfirm.value = true;
@@ -3113,12 +3135,12 @@ const canCreateSchema = computed(() => {
const canDropSchema = computed(() => {
const config = activeNode.value.connectionId ? connectionStore.getConfig(activeNode.value.connectionId) : undefined;
const dbType = effectiveDatabaseTypeForConnection(config);
return activeNode.value.type === "schema" && !isXuguPublicSynonymTreeNode(config?.db_type, activeNode.value.type, activeNode.value.schema) && !isSqlServerLinkedNode(activeNode.value) && (usesTreeSchemaMode(dbType) || dbType === "dameng") && !connectionUsesDatabaseObjectTreeMode(config);
return activeNode.value.type === "schema" && !isXuguSyntheticTreeNode(config?.db_type, activeNode.value.type, activeNode.value.schema) && !isSqlServerLinkedNode(activeNode.value) && (usesTreeSchemaMode(dbType) || dbType === "dameng") && !connectionUsesDatabaseObjectTreeMode(config);
});
const canEditSchemaComment = computed(() => {
const config = activeNode.value.connectionId ? connectionStore.getConfig(activeNode.value.connectionId) : undefined;
return activeNode.value.type === "schema" && !!activeNode.value.database && !connectionIsEffectivelyReadOnly(config) && supportsSchemaComment(effectiveDatabaseTypeForConnection(config));
return activeNode.value.type === "schema" && !!activeNode.value.database && !isXuguSyntheticTreeNode(config?.db_type, activeNode.value.type, activeNode.value.schema) && !connectionIsEffectivelyReadOnly(config) && supportsSchemaComment(effectiveDatabaseTypeForConnection(config));
});
const canBatchDropCascade = computed(() => {
@@ -3813,7 +3835,7 @@ async function confirmCreateSchema() {
function dropSchema() {
const node = sidebarDangerTarget.value ?? activeNode.value;
const config = node.connectionId ? connectionStore.getConfig(node.connectionId) : undefined;
if (isXuguPublicSynonymTreeNode(config?.db_type, node.type, node.schema)) return;
if (isXuguSyntheticTreeNode(config?.db_type, node.type, node.schema)) return;
void refreshDropSchemaPreviewSql();
showDropSchemaConfirm.value = true;
}
@@ -3823,7 +3845,7 @@ async function confirmDropSchema() {
if (!node.connectionId || !node.database) return;
try {
const config = connectionStore.getConfig(node.connectionId);
if (isXuguPublicSynonymTreeNode(config?.db_type, node.type, node.schema)) return;
if (isXuguSyntheticTreeNode(config?.db_type, node.type, node.schema)) return;
await connectionStore.ensureConnected(node.connectionId);
const dbType = effectiveDatabaseTypeForConnection(config);
let dropExecutionSchema: string | undefined;
@@ -5071,7 +5093,7 @@ function buildDatabaseSidebarMenu(context: SidebarMenuFactoryContext): boolean {
const { node, items } = context;
// 4. Database / Schema
if (node.type === "database" || node.type === "schema") {
if (isXuguPublicSynonymTreeNode(currentDatabaseType(), node.type, node.schema)) {
if (isXuguSyntheticTreeNode(currentDatabaseType(), node.type, node.schema)) {
items.push({ label: t("contextMenu.copyName"), action: copyName, icon: Copy, shortcut: shortcutCopyName.value });
items.push({ label: "", separator: true });
items.push({
@@ -5683,6 +5705,22 @@ function buildObjectSidebarMenu(context: SidebarMenuFactoryContext): boolean {
return true;
}
if (node.type === "job") {
items.push({ label: t("contextMenu.viewSource"), action: () => openObjectSourceDialog(false), icon: Code2 });
items.push({ label: t("contextMenu.copyName"), action: copyName, icon: Copy, shortcut: shortcutCopyName.value });
if (currentDatabaseType() === "xugu") {
items.push({ label: "", separator: true });
items.push({ label: t("xuguSchedulerJob.enable"), action: () => executeXuguSchedulerJobAction("enable"), icon: Play });
items.push({ label: t("xuguSchedulerJob.disable"), action: () => executeXuguSchedulerJobAction("disable"), icon: Wrench });
items.push({ label: t("xuguSchedulerJob.run"), action: () => executeXuguSchedulerJobAction("run"), icon: Play });
items.push({ label: "", separator: true });
items.push({ label: t("xuguSchedulerJob.drop"), action: () => executeXuguSchedulerJobAction("drop"), icon: Trash2, variant: "destructive" as const });
}
items.push({ label: "", separator: true });
items.push({ label: t("contextMenu.refreshChildren"), action: refresh, icon: RefreshCw, shortcut: shortcutRefresh });
return true;
}
if (node.type === "trigger" || node.type === "package" || node.type === "package-body") {
if (currentDatabaseType() === "xugu" && buildXuguCompileSql({ objectType: node.type, schema: node.schema, name: node.objectName || node.label })) {
items.push({ label: t("contextMenu.compileObject"), action: compileXuguObject, icon: Wrench });
@@ -78,7 +78,7 @@ import { focusSidebarRenameInput } from "@/lib/sidebar/sidebarRenameFocus";
import { ensureSqlExtension, stripSqlExtension } from "@/lib/savedSql/savedSqlFileName";
import { savedSqlErrorMessage } from "@/lib/savedSql/savedSqlErrors";
import { useSavedSqlStore } from "@/stores/savedSqlStore";
import { isXuguPublicSynonymTreeNode } from "@/lib/sidebar/xuguPublicSynonyms";
import { isXuguPublicSynonymTreeNode, isXuguSchedulerJobTreeNode, xuguSchemaDisplayName } from "@/lib/sidebar/xuguPublicSynonyms";
// --- Drag and Drop ---
import { useDragSort } from "@/composables/useDragSort";
import { sidebarTreeRuntimeKey } from "@/lib/sidebar/sidebarTreeRuntime";
@@ -239,6 +239,7 @@ function getIconInfo(node: TreeNode): { icon: any; colorClass: string } | null {
case "schema": {
const databaseType = node.connectionId ? effectiveDatabaseTypeForConnection(connectionStore.getConfig(node.connectionId)) : undefined;
if (isXuguPublicSynonymTreeNode(databaseType, node.type, node.schema)) return { icon: Link2, colorClass: "text-sky-500" };
if (isXuguSchedulerJobTreeNode(databaseType, node.type, node.schema)) return { icon: CalendarClock, colorClass: "text-primary" };
return { icon: FolderOpen, colorClass: "text-sky-400" };
}
case "table":
@@ -344,6 +345,8 @@ function getIconInfo(node: TreeNode): { icon: any; colorClass: string } | null {
return { icon: ListTree, colorClass: "text-emerald-500" };
case "synonym":
return { icon: Link2, colorClass: "text-sky-500" };
case "job":
return { icon: Clock, colorClass: "text-orange-400" };
case "package":
return { icon: Package, colorClass: "text-cyan-500" };
case "package-body":
@@ -370,6 +373,8 @@ function getIconInfo(node: TreeNode): { icon: any; colorClass: string } | null {
return { icon: ListTree, colorClass: "text-emerald-500" };
case "group-synonyms":
return { icon: Link2, colorClass: "text-sky-500" };
case "group-jobs":
return { icon: Clock, colorClass: "text-orange-400" };
case "group-packages":
return { icon: Package, colorClass: "text-cyan-500" };
case "group-types":
@@ -392,6 +397,13 @@ function isGroupLabel(node: TreeNode): boolean {
}
function displayLabel(node: TreeNode): string {
// Synthetic Xugu scopes are persisted with their reserved protocol value.
// Resolve them at render time as well, so an already-cached tree never
// exposes that implementation detail after the feature is introduced.
if (node.type === "schema" && node.connectionId) {
const databaseType = effectiveDatabaseTypeForConnection(connectionStore.getConfig(node.connectionId));
if (databaseType === "xugu") return xuguSchemaDisplayName(node.schema ?? node.label);
}
if (node.type === "load-more") return t(node.label);
if (node.type === "object-browser") return t(node.label, { count: node.objectCount ?? 0 });
// Use the canonical key for persisted trees created before this label was
@@ -1485,6 +1497,7 @@ function onKeydown(event: KeyboardEvent) {
node.type === 'group-events' ||
node.type === 'group-sequences' ||
node.type === 'group-synonyms' ||
node.type === 'group-jobs' ||
node.type === 'group-packages' ||
node.type === 'group-types' ||
node.type === 'group-partitions' ||
@@ -15,7 +15,7 @@ function functionSource(name: string): string {
describe("Xugu public synonym actions", () => {
it("returns a read-only schema menu before default and destructive actions", () => {
const menu = functionSource("buildDatabaseSidebarMenu");
const readOnlyGuard = menu.indexOf("isXuguPublicSynonymTreeNode");
const readOnlyGuard = menu.indexOf("isXuguSyntheticTreeNode");
expect(readOnlyGuard).toBeGreaterThan(-1);
expect(readOnlyGuard).toBeLessThan(menu.indexOf("contextMenu.setDefaultSchema"));
@@ -23,8 +23,8 @@ describe("Xugu public synonym actions", () => {
});
it("guards both the drop request and confirmed execution", () => {
expect(functionSource("dropSchema")).toContain("isXuguPublicSynonymTreeNode");
expect(functionSource("confirmDropSchema")).toContain("isXuguPublicSynonymTreeNode");
expect(functionSource("dropSchema")).toContain("isXuguSyntheticTreeNode");
expect(functionSource("confirmDropSchema")).toContain("isXuguSyntheticTreeNode");
});
it("uses a distinct link icon for the database-global synonym scope", () => {