feat(xugu): discover and manage scheduler jobs
This commit is contained in:
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' ||
|
||||
|
||||
+3
-3
@@ -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", () => {
|
||||
|
||||
Reference in new issue
Block a user