Agent - Step Actions (#2779)

* fixed right click context menu bug in jobs view

* added stepInfo and edit job WIP

* show jobs in job edit

* added schedule description on select schedule

* fetch schedules during history and show in edit job

* added alerts to job histories and show in edit

* made history calls async

* filter menus now close when esc is pressed

* fixed bug where clicking on error row wouldnt populate job details

* added functionality to delete steps in a job

* added real time adding steps in edit job
This commit is contained in:
Aditya Bist
2018-10-09 10:28:55 -07:00
committed by GitHub
parent 7aa2ee08bf
commit c800e70ec1
18 changed files with 190 additions and 77 deletions

View File

@@ -6,37 +6,47 @@
import { AgentUtils } from '../agentUtils';
import { IAgentDialogData, AgentDialogMode } from '../interfaces';
import { JobData } from './jobData';
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
export class JobStepData implements IAgentDialogData {
// Error Messages
private readonly CreateStepErrorMessage_JobNameIsEmpty = localize('stepData.jobNameRequired', 'Job name must be provided');
private readonly CreateStepErrorMessage_StepNameIsEmpty = localize('stepData.stepNameRequired', 'Step name must be provided');
public dialogMode: AgentDialogMode = AgentDialogMode.CREATE;
public ownerUri: string;
public jobId: string; //
public jobId: string;
public jobName: string;
public script: string; //
public script: string;
public scriptName: string;
public stepName: string; //
public subSystem: string; //
public stepName: string;
public subSystem: string;
public id: number;
public failureAction: string; //
public successAction: string; //
public failureAction: string;
public successAction: string;
public failStepId: number;
public successStepId: number;
public command: string;
public commandExecutionSuccessCode: number;
public databaseName: string; //
public databaseName: string;
public databaseUserName: string;
public server: string;
public outputFileName: string; //
public outputFileName: string;
public appendToLogFile: boolean;
public appendToStepHist: boolean;
public writeLogToTable: boolean;
public appendLogToTable: boolean;
public retryAttempts: number; //
public retryInterval: number; //
public retryAttempts: number;
public retryInterval: number;
public proxyName: string;
constructor(ownerUri:string) {
constructor(ownerUri:string, jobModel?: JobData) {
this.ownerUri = ownerUri;
this.jobName = jobModel.name;
}
public async initialize() {
@@ -51,7 +61,7 @@ export class JobStepData implements IAgentDialogData {
scriptName: this.scriptName,
stepName: this.stepName,
subSystem: this.subSystem,
id: 1,
id: this.id,
failureAction: this.failureAction,
successAction: this.successAction,
failStepId: this.failStepId,
@@ -70,7 +80,26 @@ export class JobStepData implements IAgentDialogData {
retryInterval: this.retryInterval,
proxyName: this.proxyName
}).then(result => {
console.info(result);
if (result && result.success) {
console.info(result);
}
});
}
public validate(): { valid: boolean, errorMessages: string[] } {
let validationErrors: string[] = [];
if (!(this.stepName && this.stepName.trim())) {
validationErrors.push(this.CreateStepErrorMessage_StepNameIsEmpty);
}
if (!(this.jobName && this.jobName.trim())) {
validationErrors.push(this.CreateStepErrorMessage_JobNameIsEmpty);
}
return {
valid: validationErrors.length === 0,
errorMessages: validationErrors
};
}
}

View File

@@ -49,10 +49,8 @@ export abstract class AgentDialog<T extends IAgentDialogData> {
protected async execute() {
this.updateModel();
let success = await this.model.save();
if (success) {
this._onSuccess.fire(this.model);
}
await this.model.save();
this._onSuccess.fire(this.model);
}
protected async cancel() {

View File

@@ -10,6 +10,7 @@ import { JobStepDialog } from './jobStepDialog';
import { PickScheduleDialog } from './pickScheduleDialog';
import { AlertDialog } from './alertDialog';
import { AgentDialog } from './agentDialog';
import { AgentUtils } from '../agentUtils';
const localize = nls.loadMessageBundle();
@@ -234,10 +235,14 @@ export class JobDialog extends AgentDialog<JobData> {
width: 80
}).component();
let stepDialog = new JobStepDialog(this.model.ownerUri, '' , data.length + 1, this.model);
stepDialog.onSuccess((step) => {
this.model.jobSteps.push(step);
this.stepsTable.data = this.convertStepsToData(this.model.jobSteps);
});
this.newStepButton.onDidClick((e)=>{
if (this.nameTextBox.value && this.nameTextBox.value.length > 0) {
let stepDialog = new JobStepDialog(this.model.ownerUri, this.nameTextBox.value, '' , 1, this.model);
stepDialog.openNewStepDialog();
stepDialog.openDialog();
} else {
this.dialog.message = { text: this.BlankJobNameErrorText };
}
@@ -273,9 +278,17 @@ export class JobDialog extends AgentDialog<JobData> {
});
this.deleteStepButton.onDidClick((e) => {
// implement delete steps
AgentUtils.getAgentService().then((agentService) => {
let steps = this.model.jobSteps ? this.model.jobSteps : [];
agentService.deleteJobStep(this.ownerUri, stepData).then((result) => {
if (result && result.success) {
delete steps[rowNumber];
this.model.jobSteps = steps;
let data = this.convertStepsToData(steps);
this.stepsTable.data = data;
}
});
});
});
}
});
@@ -529,7 +542,6 @@ export class JobDialog extends AgentDialog<JobData> {
let result = [];
alerts.forEach(alert => {
let cols = [];
console.log(alert);
cols.push(alert.name);
cols.push(alert.isEnabled);
cols.push(alert.alertType.toString());

View File

@@ -9,11 +9,12 @@ import * as vscode from 'vscode';
import { JobStepData } from '../data/jobStepData';
import { AgentUtils } from '../agentUtils';
import { JobData } from '../data/jobData';
import { AgentDialog } from './agentDialog';
const path = require('path');
const localize = nls.loadMessageBundle();
export class JobStepDialog {
export class JobStepDialog extends AgentDialog<JobStepData> {
// TODO: localize
// Top level
@@ -67,7 +68,6 @@ export class JobStepDialog {
// UI Components
// Dialogs
private dialog: sqlops.window.modelviewdialog.Dialog;
private fileBrowserDialog: sqlops.window.modelviewdialog.Dialog;
// Dialog tabs
@@ -105,35 +105,27 @@ export class JobStepDialog {
private fileBrowserTree: sqlops.FileBrowserTreeComponent;
private jobModel: JobData;
private model: JobStepData;
private ownerUri: string;
private jobName: string;
private server: string;
private stepId: number;
constructor(
ownerUri: string,
jobName: string,
server: string,
stepId: number,
jobModel?: JobData
) {
this.model = new JobStepData(ownerUri);
super(ownerUri, new JobStepData(ownerUri, jobModel), 'New Step');
this.stepId = stepId;
this.ownerUri = ownerUri;
this.jobName = jobName;
this.jobName = jobModel.name;
this.server = server;
this.jobModel = jobModel;
}
private initializeUIComponents() {
this.dialog = sqlops.window.modelviewdialog.createDialog(this.DialogTitle);
this.generalTab = sqlops.window.modelviewdialog.createTab(this.GeneralTabText);
this.advancedTab = sqlops.window.modelviewdialog.createTab(this.AdvancedTabText);
this.dialog.content = [this.generalTab, this.advancedTab];
this.dialog.okButton.onClick(async () => await this.execute());
this.dialog.okButton.label = this.OkButtonText;
this.dialog.cancelButton.label = this.CancelButtonText;
}
private createCommands(view, queryProvider: sqlops.QueryProvider) {
@@ -478,7 +470,7 @@ export class JobStepDialog {
return outputFileForm;
}
protected execute() {
protected updateModel() {
this.model.stepName = this.nameTextBox.value;
if (!this.model.stepName || this.model.stepName.length === 0) {
this.dialog.message = this.dialog.message = { text: this.BlankStepNameErrorText };
@@ -499,12 +491,20 @@ export class JobStepDialog {
this.model.appendToLogFile = this.appendToExistingFileCheckbox.checked;
}
public async openNewStepDialog() {
public async initializeDialog() {
let databases = await AgentUtils.getDatabases(this.ownerUri);
let queryProvider = await AgentUtils.getQueryProvider();
this.initializeUIComponents();
this.createGeneralTab(databases, queryProvider);
this.createAdvancedTab();
sqlops.window.modelviewdialog.openDialog(this.dialog);
this.dialog.registerCloseValidator(() => {
this.updateModel();
let validationResult = this.model.validate();
if (!validationResult.valid) {
// TODO: Show Error Messages
console.error(validationResult.errorMessages.join(','));
}
return validationResult.valid;
});
}
}

View File

@@ -74,7 +74,6 @@ export class PickScheduleDialog {
let data: any[][] = [];
for (let i = 0; i < this.model.schedules.length; ++i) {
let schedule = this.model.schedules[i];
console.log(schedule);
data[i] = [ schedule.id, schedule.name, schedule.description ];
}
this.schedulesTable.data = data;

View File

@@ -13,5 +13,5 @@ export enum AgentDialogMode {
export interface IAgentDialogData {
dialogMode: AgentDialogMode;
initialize(): void;
save(): void;
save(): Promise<void>;
}

View File

@@ -40,9 +40,9 @@ export class MainController {
let dialog = new JobDialog(ownerUri, jobInfo);
dialog.openDialog();
});
vscode.commands.registerCommand('agent.openNewStepDialog', (ownerUri: string, jobId: string, server: string, stepId: number) => {
let dialog = new JobStepDialog(ownerUri, jobId, server, stepId);
dialog.openNewStepDialog();
vscode.commands.registerCommand('agent.openNewStepDialog', (ownerUri: string, server: string, stepId: number) => {
let dialog = new JobStepDialog(ownerUri, server, stepId);
dialog.openDialog();
});
vscode.commands.registerCommand('agent.openPickScheduleDialog', (ownerUri: string) => {
let dialog = new PickScheduleDialog(ownerUri);
@@ -57,9 +57,8 @@ export class MainController {
dialog.openDialog();
});
vscode.commands.registerCommand('agent.openProxyDialog', (ownerUri: string, proxyInfo: sqlops.AgentProxyInfo, credentials: sqlops.CredentialInfo[]) => {
//@TODO: reenable create proxy after snapping July release (7/14/18)
// let dialog = new ProxyDialog(ownerUri, proxyInfo, credentials);
// dialog.openDialog();
let dialog = new ProxyDialog(ownerUri, proxyInfo, credentials);
dialog.openDialog();
MainController.showNotYetImplemented();
});
}

View File

@@ -25,6 +25,8 @@ export interface IJobManagementService {
getJobHistory(connectionUri: string, jobID: string): Thenable<sqlops.AgentJobHistoryResult>;
deleteJob(connectionUri: string, job: sqlops.AgentJobInfo): Thenable<sqlops.ResultStatus>;
deleteJobStep(connectionUri: string, step: sqlops.AgentJobStepInfo): Thenable<sqlops.ResultStatus>;
getAlerts(connectionUri: string): Thenable<sqlops.AgentAlertsResult>;
deleteAlert(connectionUri: string, alert: sqlops.AgentAlertInfo): Thenable<sqlops.ResultStatus>;

View File

@@ -26,6 +26,7 @@ export enum JobActions {
export interface IJobActionInfo {
ownerUri: string;
targetObject: any;
jobHistoryComponent?: JobHistoryComponent;
}
// Job actions
@@ -230,6 +231,48 @@ export class NewStepAction extends Action {
}
}
export class DeleteStepAction extends Action {
public static ID = 'jobaction.deleteStep';
public static LABEL = nls.localize('jobaction.deleteStep', "Delete Step");
constructor(
@INotificationService private _notificationService: INotificationService,
@IJobManagementService private _jobService: IJobManagementService,
@IInstantiationService private instantationService: IInstantiationService
) {
super(DeleteStepAction.ID, DeleteStepAction.LABEL);
}
public run(actionInfo: IJobActionInfo): TPromise<boolean> {
let self = this;
let step = actionInfo.targetObject as sqlops.AgentJobStepInfo;
let refreshAction = this.instantationService.createInstance(JobsRefreshAction);
self._notificationService.prompt(
Severity.Info,
nls.localize('jobaction.deleteStepConfirm,', "Are you sure you'd like to delete the step '{0}'?", step.stepName),
[{
label: DeleteStepAction.LABEL,
run: () => {
self._jobService.deleteJobStep(actionInfo.ownerUri, actionInfo.targetObject).then(result => {
if (!result || !result.success) {
let errorMessage = nls.localize("jobaction.failedToDeleteStep", "Could not delete step '{0}'.\nError: {1}",
step.stepName, result.errorMessage ? result.errorMessage : 'Unknown error');
self._notificationService.error(errorMessage);
} else {
refreshAction.run(actionInfo.jobHistoryComponent);
}
});
}
}, {
label: DeleteAlertAction.CancelLabel,
run: () => { }
}]
);
return TPromise.as(true);
}
}
// Alert Actions
export class NewAlertAction extends Action {

View File

@@ -30,6 +30,7 @@ export class JobManagementService implements IJobManagementService {
this._onDidChange.fire(void 0);
}
// Jobs
public getJobs(connectionUri: string): Thenable<sqlops.AgentJobsResult> {
return this._runAction(connectionUri, (runner) => {
return runner.getJobs(connectionUri);
@@ -42,6 +43,27 @@ export class JobManagementService implements IJobManagementService {
});
}
public getJobHistory(connectionUri: string, jobID: string): Thenable<sqlops.AgentJobHistoryResult> {
return this._runAction(connectionUri, (runner) => {
return runner.getJobHistory(connectionUri, jobID);
});
}
public jobAction(connectionUri: string, jobName: string, action: string): Thenable<sqlops.ResultStatus> {
return this._runAction(connectionUri, (runner) => {
return runner.jobAction(connectionUri, jobName, action);
});
}
// Steps
public deleteJobStep(connectionUri: string, stepInfo: sqlops.AgentJobStepInfo): Thenable<sqlops.ResultStatus> {
return this._runAction(connectionUri, (runner) => {
return runner.deleteJobStep(connectionUri, stepInfo);
});
}
// Alerts
public getAlerts(connectionUri: string): Thenable<sqlops.AgentAlertsResult> {
return this._runAction(connectionUri, (runner) => {
return runner.getAlerts(connectionUri);
@@ -54,6 +76,7 @@ export class JobManagementService implements IJobManagementService {
});
}
// Operators
public getOperators(connectionUri: string): Thenable<sqlops.AgentOperatorsResult> {
return this._runAction(connectionUri, (runner) => {
return runner.getOperators(connectionUri);
@@ -66,6 +89,7 @@ export class JobManagementService implements IJobManagementService {
});
}
// Proxies
public getProxies(connectionUri: string): Thenable<sqlops.AgentProxiesResult> {
return this._runAction(connectionUri, (runner) => {
return runner.getProxies(connectionUri);
@@ -84,18 +108,6 @@ export class JobManagementService implements IJobManagementService {
});
}
public getJobHistory(connectionUri: string, jobID: string): Thenable<sqlops.AgentJobHistoryResult> {
return this._runAction(connectionUri, (runner) => {
return runner.getJobHistory(connectionUri, jobID);
});
}
public jobAction(connectionUri: string, jobName: string, action: string): Thenable<sqlops.ResultStatus> {
return this._runAction(connectionUri, (runner) => {
return runner.jobAction(connectionUri, jobName, action);
});
}
private _runAction<T>(uri: string, action: (handler: sqlops.AgentServicesProvider) => Thenable<T>): Thenable<T> {
let providerId: string = this._connectionService.getProviderIdFromUri(uri);

View File

@@ -74,7 +74,6 @@ export class JobHistoryComponent extends JobManagementView implements OnInit {
@Inject(forwardRef(() => CommonServiceInterface)) commonService: CommonServiceInterface,
@Inject(forwardRef(() => AgentViewComponent)) private _agentViewComponent: AgentViewComponent,
@Inject(IWorkbenchThemeService) private themeService: IWorkbenchThemeService,
@Inject(INotificationService) private _notificationService: INotificationService,
@Inject(IInstantiationService) private instantiationService: IInstantiationService,
@Inject(IContextMenuService) private contextMenuService: IContextMenuService,
@Inject(IJobManagementService) private _jobManagementService: IJobManagementService,

View File

@@ -20,7 +20,7 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { TabChild } from 'sql/base/browser/ui/panel/tab.component';
import * as dom from 'vs/base/browser/dom';
import { IJobManagementService } from 'sql/parts/jobManagement/common/interfaces';
export const JOBSTEPSVIEW_SELECTOR: string = 'jobstepsview-component';
@@ -36,7 +36,7 @@ export class JobStepsViewComponent extends JobManagementView implements OnInit,
private _treeDataSource = new JobStepsViewDataSource();
private _treeRenderer = new JobStepsViewRenderer();
private _treeFilter = new JobStepsViewFilter();
private static _pageSize = 1024;
private _pageSize = 1024;
@ViewChild('table') private _tableContainer: ElementRef;
@@ -66,7 +66,7 @@ export class JobStepsViewComponent extends JobManagementView implements OnInit,
}, { verticalScrollMode: ScrollbarVisibility.Visible });
this._register(attachListStyler(this._tree, this.themeService));
}
this._tree.layout(500);
this._tree.layout(this._pageSize);
this._tree.setInput(new JobStepsViewModel());
$('jobstepsview-component .steps-tree .monaco-tree').attr('tabIndex', '-1');
$('jobstepsview-component .steps-tree .monaco-tree-row').attr('tabIndex', '0');

View File

@@ -10,7 +10,6 @@ import * as TreeDefaults from 'vs/base/parts/tree/browser/treeDefaults';
import { Promise, TPromise } from 'vs/base/common/winjs.base';
import { IMouseEvent } from 'vs/base/browser/mouseEvent';
import { generateUuid } from 'vs/base/common/uuid';
import { AgentJobHistoryInfo } from 'sqlops';
import { JobManagementUtilities } from 'sql/parts/jobManagement/common/jobManagementUtilities';
export class JobStepsViewRow {
@@ -27,7 +26,6 @@ export class JobStepsViewModel {
}
export class JobStepsViewController extends TreeDefaults.DefaultController {
private _jobHistories: AgentJobHistoryInfo[];
protected onLeftClick(tree: tree.ITree, element: JobStepsViewRow, event: IMouseEvent, origin: string = 'mouse'): boolean {
return true;
@@ -37,14 +35,6 @@ export class JobStepsViewController extends TreeDefaults.DefaultController {
return true;
}
public set jobHistories(value: AgentJobHistoryInfo[]) {
this._jobHistories = value;
}
public get jobHistories(): AgentJobHistoryInfo[] {
return this._jobHistories;
}
}
export class JobStepsViewDataSource implements tree.IDataSource {

View File

@@ -584,7 +584,12 @@ export class JobsViewComponent extends JobManagementView implements OnInit {
self.jobHistories[job.jobId] = result.jobs;
self._jobCacheObject.setJobHistory(job.jobId, result.jobs);
let jobHistories = self._jobCacheObject.getJobHistory(job.jobId);
let previousRuns = jobHistories.slice(jobHistories.length - 5, jobHistories.length);
let previousRuns: sqlops.AgentJobHistoryInfo[];
if (jobHistories.length >= 5) {
previousRuns = jobHistories.slice(jobHistories.length - 5, jobHistories.length);
} else {
previousRuns = jobHistories;
}
self.createJobChart(job.jobId, previousRuns);
if (self._agentViewComponent.expanded.has(job.jobId)) {
let lastJobHistory = jobHistories[result.jobs.length - 1];
@@ -645,12 +650,22 @@ export class JobsViewComponent extends JobManagementView implements OnInit {
maxDuration = maxDuration === 0 ? 1 : maxDuration;
let maxBarHeight: number = 24;
let chartHeights = [];
let zeroDurationJobCount = 0;
for (let i = 0; i < jobHistories.length; i++) {
let duration = jobHistories[i].runDuration;
let chartHeight = (maxBarHeight * JobManagementUtilities.convertDurationToSeconds(duration)) / maxDuration;
chartHeights.push(`${chartHeight}px`);
if (chartHeight === 0) {
zeroDurationJobCount++;
}
}
// if the durations are all 0 secs, show minimal chart
// instead of nothing
if (zeroDurationJobCount === jobHistories.length) {
return ['5px', '5px', '5px', '5px', '5px'];
} else {
return chartHeights;
}
return chartHeights;
}
private expandJobs(start: boolean): void {

6
src/sql/sqlops.d.ts vendored
View File

@@ -1537,9 +1537,9 @@ declare module 'sqlops' {
getJobDefaults(ownerUri: string): Thenable<AgentJobDefaultsResult>;
// Job Step management methods
createJobStep(ownerUri: string, jobInfo: AgentJobStepInfo): Thenable<CreateAgentJobStepResult>;
updateJobStep(ownerUri: string, originalJobStepName: string, jobInfo: AgentJobStepInfo): Thenable<UpdateAgentJobStepResult>;
deleteJobStep(ownerUri: string, jobInfo: AgentJobStepInfo): Thenable<ResultStatus>;
createJobStep(ownerUri: string, stepInfo: AgentJobStepInfo): Thenable<CreateAgentJobStepResult>;
updateJobStep(ownerUri: string, originalJobStepName: string, stepInfo: AgentJobStepInfo): Thenable<UpdateAgentJobStepResult>;
deleteJobStep(ownerUri: string, stepInfo: AgentJobStepInfo): Thenable<ResultStatus>;
// Alert management methods
getAlerts(ownerUri: string): Thenable<AgentAlertsResult>;

View File

@@ -596,6 +596,13 @@ export class ExtHostDataProtocol extends ExtHostDataProtocolShape {
throw this._resolveProvider<sqlops.AgentServicesProvider>(handle).deleteJob(ownerUri, job);
}
/**
* Deletes a job step
*/
$deleteJobStep(handle: number, ownerUri: string, step: sqlops.AgentJobStepInfo): Thenable<sqlops.ResultStatus> {
throw this._resolveProvider<sqlops.AgentServicesProvider>(handle).deleteJobStep(ownerUri, step);
}
/**
* Get Agent Alerts list
*/

View File

@@ -359,6 +359,9 @@ export class MainThreadDataProtocol implements MainThreadDataProtocolShape {
deleteJob(connectionUri: string, jobInfo: sqlops.AgentJobInfo): Thenable<sqlops.ResultStatus> {
return self._proxy.$deleteJob(handle, connectionUri, jobInfo);
},
deleteJobStep(connectionUri: string, stepInfo: sqlops.AgentJobStepInfo): Thenable<sqlops.ResultStatus> {
return self._proxy.$deleteJobStep(handle, connectionUri, stepInfo);
},
getAlerts(connectionUri: string): Thenable<sqlops.AgentAlertsResult> {
return self._proxy.$getAlerts(handle, connectionUri);
},

View File

@@ -370,6 +370,11 @@ export abstract class ExtHostDataProtocolShape {
*/
$deleteJob(handle: number, ownerUri: string, job: sqlops.AgentJobInfo): Thenable<sqlops.ResultStatus> { throw ni(); }
/**
* Deletes a job step
*/
$deleteJobStep(handle: number, ownerUri: string, step: sqlops.AgentJobStepInfo): Thenable<sqlops.ResultStatus> { throw ni(); }
/**
* Get Agent Alerts list
*/