Agent Notebooks Scheduler (#6786)

* added agent notebooks, notebook history view and view materialized notebook button

* Got a basic UI running for viewing notebook history

* made some changes to make UI look good

* Added new notebook dialog

* Added new notebook Dialog

* Added create notebook dialog

* Added edit and delete notebook job

* Added some notebook history features

* Added new notebook job icons, fixed a minor bug
in openmaterializednotebookAPI and added fixed the
schedule Picker API.

* Fixed Bugs in Notebook Grid expansion

* Fixed Notebook table highlighting and
grid generation is done using code.

* fixed some UI bugs

* Added changes to reflect sqltoolservice api

* Fixed some localize keys

* Made changes in the PR and added
ability to open Template Notebooks from
notebook history view.

* Added pin and renaming to notebook history

* made some library calls async

* fixed an import bug caused by merging from master

* Validation in NotebookJobDialog

* Added entry points for scheduling notebooks
on file explorer and notebook editor

* Handled no active connections and
a small bug in collapsing grid

* fix a bug in scheduling notebook from explorer
and toolbar

* setting up agent providers from connection now

* changed modals

* Reupload edited template

* Add dialog info, solved an edit bug and localized
UI strings.

* Bug fixes in UI, notebook renaming and
editing template on fly.

* fixed a bug that failed editing notebook jobs from notebook jobs table

* Fixed a cyclic dependency, made strings const and
some other changes in the PR

* Made some cyclic dependency and some fixes from PR

* made some changes mentioned in the PR

* Changed storage database health text

* Changed the sqltoolservice version to the point to the latest build.
This commit is contained in:
Aasim Khan
2019-09-04 15:12:35 -07:00
committed by GitHub
parent 0a393400b2
commit fbb2accacb
48 changed files with 3948 additions and 149 deletions

View File

@@ -1,62 +1,93 @@
{
"name": "agent",
"displayName": "SQL Server Agent",
"description": "Manage and troubleshoot SQL Server Agent jobs",
"version": "0.42.0",
"publisher": "Microsoft",
"preview": true,
"license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/master/LICENSE.txt",
"icon": "images/sqlserver.png",
"aiKey": "AIF-5574968e-856d-40d2-af67-c89a14e76412",
"engines": {
"vscode": "^1.25.0"
},
"activationEvents": [
"*"
],
"main": "./out/main",
"repository": {
"type": "git",
"url": "https://github.com/Microsoft/azuredatastudio.git"
},
"extensionDependencies": [
"Microsoft.mssql"
],
"contributes": {
"outputChannels": [
"sqlagent"
],
"dashboard.tabs": [
{
"id": "data-management-agent",
"description": "Manage and troubleshoot SQL Agent jobs",
"provider": "MSSQL",
"title": "SQL Agent",
"when": "connectionProvider == 'MSSQL' && !mssql:iscloud",
"container": {
"controlhost-container": {
"type": "agent"
}
}
}
]
},
"dependencies": {
"vscode-nls": "^3.2.1"
},
"devDependencies": {
"mocha-junit-reporter": "^1.17.0",
"mocha-multi-reporters": "^1.1.7",
"@types/mocha": "^5.2.5",
"@types/node": "^8.10.25",
"mocha": "^5.2.0",
"should": "^13.2.1",
"typemoq": "^2.1.0",
"vscode": "1.1.5"
},
"__metadata": {
"id": "10",
"publisherDisplayName": "Microsoft",
"publisherId": "Microsoft"
}
"name": "agent",
"displayName": "SQL Server Agent",
"description": "Manage and troubleshoot SQL Server Agent jobs",
"version": "0.41.0",
"publisher": "Microsoft",
"preview": true,
"license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/master/LICENSE.txt",
"icon": "images/sqlserver.png",
"aiKey": "AIF-5574968e-856d-40d2-af67-c89a14e76412",
"engines": {
"vscode": "^1.25.0"
},
"activationEvents": [
"*"
],
"main": "./out/main",
"repository": {
"type": "git",
"url": "https://github.com/Microsoft/azuredatastudio.git"
},
"extensionDependencies": [
"Microsoft.mssql"
],
"contributes": {
"outputChannels": [
"sqlagent"
],
"dashboard.tabs": [
{
"id": "data-management-agent",
"description": "Manage and troubleshoot SQL Agent jobs",
"provider": "MSSQL",
"title": "SQL Agent",
"when": "connectionProvider == 'MSSQL' && !mssql:iscloud",
"container": {
"controlhost-container": {
"type": "agent"
}
}
}
],
"commands": [
{
"command": "agent.openNotebookDialog",
"title": "Schedule Notebook",
"icon": {
"dark": "resources/dark/open_notebook_inverse.svg",
"light": "resources/light/open_notebook.svg"
}
},
{
"command": "agent.reuploadTemplate",
"title": "Reupload Template",
"icon": {
"dark": "resources/dark/open_notebook_inverse.svg",
"light": "resources/light/open_notebook.svg"
}
}
],
"menus": {
"notebook/toolbar": [
{
"command": "agent.openNotebookDialog",
"when": "providerId == sql"
},
{
"command": "agent.reuploadTemplate",
"when": "agent:trackedTemplate"
}
],
"explorer/context": [
{
"command": "agent.openNotebookDialog",
"when": "resourceExtname == .ipynb"
}
]
}
},
"dependencies": {
"vscode-nls": "^3.2.1"
},
"devDependencies": {
"mocha-junit-reporter": "^1.17.0",
"mocha-multi-reporters": "^1.1.7",
"@types/mocha": "^5.2.5",
"@types/node": "^8.10.25",
"mocha": "^5.2.0",
"should": "^13.2.1",
"typemoq": "^2.1.0",
"vscode": "1.1.5"
}
}

View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{fill:#fff;}</style></defs><title>notebook_inverse</title><path class="cls-1" d="M15.46,2V15H.46V2h2V1h3a4.19,4.19,0,0,1,1.32.21A3.87,3.87,0,0,1,8,1.84a3.87,3.87,0,0,1,1.18-.63A4.19,4.19,0,0,1,10.46,1h3V2Zm-14,12h6.3a4.43,4.43,0,0,0-.51-.43,3.41,3.41,0,0,0-.54-.31,2.74,2.74,0,0,0-.59-.2A3.2,3.2,0,0,0,5.46,13h-3V3h-1Zm2-2h2a4.18,4.18,0,0,1,1,.13,4,4,0,0,1,1,.39V2.72a3,3,0,0,0-.94-.54A3.15,3.15,0,0,0,5.46,2h-2Zm11-9h-1V13h-3a3.2,3.2,0,0,0-.67.07,2.74,2.74,0,0,0-.59.2,3.41,3.41,0,0,0-.54.31,4.43,4.43,0,0,0-.51.43h6.3Zm-4-1a3.15,3.15,0,0,0-1.06.18,3,3,0,0,0-.94.54v9.8a4,4,0,0,1,1-.39,4.18,4.18,0,0,1,1-.13h2V2Z"/></svg>

After

Width:  |  Height:  |  Size: 734 B

View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{fill:#fff;}.cls-2{fill:#0095d7;}</style></defs><title>open_notebook_inverse</title><path class="cls-1" d="M12.55,4.21l-.08-.11h-.56l-.69.06a1.54,1.54,0,0,0-.23.29v8.69H9.18a3.32,3.32,0,0,0-.93.13,3.34,3.34,0,0,0-.87.34V4.76a2.88,2.88,0,0,1,.43-.31A5.58,5.58,0,0,1,8.29,3.3a2.63,2.63,0,0,0-.3.09A3.62,3.62,0,0,0,6.93,4a3.68,3.68,0,0,0-1.07-.57A3.58,3.58,0,0,0,4.67,3.2H2v.9H.15V15.85H13.72V5.48ZM2.86,4.1H4.67a2.61,2.61,0,0,1,1,.17,2.32,2.32,0,0,1,.86.49v8.85a3.27,3.27,0,0,0-.88-.34,3.22,3.22,0,0,0-.93-.13H2.86ZM1,15V5H2v9H4.67a3.94,3.94,0,0,1,.61.06,3.2,3.2,0,0,1,.52.18,4.19,4.19,0,0,1,.49.29,2.28,2.28,0,0,1,.45.39ZM12.8,15H7.11a2.7,2.7,0,0,1,.47-.39A2.83,2.83,0,0,1,8,14.28a3.42,3.42,0,0,1,.54-.18A3.81,3.81,0,0,1,9.18,14h2.73V5h.89Z"/><polygon class="cls-2" points="13.2 3.56 13.2 3.58 13.19 3.57 13.2 3.56"/><path class="cls-2" d="M13.19,3.57h0v0Z"/><polygon class="cls-2" points="13.2 3.56 13.2 3.58 13.19 3.57 13.2 3.56"/><polygon class="cls-2" points="14.21 1.65 14.19 1.65 14.19 1.63 14.21 1.65"/><path class="cls-2" d="M15.91,2.1,14.2,3.81l-.38.38-.62-.61v0l1-1H12.79a3.35,3.35,0,0,0-1.09.26h0a3.94,3.94,0,0,0-.86.52l-.24.21s0,0,0,0a3.3,3.3,0,0,0-.51.67,3.1,3.1,0,0,0-.26.47A3.41,3.41,0,0,0,9.5,6.11H8.6a4.68,4.68,0,0,1,.16-1.19A4.74,4.74,0,0,1,9,4.26a2.21,2.21,0,0,1,.2-.41,4.66,4.66,0,0,1,.36-.51c.1-.13.22-.26.34-.39a4.14,4.14,0,0,1,.66-.53,1.19,1.19,0,0,1,.23-.16,2.79,2.79,0,0,1,.34-.18l.31-.13.42-.14a4.32,4.32,0,0,1,1.19-.16h1.15l-1-1L13.82,0Z"/></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><title>notebook</title><path d="M15.5,2V15H.5V2h2V1h3a4.19,4.19,0,0,1,1.32.21A3.87,3.87,0,0,1,8,1.84a3.87,3.87,0,0,1,1.18-.63A4.19,4.19,0,0,1,10.5,1h3V2ZM1.5,14H7.8a4.43,4.43,0,0,0-.51-.43,3.41,3.41,0,0,0-.54-.31,2.74,2.74,0,0,0-.59-.2A3.2,3.2,0,0,0,5.5,13h-3V3h-1Zm2-2h2a4.18,4.18,0,0,1,1,.13,4,4,0,0,1,1,.39V2.72a3,3,0,0,0-.94-.54A3.15,3.15,0,0,0,5.5,2h-2Zm11-9h-1V13h-3a3.2,3.2,0,0,0-.67.07,2.74,2.74,0,0,0-.59.2,3.41,3.41,0,0,0-.54.31A4.43,4.43,0,0,0,8.2,14h6.3Zm-4-1a3.15,3.15,0,0,0-1.06.18,3,3,0,0,0-.94.54v9.8a4,4,0,0,1,1-.39,4.18,4.18,0,0,1,1-.13h2V2Z"/></svg>

After

Width:  |  Height:  |  Size: 661 B

View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{fill:#00539c;}</style></defs><title>open_notebook</title><path d="M12.4,4.21l-.08-.11h-.56l-.69.06a1.54,1.54,0,0,0-.23.29v8.69H9a3.32,3.32,0,0,0-.93.13,3.34,3.34,0,0,0-.87.34V4.76a2.88,2.88,0,0,1,.43-.31A5.58,5.58,0,0,1,8.14,3.3a2.63,2.63,0,0,0-.3.09A3.62,3.62,0,0,0,6.78,4a3.68,3.68,0,0,0-1.07-.57A3.58,3.58,0,0,0,4.52,3.2H1.81v.9H0V15.85H13.57V5.48ZM2.71,4.1H4.52a2.61,2.61,0,0,1,1,.17,2.32,2.32,0,0,1,.86.49v8.85a3.27,3.27,0,0,0-.88-.34,3.22,3.22,0,0,0-.93-.13H2.71ZM.9,15V5h.91v9H4.52a3.94,3.94,0,0,1,.61.06,3.2,3.2,0,0,1,.52.18,4.19,4.19,0,0,1,.49.29,2.28,2.28,0,0,1,.45.39Zm11.75,0H7a2.7,2.7,0,0,1,.47-.39,2.83,2.83,0,0,1,.47-.29,3.42,3.42,0,0,1,.54-.18A3.81,3.81,0,0,1,9,14h2.73V5h.89Z"/><polygon class="cls-1" points="13.05 3.56 13.05 3.58 13.04 3.57 13.05 3.56"/><path class="cls-1" d="M13,3.57h0v0Z"/><polygon class="cls-1" points="13.05 3.56 13.05 3.58 13.04 3.57 13.05 3.56"/><polygon class="cls-1" points="14.06 1.65 14.04 1.65 14.04 1.63 14.06 1.65"/><path class="cls-1" d="M15.76,2.1,14,3.81l-.38.38L13,3.58v0l1-1H12.64a3.35,3.35,0,0,0-1.09.26h0a3.94,3.94,0,0,0-.86.52l-.24.21s0,0,0,0a3.3,3.3,0,0,0-.51.67,3.1,3.1,0,0,0-.26.47,3.41,3.41,0,0,0-.27,1.39h-.9a4.68,4.68,0,0,1,.16-1.19,4.74,4.74,0,0,1,.25-.66,2.21,2.21,0,0,1,.2-.41,4.66,4.66,0,0,1,.36-.51c.1-.13.22-.26.34-.39a4.14,4.14,0,0,1,.66-.53,1.19,1.19,0,0,1,.23-.16A2.79,2.79,0,0,1,11,2.08l.31-.13.42-.14a4.32,4.32,0,0,1,1.19-.16h1.15l-1-1L13.67,0Z"/></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -6,6 +6,9 @@
'use strict';
import * as azdata from 'azdata';
import * as fs from 'fs';
import { promisify } from 'util';
export class AgentUtils {
@@ -13,6 +16,12 @@ export class AgentUtils {
private static _connectionService: azdata.ConnectionProvider;
private static _queryProvider: azdata.QueryProvider;
public static async setupProvidersFromConnection(connection?: azdata.connection.Connection) {
this._agentService = azdata.dataprotocol.getProvider<azdata.AgentServicesProvider>(connection.providerName, azdata.DataProviderType.AgentServicesProvider);
this._connectionService = azdata.dataprotocol.getProvider<azdata.ConnectionProvider>(connection.providerName, azdata.DataProviderType.ConnectionProvider);
this._queryProvider = azdata.dataprotocol.getProvider<azdata.QueryProvider>(connection.providerName, azdata.DataProviderType.QueryProvider);
}
public static async getAgentService(): Promise<azdata.AgentServicesProvider> {
if (!AgentUtils._agentService) {
let currentConnection = await azdata.connection.getCurrentConnection();
@@ -41,4 +50,20 @@ export class AgentUtils {
return this._queryProvider;
}
}
}
export function exists(path: string): Promise<boolean> {
return promisify(fs.exists)(path);
}
export function mkdir(path: string): Promise<void> {
return promisify(fs.mkdir)(path);
}
export function unlink(path: string): Promise<void> {
return promisify(fs.unlink)(path);
}
export function writeFile(path: string, data: string): Promise<void> {
return promisify(fs.writeFile)(path, data);
}

View File

@@ -0,0 +1,260 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as nls from 'vscode-nls';
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import * as fs from 'fs';
import * as path from 'path';
import { AgentUtils } from '../agentUtils';
import { IAgentDialogData, AgentDialogMode } from '../interfaces';
import { NotebookDialogOptions } from '../dialogs/notebookDialog';
import { createConnection } from 'net';
const localize = nls.loadMessageBundle();
const NotebookCompletionActionCondition_Always: string = localize('notebookData.whenJobCompletes', 'When the notebook completes');
const NotebookCompletionActionCondition_OnFailure: string = localize('notebookData.whenJobFails', 'When the notebook fails');
const NotebookCompletionActionCondition_OnSuccess: string = localize('notebookData.whenJobSucceeds', 'When the notebook succeeds');
// Error Messages
const CreateNotebookErrorMessage_NameIsEmpty = localize('notebookData.jobNameRequired', 'Notebook name must be provided');
const TemplatePathEmptyErrorMessage = localize('notebookData.templatePathRequired', 'Template path must be provided');
const InvalidNotebookPathErrorMessage = localize('notebookData.invalidNotebookPath', 'Invalid notebook path');
const SelectStorageDatabaseErrorMessage = localize('notebookData.selectStorageDatabase', 'Select storage database');
const SelectExecutionDatabaseErrorMessage = localize('notebookData.selectExecutionDatabase', 'Select execution database');
const JobWithSameNameExistsErrorMessage = localize('notebookData.jobExists', 'Job with similar name already exists');
export class NotebookData implements IAgentDialogData {
private _ownerUri: string;
private _jobCategories: string[];
private _operators: string[];
private _defaultOwner: string;
private _jobCompletionActionConditions: azdata.CategoryValue[];
private _jobCategoryIdsMap: azdata.AgentJobCategory[];
public dialogMode: AgentDialogMode = AgentDialogMode.CREATE;
public name: string;
public originalName: string;
public enabled: boolean = true;
public description: string;
public category: string;
public categoryId: number;
public owner: string;
public emailLevel: azdata.JobCompletionActionCondition = azdata.JobCompletionActionCondition.OnFailure;
public pageLevel: azdata.JobCompletionActionCondition = azdata.JobCompletionActionCondition.OnFailure;
public eventLogLevel: azdata.JobCompletionActionCondition = azdata.JobCompletionActionCondition.OnFailure;
public deleteLevel: azdata.JobCompletionActionCondition = azdata.JobCompletionActionCondition.OnSuccess;
public operatorToEmail: string;
public operatorToPage: string;
public jobSteps: azdata.AgentJobStepInfo[];
public jobSchedules: azdata.AgentJobScheduleInfo[];
public alerts: azdata.AgentAlertInfo[];
public jobId: string;
public startStepId: number;
public categoryType: number;
public targetDatabase: string;
public executeDatabase: string;
public templateId: number;
public templatePath: string;
public static jobLists: azdata.AgentJobInfo[];
public connection: azdata.connection.Connection;
constructor(
ownerUri: string,
options: NotebookDialogOptions = undefined,
private _agentService: azdata.AgentServicesProvider = undefined) {
this._ownerUri = ownerUri;
this.enabled = true;
if (options.notebookInfo) {
let notebookInfo = options.notebookInfo;
this.dialogMode = AgentDialogMode.EDIT;
this.name = notebookInfo.name;
this.originalName = notebookInfo.name;
this.owner = notebookInfo.owner;
this.category = notebookInfo.category;
this.description = notebookInfo.description;
this.enabled = notebookInfo.enabled;
this.jobSteps = notebookInfo.jobSteps;
this.jobSchedules = notebookInfo.jobSchedules;
this.alerts = notebookInfo.alerts;
this.jobId = notebookInfo.jobId;
this.startStepId = notebookInfo.startStepId;
this.categoryId = notebookInfo.categoryId;
this.categoryType = notebookInfo.categoryType;
this.targetDatabase = notebookInfo.targetDatabase;
this.executeDatabase = notebookInfo.executeDatabase;
}
if (options.filePath) {
this.name = path.basename(options.filePath).split('.').slice(0, -1).join('.');
this.templatePath = options.filePath;
}
if (options.connection) {
this.connection = options.connection;
}
}
public get jobCategories(): string[] {
return this._jobCategories;
}
public get jobCategoryIdsMap(): azdata.AgentJobCategory[] {
return this._jobCategoryIdsMap;
}
public get operators(): string[] {
return this._operators;
}
public get ownerUri(): string {
return this._ownerUri;
}
public get defaultOwner(): string {
return this._defaultOwner;
}
public get JobCompletionActionConditions(): azdata.CategoryValue[] {
return this._jobCompletionActionConditions;
}
public async initialize() {
if (this.connection) {
await AgentUtils.setupProvidersFromConnection(this.connection);
}
this._agentService = await AgentUtils.getAgentService();
let jobDefaults = await this._agentService.getJobDefaults(this.ownerUri);
if (jobDefaults && jobDefaults.success) {
this._jobCategories = jobDefaults.categories.map((cat) => {
return cat.name;
});
this._jobCategoryIdsMap = jobDefaults.categories;
this._defaultOwner = jobDefaults.owner;
this._operators = ['', this._defaultOwner];
this.owner = this.owner ? this.owner : this._defaultOwner;
}
this._jobCompletionActionConditions = [{
displayName: NotebookCompletionActionCondition_OnSuccess,
name: azdata.JobCompletionActionCondition.OnSuccess.toString()
}, {
displayName: NotebookCompletionActionCondition_OnFailure,
name: azdata.JobCompletionActionCondition.OnFailure.toString()
}, {
displayName: NotebookCompletionActionCondition_Always,
name: azdata.JobCompletionActionCondition.Always.toString()
}];
this._agentService.getJobs(this.ownerUri).then((value) => {
NotebookData.jobLists = value.jobs;
});
}
public async save() {
let notebookInfo: azdata.AgentNotebookInfo = this.toAgentJobInfo();
let result = this.dialogMode === AgentDialogMode.CREATE
? await this._agentService.createNotebook(this.ownerUri, notebookInfo, this.templatePath)
: await this._agentService.updateNotebook(this.ownerUri, this.originalName, notebookInfo, this.templatePath);
if (!result || !result.success) {
if (this.dialogMode === AgentDialogMode.EDIT) {
vscode.window.showErrorMessage(
localize('notebookData.saveErrorMessage', "Notebook update failed '{0}'", result.errorMessage ? result.errorMessage : 'Unknown'));
} else {
vscode.window.showErrorMessage(
localize('notebookData.newJobErrorMessage', "Notebook creation failed '{0}'", result.errorMessage ? result.errorMessage : 'Unknown'));
}
} else {
if (this.dialogMode === AgentDialogMode.EDIT) {
vscode.window.showInformationMessage(
localize('notebookData.saveSucessMessage', "Notebook '{0}' updated successfully", notebookInfo.name));
} else {
vscode.window.showInformationMessage(
localize('notebookData.newJobSuccessMessage', "Notebook '{0}' created successfully", notebookInfo.name));
}
}
}
public validate(): { valid: boolean, errorMessages: string[] } {
let validationErrors: string[] = [];
if (this.dialogMode !== AgentDialogMode.EDIT) {
if (!(this.name && this.name.trim())) {
validationErrors.push(CreateNotebookErrorMessage_NameIsEmpty);
}
if (!(this.templatePath && this.name.trim())) {
validationErrors.push(TemplatePathEmptyErrorMessage);
}
if (!fs.existsSync(this.templatePath)) {
validationErrors.push(InvalidNotebookPathErrorMessage);
}
if (NotebookData.jobLists) {
for (let i = 0; i < NotebookData.jobLists.length; i++) {
if (this.name === NotebookData.jobLists[i].name) {
validationErrors.push(JobWithSameNameExistsErrorMessage);
break;
}
}
}
}
else {
if (this.templatePath && this.templatePath !== '' && !fs.existsSync(this.templatePath)) {
validationErrors.push(InvalidNotebookPathErrorMessage);
}
}
if (this.targetDatabase === 'Select Database') {
validationErrors.push(SelectStorageDatabaseErrorMessage);
}
if (this.executeDatabase === 'Select Database') {
validationErrors.push(SelectExecutionDatabaseErrorMessage);
}
return {
valid: validationErrors.length === 0,
errorMessages: validationErrors
};
}
public toAgentJobInfo(): azdata.AgentNotebookInfo {
return {
name: this.name,
owner: this.owner ? this.owner : this.defaultOwner,
description: this.description,
emailLevel: this.emailLevel,
pageLevel: this.pageLevel,
eventLogLevel: this.eventLogLevel,
deleteLevel: this.deleteLevel,
operatorToEmail: this.operatorToEmail,
operatorToPage: this.operatorToPage,
enabled: this.enabled,
category: this.category,
alerts: this.alerts,
jobSchedules: this.jobSchedules,
jobSteps: this.jobSteps,
targetDatabase: this.targetDatabase,
executeDatabase: this.executeDatabase,
// The properties below are not collected from UI
// We could consider using a seperate class for create job request
//
templateId: this.templateId,
currentExecutionStatus: 0,
lastRunOutcome: 0,
currentExecutionStep: '',
hasTarget: true,
hasSchedule: false,
hasStep: false,
runnable: true,
categoryId: this.categoryId,
categoryType: this.categoryType,
lastRun: '',
nextRun: '',
jobId: this.jobId,
startStepId: this.startStepId,
lastRunNotebookError: '',
};
}
}

View File

@@ -169,7 +169,7 @@ export class JobStepDialog extends AgentDialog<JobStepData> {
isFile: false
}).component();
this.openButton.onDidClick(e => {
let queryContent = e;
let queryContent = e.fileContent;
this.commandTextBox.value = queryContent;
});
this.parseButton.onDidClick(e => {

View File

@@ -0,0 +1,340 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as nls from 'vscode-nls';
import * as path from 'path';
import * as azdata from 'azdata';
import { PickScheduleDialog } from './pickScheduleDialog';
import { AgentDialog } from './agentDialog';
import { AgentUtils } from '../agentUtils';
import { NotebookData } from '../data/notebookData';
const localize = nls.loadMessageBundle();
// TODO: localize
// Top level
const CreateDialogTitle: string = localize('notebookDialog.newJob', "New Notebook Job");
const EditDialogTitle: string = localize('notebookDialog.editJob', "Edit Notebook Job");
const GeneralTabText: string = localize('notebookDialog.general', "General");
const BlankJobNameErrorText: string = localize('notebookDialog.blankJobNameError', "The name of the job cannot be blank.");
// Notebook details strings
const NotebookDetailsSeparatorTitle: string = localize('notebookDialog.notebookSection', "Notebook Details");
const TemplateNotebookTextBoxLabel: string = localize('notebookDialog.templateNotebook', "Notebook Path");
const TargetDatabaseDropdownLabel: string = localize('notebookDialog.targetDatabase', "Storage Database");
const ExecuteDatabaseDropdownLabel: string = localize('notebookDialog.executeDatabase', "Execution Database");
const DefaultDropdownString: string = localize('notebookDialog.defaultDropdownString', "Select Database");
// Job details string
const JobDetailsSeparatorTitle: string = localize('notebookDialog.jobSection', "Job Details");
const NameTextBoxLabel: string = localize('notebookDialog.name', "Name");
const OwnerTextBoxLabel: string = localize('notebookDialog.owner', "Owner");
const SchedulesTopLabelString: string = localize('notebookDialog.schedulesaLabel', "Schedules list");
const PickScheduleButtonString: string = localize('notebookDialog.pickSchedule', "Pick Schedule");
const RemoveScheduleButtonString: string = localize('notebookDialog.removeSchedule', "Remove Schedule");
const ScheduleNameLabelString: string = localize('notebookDialog.scheduleNameLabel', "Schedule Name");
const DescriptionTextBoxLabel: string = localize('notebookDialog.description', "Description");
// Event Name strings
const NewJobDialogEvent: string = 'NewNotebookJobDialogOpened';
const EditJobDialogEvent: string = 'EditNotebookJobDialogOpened';
export class NotebookDialogOptions {
notebookInfo?: azdata.AgentNotebookInfo;
filePath?: string;
connection?: azdata.connection.Connection;
}
export class NotebookDialog extends AgentDialog<NotebookData> {
// UI Components
private generalTab: azdata.window.DialogTab;
// Notebook Details controls
private templateFilePathBox: azdata.InputBoxComponent;
private openTemplateFileButton: azdata.ButtonComponent;
private targetDatabaseDropDown: azdata.DropDownComponent;
private executeDatabaseDropDown: azdata.DropDownComponent;
// Job Details controls
private nameTextBox: azdata.InputBoxComponent;
private ownerTextBox: azdata.InputBoxComponent;
private schedulesTable: azdata.TableComponent;
private pickScheduleButton: azdata.ButtonComponent;
private removeScheduleButton: azdata.ButtonComponent;
private descriptionTextBox: azdata.InputBoxComponent;
private isEdit: boolean = false;
// Job objects
private steps: azdata.AgentJobStepInfo[];
private schedules: azdata.AgentJobScheduleInfo[];
constructor(ownerUri: string, options: NotebookDialogOptions = undefined) {
super(
ownerUri,
new NotebookData(ownerUri, options),
options.notebookInfo ? EditDialogTitle : CreateDialogTitle);
this.steps = this.model.jobSteps ? this.model.jobSteps : [];
this.schedules = this.model.jobSchedules ? this.model.jobSchedules : [];
this.isEdit = options.notebookInfo ? true : false;
this.dialogName = this.isEdit ? EditJobDialogEvent : NewJobDialogEvent;
}
protected async initializeDialog() {
this.generalTab = azdata.window.createTab(GeneralTabText);
this.initializeGeneralTab();
this.dialog.content = [this.generalTab];
this.dialog.registerCloseValidator(() => {
this.updateModel();
let validationResult = this.model.validate();
if (!validationResult.valid) {
// TODO: Show Error Messages
this.dialog.message = { text: validationResult.errorMessages[0] };
console.error(validationResult.errorMessages.join(','));
}
return validationResult.valid;
});
}
private initializeGeneralTab() {
this.generalTab.registerContent(async view => {
this.templateFilePathBox = view.modelBuilder.inputBox()
.withProperties({
width: 400,
inputType: 'text'
}).component();
this.openTemplateFileButton = view.modelBuilder.button()
.withProperties({
label: '...',
title: '...',
width: '20px',
isFile: true,
fileType: '.ipynb'
}).component();
this.openTemplateFileButton.onDidClick(e => {
if (e) {
this.templateFilePathBox.value = e.filePath;
if (!this.isEdit) {
let fileName = path.basename(e.filePath).split('.').slice(0, -1).join('.');
this.nameTextBox.value = fileName;
}
}
});
let outputButtonContainer = view.modelBuilder.flexContainer()
.withLayout({
flexFlow: 'row',
textAlign: 'right',
width: 20
}).withItems([this.openTemplateFileButton], { flex: '1 1 80%' }).component();
let notebookPathFlexBox = view.modelBuilder.flexContainer()
.withLayout({
flexFlow: 'row',
width: '100%',
}).withItems([this.templateFilePathBox, outputButtonContainer], {
flex: '1 1 50%'
}).component();
this.targetDatabaseDropDown = view.modelBuilder.dropDown().component();
this.executeDatabaseDropDown = view.modelBuilder.dropDown().component();
let databases = await AgentUtils.getDatabases(this.ownerUri);
databases.unshift(DefaultDropdownString);
this.targetDatabaseDropDown = view.modelBuilder.dropDown()
.withProperties({
value: databases[0],
values: databases
}).component();
this.descriptionTextBox = view.modelBuilder.inputBox().withProperties({
multiline: true,
height: 50
}).component();
this.executeDatabaseDropDown = view.modelBuilder.dropDown()
.withProperties({
value: databases[0],
values: databases
}).component();
this.targetDatabaseDropDown.required = true;
this.executeDatabaseDropDown.required = true;
this.descriptionTextBox = view.modelBuilder.inputBox().withProperties({
multiline: true,
height: 50
}).component();
this.nameTextBox = view.modelBuilder.inputBox().component();
this.nameTextBox.required = true;
this.nameTextBox.onTextChanged(() => {
if (this.nameTextBox.value && this.nameTextBox.value.length > 0) {
this.dialog.message = null;
// Change the job name immediately since steps
// depends on the job name
this.model.name = this.nameTextBox.value;
}
});
this.ownerTextBox = view.modelBuilder.inputBox().component();
this.schedulesTable = view.modelBuilder.table()
.withProperties({
columns: [
PickScheduleDialog.SchedulesIDText,
PickScheduleDialog.ScheduleNameLabelText,
PickScheduleDialog.ScheduleDescription
],
data: [],
height: 50,
width: 420
}).component();
this.pickScheduleButton = view.modelBuilder.button().withProperties({
label: PickScheduleButtonString,
width: 100
}).component();
this.removeScheduleButton = view.modelBuilder.button().withProperties({
label: RemoveScheduleButtonString,
width: 100
}).component();
this.pickScheduleButton.onDidClick(() => {
let pickScheduleDialog = new PickScheduleDialog(this.model.ownerUri, this.model.name);
pickScheduleDialog.onSuccess((dialogModel) => {
let selectedSchedule = dialogModel.selectedSchedule;
if (selectedSchedule) {
let existingSchedule = this.schedules.find(item => item.name === selectedSchedule.name);
if (!existingSchedule) {
selectedSchedule.jobName = this.model.name ? this.model.name : this.nameTextBox.value;
this.schedules.push(selectedSchedule);
}
this.populateScheduleTable();
}
});
pickScheduleDialog.showDialog();
});
this.removeScheduleButton.onDidClick(() => {
if (this.schedulesTable.selectedRows.length === 1) {
let selectedRow = this.schedulesTable.selectedRows[0];
let selectedScheduleName = this.schedulesTable.data[selectedRow][1];
for (let i = 0; i < this.schedules.length; i++) {
if (this.schedules[i].name === selectedScheduleName) {
this.schedules.splice(i, 1);
}
}
this.populateScheduleTable();
}
});
let formModel = view.modelBuilder.formContainer()
.withFormItems([
{
components: [{
component: notebookPathFlexBox,
title: TemplateNotebookTextBoxLabel,
layout: {
info: localize('notebookDialog.templatePath', 'Select a notebook to schedule from PC')
}
},
{
component: this.targetDatabaseDropDown,
title: TargetDatabaseDropdownLabel,
layout: {
info: localize('notebookDialog.targetDatabaseInfo', 'Select a database to store all notebook job metadata and results')
}
}, {
component: this.executeDatabaseDropDown,
title: ExecuteDatabaseDropdownLabel,
layout: {
info: localize('notebookDialog.executionDatabaseInfo', 'Select a database against which notebook queries will run')
}
}],
title: NotebookDetailsSeparatorTitle
}, {
components: [{
component: this.nameTextBox,
title: NameTextBoxLabel
}, {
component: this.ownerTextBox,
title: OwnerTextBoxLabel
}, {
component: this.schedulesTable,
title: SchedulesTopLabelString,
actions: [this.pickScheduleButton, this.removeScheduleButton]
}, {
component: this.descriptionTextBox,
title: DescriptionTextBoxLabel
}],
title: JobDetailsSeparatorTitle
}]).withLayout({ width: '100%' }).component();
await view.initializeModel(formModel);
this.nameTextBox.value = this.model.name;
this.ownerTextBox.value = this.model.owner;
this.templateFilePathBox.value = this.model.templatePath;
if (this.isEdit) {
this.templateFilePathBox.placeHolder = this.model.targetDatabase + '\\' + this.model.name;
this.targetDatabaseDropDown.value = this.model.targetDatabase;
this.executeDatabaseDropDown.value = this.model.executeDatabase;
this.targetDatabaseDropDown.enabled = false;
this.schedules = this.model.jobSchedules;
}
else {
this.templateFilePathBox.required = true;
}
let idx: number = undefined;
if (this.model.category && this.model.category !== '') {
idx = this.model.jobCategories.indexOf(this.model.category);
}
this.descriptionTextBox.value = this.model.description;
this.openTemplateFileButton.onDidClick(e => {
});
this.populateScheduleTable();
});
}
private populateScheduleTable() {
let data = this.convertSchedulesToData(this.schedules);
this.schedulesTable.data = data;
this.schedulesTable.height = 100;
}
private createRowContainer(view: azdata.ModelView): azdata.FlexBuilder {
return view.modelBuilder.flexContainer().withLayout({
flexFlow: 'row',
alignItems: 'left',
justifyContent: 'space-between'
});
}
private convertSchedulesToData(jobSchedules: azdata.AgentJobScheduleInfo[]): any[][] {
let result = [];
jobSchedules.forEach(schedule => {
let cols = [];
cols.push(schedule.id);
cols.push(schedule.name);
cols.push(schedule.description);
result.push(cols);
});
return result;
}
protected updateModel() {
this.model.name = this.nameTextBox.value;
this.model.owner = this.ownerTextBox.value;
this.model.description = this.descriptionTextBox.value;
this.model.templatePath = this.templateFilePathBox.value;
this.model.targetDatabase = this.targetDatabaseDropDown.value as string;
this.model.executeDatabase = this.executeDatabaseDropDown.value as string;
if (!this.model.jobSchedules) {
this.model.jobSchedules = [];
}
this.model.alerts = [];
this.model.jobSteps = [];
this.model.jobSchedules = this.schedules;
this.model.category = '[Uncategorized (Local)]';
this.model.categoryId = 0;
this.model.eventLogLevel = 0;
}
}

View File

@@ -82,9 +82,22 @@ export class PickScheduleDialog {
}]).withLayout({ width: '100%' }).component();
this.loadingComponent = view.modelBuilder.loadingComponent().withItem(formModel).component();
this.loadingComponent.loading = true;
this.model.initialize().then((result) => {
this.loadingComponent.loading = false;
if (this.model.schedules) {
let data: any[][] = [];
for (let i = 0; i < this.model.schedules.length; ++i) {
let schedule = this.model.schedules[i];
data[i] = [schedule.id, schedule.name, schedule.description];
}
this.schedulesTable.data = data;
}
});
this.loadingComponent.loading = !this.model.isInitialized();
await view.initializeModel(this.loadingComponent);
});
}
private async execute() {

View File

@@ -7,6 +7,9 @@
import * as nls from 'vscode-nls';
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { AlertDialog } from './dialogs/alertDialog';
import { JobDialog } from './dialogs/jobDialog';
import { OperatorDialog } from './dialogs/operatorDialog';
@@ -14,13 +17,22 @@ import { ProxyDialog } from './dialogs/proxyDialog';
import { JobStepDialog } from './dialogs/jobStepDialog';
import { PickScheduleDialog } from './dialogs/pickScheduleDialog';
import { JobData } from './data/jobData';
import { AgentUtils } from './agentUtils';
import { AgentUtils, exists, mkdir, unlink, writeFile } from './agentUtils';
import { NotebookDialog, NotebookDialogOptions } from './dialogs/notebookDialog';
import { promisify } from 'util';
const localize = nls.loadMessageBundle();
/**
* The main controller class that initializes the extension
*/
export class TemplateMapObject {
notebookInfo: azdata.AgentNotebookInfo;
fileUri: vscode.Uri;
tempPath: string;
ownerUri: string;
}
export class MainController {
protected _context: vscode.ExtensionContext;
@@ -29,7 +41,8 @@ export class MainController {
private alertDialog: AlertDialog;
private operatorDialog: OperatorDialog;
private proxyDialog: ProxyDialog;
private notebookDialog: NotebookDialog;
private notebookTemplateMap = new Map<string, TemplateMapObject>();
// PUBLIC METHODS //////////////////////////////////////////////////////
public constructor(context: vscode.ExtensionContext) {
this._context = context;
@@ -82,6 +95,26 @@ export class MainController {
this.operatorDialog.dialogName ? await this.operatorDialog.openDialog(this.operatorDialog.dialogName) : await this.operatorDialog.openDialog();
}
});
vscode.commands.registerCommand('agent.reuploadTemplate', async (ownerUri: string, operatorInfo: azdata.AgentOperatorInfo) => {
let nbEditor = azdata.nb.activeNotebookEditor;
// await nbEditor.document.save();
let templateMap = this.notebookTemplateMap.get(nbEditor.document.uri.toString());
let vsEditor = await vscode.workspace.openTextDocument(templateMap.fileUri);
let content = vsEditor.getText();
promisify(fs.writeFile)(templateMap.tempPath, content);
AgentUtils.getAgentService().then(async (agentService) => {
let result = await agentService.updateNotebook(templateMap.ownerUri, templateMap.notebookInfo.name, templateMap.notebookInfo, templateMap.tempPath);
if (result.success) {
vscode.window.showInformationMessage(localize('agent.templateUploadSuccessful', 'Template updated successfully'));
}
else {
vscode.window.showInformationMessage(localize('agent.templateUploadError', 'Template update failure'));
}
});
});
vscode.commands.registerCommand('agent.openProxyDialog', async (ownerUri: string, proxyInfo: azdata.AgentProxyInfo, credentials: azdata.CredentialInfo[]) => {
if (!this.proxyDialog || (this.proxyDialog && !this.proxyDialog.isOpen)) {
this.proxyDialog = new ProxyDialog(ownerUri, proxyInfo, credentials);
@@ -91,6 +124,117 @@ export class MainController {
}
this.proxyDialog.dialogName ? await this.proxyDialog.openDialog(this.proxyDialog.dialogName) : await this.proxyDialog.openDialog();
});
vscode.commands.registerCommand('agent.openNotebookEditorFromJsonString', async (filename: string, jsonNotebook: string, notebookInfo?: azdata.AgentNotebookInfo, ownerUri?: string) => {
const tempfilePath = path.join(os.tmpdir(), 'mssql_notebooks', filename + '.ipynb');
if (!await exists(path.join(os.tmpdir(), 'mssql_notebooks'))) {
await mkdir(path.join(os.tmpdir(), 'mssql_notebooks'));
}
let editors = azdata.nb.visibleNotebookEditors;
if (await exists(tempfilePath)) {
await unlink(tempfilePath);
}
try {
await writeFile(tempfilePath, jsonNotebook);
let uri = vscode.Uri.parse(`untitled:${path.basename(tempfilePath)}`);
if (notebookInfo) {
this.notebookTemplateMap.set(uri.toString(), { notebookInfo: notebookInfo, fileUri: uri, ownerUri: ownerUri, tempPath: tempfilePath });
vscode.commands.executeCommand('setContext', 'agent:trackedTemplate', true);
}
await azdata.nb.showNotebookDocument(uri, {
initialContent: jsonNotebook,
initialDirtyState: false
});
vscode.commands.executeCommand('setContext', 'agent:trackedTemplate', false);
}
catch (e) {
vscode.window.showErrorMessage(e);
}
});
vscode.commands.registerCommand('agent.openNotebookDialog', async (ownerUri: any, notebookInfo: azdata.AgentNotebookInfo) => {
/*
There are four entry points to this commands:
1. Explorer context menu:
The first arg becomes a vscode URI
the second argument is undefined
2. Notebook toolbar:
both the args are undefined
3. Agent New Notebook Action
the first arg is database OwnerUri
the second arg is undefined
4. Agent Edit Notebook Action
the first arg is database OwnerUri
the second arg is notebookInfo from database
*/
if (!ownerUri || ownerUri instanceof vscode.Uri) {
let path: string;
if (!ownerUri) {
if (azdata.nb.activeNotebookEditor.document.isDirty) {
vscode.window.showErrorMessage(localize('agent.unsavedFileSchedulingError', 'Save file before scheduling'), { modal: true });
return;
}
path = azdata.nb.activeNotebookEditor.document.fileName;
} else {
path = ownerUri.fsPath;
}
let connection = await this.getConnectionFromUser();
ownerUri = await azdata.connection.getUriForConnection(connection.connectionId);
this.notebookDialog = new NotebookDialog(ownerUri, <NotebookDialogOptions>{ filePath: path, connection: connection });
if (!this.notebookDialog.isOpen) {
this.notebookDialog.dialogName ? await this.notebookDialog.openDialog(this.notebookDialog.dialogName) : await this.notebookDialog.openDialog();
}
}
else {
if (!this.notebookDialog || (this.notebookDialog && !this.notebookDialog.isOpen)) {
this.notebookDialog = new NotebookDialog(ownerUri, <NotebookDialogOptions>{ notebookInfo: notebookInfo });
}
if (!this.notebookDialog.isOpen) {
this.notebookDialog.dialogName ? await this.notebookDialog.openDialog(this.notebookDialog.dialogName) : await this.notebookDialog.openDialog();
}
}
});
}
public async getConnectionFromUser(): Promise<azdata.connection.Connection> {
let connection: azdata.connection.Connection = null;
let connections = await azdata.connection.getActiveConnections();
if (!connections || connections.length === 0) {
connection = await azdata.connection.openConnectionDialog();
}
else {
let sqlConnectionsPresent: boolean;
for (let i = 0; i < connections.length; i++) {
if (connections[i].providerName === 'MSSQL') {
sqlConnectionsPresent = true;
break;
}
}
let connectionNames: azdata.connection.Connection[] = [];
let connectionDisplayString: string[] = [];
for (let i = 0; i < connections.length; i++) {
let currentConnectionString = connections[i].options.server + ' (' + connections[i].options.user + ')';
connectionNames.push(connections[i]);
connectionDisplayString.push(currentConnectionString);
}
connectionDisplayString.push(localize('agent.AddNewConnection', 'Add new connection'));
let connectionName = await vscode.window.showQuickPick(connectionDisplayString, { placeHolder: localize('agent.selectConnection', 'Select a connection') });
if (connectionDisplayString.indexOf(connectionName) !== -1) {
if (connectionName === localize('agent.AddNewConnection', 'Add new connection')) {
connection = await azdata.connection.openConnectionDialog();
}
else {
connection = connections[connectionDisplayString.indexOf(connectionName)];
}
}
else {
vscode.window.showErrorMessage(localize('agent.selectValidConnection', 'Please select a valid connection'), { modal: true });
}
}
return connection;
}
/**

View File

@@ -1,6 +1,6 @@
{
"downloadUrl": "https://github.com/Microsoft/sqltoolsservice/releases/download/v{#version#}/microsoft.sqltools.servicelayer-{#fileName#}",
"version": "2.0.0-release.6",
"version": "2.0.0-release.10",
"downloadFileNames": {
"Windows_86": "win-x86-netcoreapp2.2.zip",
"Windows_64": "win-x64-netcoreapp2.2.zip",

View File

@@ -87,6 +87,68 @@ export interface DeleteAgentJobStepParams {
step: azdata.AgentJobStepInfo;
}
// Notebook management parameters
export interface AgentNotebookParams {
ownerUri: string;
}
export interface AgentNotebookHistoryParams {
ownerUri: string;
jobId: string;
jobName: string;
targetDatabase: string;
}
export interface AgentNotebookMaterializedParams {
ownerUri: string;
targetDatabase: string;
notebookMaterializedId: number;
}
export interface AgentNotebookTemplateParams {
ownerUri: string;
targetDatabase: string;
jobId: string;
}
export interface CreateAgentNotebookParams {
ownerUri: string;
notebook: azdata.AgentNotebookInfo;
templateFilePath: string;
}
export interface UpdateAgentNotebookParams {
ownerUri: string;
originalNotebookName: string;
notebook: azdata.AgentJobInfo;
templateFilePath: string;
}
export interface UpdateAgentNotebookRunPinParams {
ownerUri: string;
targetDatabase: string;
agentNotebookHistory: azdata.AgentNotebookHistoryInfo;
materializedNotebookPin: boolean;
}
export interface UpdateAgentNotebookRunNameParams {
ownerUri: string;
targetDatabase: string;
agentNotebookHistory: azdata.AgentNotebookHistoryInfo;
materializedNotebookName: string;
}
export interface DeleteAgentNotebookParams {
ownerUri: string;
notebook: azdata.AgentNotebookInfo;
}
export interface DeleteAgentMaterializedNotebookParams {
ownerUri: string;
targetDatabase: string;
agentNotebookHistory: azdata.AgentNotebookHistoryInfo;
}
// Alert management parameters
export interface AgentAlertsParams {
ownerUri: string;
@@ -218,6 +280,47 @@ export namespace DeleteAgentJobStepRequest {
export const type = new RequestType<DeleteAgentJobStepParams, azdata.ResultStatus, void, void>('agent/deletejobstep');
}
// Notebooks request
export namespace AgentNotebooksRequest {
export const type = new RequestType<AgentNotebookParams, azdata.AgentNotebooksResult, void, void>('agent/notebooks');
}
export namespace AgentNotebookHistoryRequest {
export const type = new RequestType<AgentNotebookHistoryParams, azdata.AgentNotebookHistoryResult, void, void>('agent/notebookhistory');
}
export namespace AgentNotebookMaterializedRequest {
export const type = new RequestType<AgentNotebookMaterializedParams, azdata.AgentNotebookMaterializedResult, void, void>('agent/notebookmaterialized');
}
export namespace UpdateAgentNotebookRunNameRequest {
export const type = new RequestType<UpdateAgentNotebookRunNameParams, azdata.UpdateAgentNotebookResult, void, void>('agent/updatenotebookname');
}
export namespace DeleteMaterializedNotebookRequest {
export const type = new RequestType<DeleteAgentMaterializedNotebookParams, azdata.ResultStatus, void, void>('agent/deletenotebookmaterialized');
}
export namespace UpdateAgentNotebookRunPinRequest {
export const type = new RequestType<UpdateAgentNotebookRunPinParams, azdata.ResultStatus, void, void>('agent/updatenotebookpin');
}
export namespace AgentNotebookTemplateRequest {
export const type = new RequestType<AgentNotebookTemplateParams, azdata.ResultStatus, void, void>('agent/notebooktemplate');
}
export namespace CreateAgentNotebookRequest {
export const type = new RequestType<CreateAgentNotebookParams, azdata.CreateAgentNotebookResult, void, void>('agent/createnotebook');
}
export namespace DeleteAgentNotebookRequest {
export const type = new RequestType<DeleteAgentNotebookParams, azdata.ResultStatus, void, void>('agent/deletenotebook');
}
export namespace UpdateAgentNotebookRequest {
export const type = new RequestType<UpdateAgentNotebookParams, azdata.UpdateAgentNotebookResult, void, void>('agent/updatenotebook');
}
// Alerts requests
export namespace AgentAlertsRequest {
export const type = new RequestType<CreateAgentAlertParams, azdata.AgentAlertsResult, void, void>('agent/alerts');

View File

@@ -227,6 +227,151 @@ export class AgentServicesFeature extends SqlOpsFeature<undefined> {
);
};
// Notebook Management methods
const getNotebooks = (ownerUri: string): Thenable<azdata.AgentNotebooksResult> => {
let params: contracts.AgentNotebookParams = { ownerUri: ownerUri };
return client.sendRequest(contracts.AgentNotebooksRequest.type, params).then(
r => r,
e => {
client.logFailedRequest(contracts.AgentNotebooksRequest.type, e);
return Promise.resolve(undefined);
}
);
};
const getNotebookHistory = (ownerUri: string, jobID: string, jobName: string, targetDatabase: string): Thenable<azdata.AgentNotebookHistoryResult> => {
let params: contracts.AgentNotebookHistoryParams = { ownerUri: ownerUri, jobId: jobID, jobName: jobName, targetDatabase: targetDatabase };
return client.sendRequest(contracts.AgentNotebookHistoryRequest
.type, params).then(
r => r,
e => {
client.logFailedRequest(contracts.AgentNotebookHistoryRequest.type, e);
return Promise.resolve(undefined);
}
);
};
const getMaterializedNotebook = (ownerUri: string, targetDatabase: string, notebookMaterializedId: number): Thenable<azdata.AgentNotebookMaterializedResult> => {
let params: contracts.AgentNotebookMaterializedParams = { ownerUri: ownerUri, targetDatabase: targetDatabase, notebookMaterializedId: notebookMaterializedId };
return client.sendRequest(contracts.AgentNotebookMaterializedRequest
.type, params).then(
r => r,
e => {
client.logFailedRequest(contracts.AgentNotebookMaterializedRequest.type, e);
return Promise.resolve(undefined);
}
);
};
const getTemplateNotebook = (ownerUri: string, targetDatabase: string, jobId: string): Thenable<azdata.AgentNotebookTemplateResult> => {
let params: contracts.AgentNotebookTemplateParams = { ownerUri: ownerUri, targetDatabase: targetDatabase, jobId: jobId };
return client.sendRequest(contracts.AgentNotebookTemplateRequest
.type, params).then(
r => r,
e => {
client.logFailedRequest(contracts.AgentNotebookTemplateRequest.type, e);
return Promise.resolve(undefined);
}
);
};
const createNotebook = (ownerUri: string, notebookInfo: azdata.AgentNotebookInfo, templateFilePath: string): Thenable<azdata.CreateAgentNotebookResult> => {
let params: contracts.CreateAgentNotebookParams = {
ownerUri: ownerUri,
notebook: notebookInfo,
templateFilePath: templateFilePath
};
let requestType = contracts.CreateAgentNotebookRequest.type;
return client.sendRequest(requestType, params).then(
r => {
fireOnUpdated();
return r;
},
e => {
client.logFailedRequest(requestType, e);
return Promise.resolve(undefined);
}
);
};
const updateNotebook = (ownerUri: string, originalNotebookName: string, notebookInfo: azdata.AgentNotebookInfo, templateFilePath: string): Thenable<azdata.UpdateAgentNotebookResult> => {
let params: contracts.UpdateAgentNotebookParams = {
ownerUri: ownerUri,
originalNotebookName: originalNotebookName,
notebook: notebookInfo,
templateFilePath: templateFilePath
};
let requestType = contracts.UpdateAgentNotebookRequest.type;
return client.sendRequest(requestType, params).then(
r => {
fireOnUpdated();
return r;
},
e => {
client.logFailedRequest(requestType, e);
return Promise.resolve(undefined);
}
);
};
const deleteNotebook = (ownerUri: string, notebookInfo: azdata.AgentNotebookInfo): Thenable<azdata.ResultStatus> => {
let params: contracts.DeleteAgentNotebookParams = {
ownerUri: ownerUri,
notebook: notebookInfo
};
let requestType = contracts.DeleteAgentNotebookRequest.type;
return client.sendRequest(requestType, params).then(
r => {
fireOnUpdated();
return r;
},
e => {
client.logFailedRequest(requestType, e);
return Promise.resolve(undefined);
}
);
};
const deleteMaterializedNotebook = (ownerUri: string, agentNotebookHistory: azdata.AgentNotebookHistoryInfo, targetDatabase: string): Thenable<azdata.ResultStatus> => {
let params: contracts.DeleteAgentMaterializedNotebookParams = { ownerUri: ownerUri, targetDatabase: targetDatabase, agentNotebookHistory: agentNotebookHistory };
return client.sendRequest(contracts.DeleteMaterializedNotebookRequest
.type, params).then(
r => r,
e => {
client.logFailedRequest(contracts.DeleteMaterializedNotebookRequest.type, e);
return Promise.resolve(undefined);
}
);
};
const updateNotebookMaterializedName = (ownerUri: string, agentNotebookHistory: azdata.AgentNotebookHistoryInfo, targetDatabase: string, name: string): Thenable<azdata.ResultStatus> => {
let params: contracts.UpdateAgentNotebookRunNameParams = { ownerUri: ownerUri, targetDatabase: targetDatabase, agentNotebookHistory: agentNotebookHistory, materializedNotebookName: name };
return client.sendRequest(contracts.UpdateAgentNotebookRunNameRequest
.type, params).then(
r => r,
e => {
client.logFailedRequest(contracts.UpdateAgentNotebookRunNameRequest.type, e);
return Promise.resolve(undefined);
}
);
};
const updateNotebookMaterializedPin = (ownerUri: string, agentNotebookHistory: azdata.AgentNotebookHistoryInfo, targetDatabase: string, pin: boolean): Thenable<azdata.ResultStatus> => {
let params: contracts.UpdateAgentNotebookRunPinParams = { ownerUri: ownerUri, targetDatabase: targetDatabase, agentNotebookHistory: agentNotebookHistory, materializedNotebookPin: pin };
return client.sendRequest(contracts.UpdateAgentNotebookRunPinRequest
.type, params).then(
r => r,
e => {
client.logFailedRequest(contracts.UpdateAgentNotebookRunPinRequest.type, e);
return Promise.resolve(undefined);
}
);
};
// Alert management methods
let getAlerts = (ownerUri: string): Thenable<azdata.AgentAlertsResult> => {
let params: contracts.AgentAlertsParams = {
@@ -535,6 +680,16 @@ export class AgentServicesFeature extends SqlOpsFeature<undefined> {
createJobStep,
updateJobStep,
deleteJobStep,
getNotebooks,
getNotebookHistory,
getMaterializedNotebook,
getTemplateNotebook,
createNotebook,
updateNotebook,
deleteMaterializedNotebook,
updateNotebookMaterializedName,
updateNotebookMaterializedPin,
deleteNotebook,
getAlerts,
createAlert,
updateAlert,

68
src/sql/azdata.d.ts vendored
View File

@@ -1387,6 +1387,20 @@ declare module 'azdata' {
alerts: AgentAlertInfo[];
}
export interface AgentNotebookInfo extends AgentJobInfo {
templateId: number;
targetDatabase: string;
lastRunNotebookError: string;
executeDatabase: string;
}
export interface AgentNotebookMaterializedInfo {
materializedId: number;
targetDatabase: string;
materializedName: string;
favorite: boolean;
}
export interface AgentJobScheduleInfo {
id: number;
name: string;
@@ -1487,6 +1501,14 @@ declare module 'azdata' {
steps: AgentJobStep[];
}
export interface AgentNotebookHistoryInfo extends AgentJobHistoryInfo {
materializedNotebookId: number;
materializedNotebookName: string;
materializedNotebookPin: boolean;
materializedNotebookErrorInfo: string;
materializedNotebookDeleted: boolean;
}
export interface AgentProxyInfo {
id: number;
accountName: string;
@@ -1577,6 +1599,39 @@ declare module 'azdata' {
categories: AgentJobCategory[];
}
export interface AgentNotebooksResult extends ResultStatus {
notebooks: AgentNotebookInfo[];
}
export interface AgentJobHistoryResult extends ResultStatus {
histories: AgentJobHistoryInfo[];
schedules: AgentJobScheduleInfo[];
alerts: AgentAlertInfo[];
steps: AgentJobStepInfo[];
}
export interface AgentNotebookHistoryResult extends ResultStatus {
histories: AgentNotebookHistoryInfo[];
schedules: AgentJobScheduleInfo[];
steps: AgentJobStepInfo[];
}
export interface AgentNotebookMaterializedResult extends ResultStatus {
notebookMaterialized: string;
}
export interface AgentNotebookTemplateResult extends ResultStatus {
notebookTemplate: string;
}
export interface CreateAgentNotebookResult extends ResultStatus {
notebook: AgentNotebookInfo;
}
export interface UpdateAgentNotebookResult extends ResultStatus {
notebook: AgentNotebookInfo;
}
export interface CreateAgentJobStepResult extends ResultStatus {
step: AgentJobStepInfo;
}
@@ -1651,6 +1706,18 @@ declare module 'azdata' {
deleteJob(ownerUri: string, jobInfo: AgentJobInfo): Thenable<ResultStatus>;
getJobDefaults(ownerUri: string): Thenable<AgentJobDefaultsResult>;
// Notebook management methods
getNotebooks(ownerUri: string): Thenable<AgentNotebooksResult>;
getNotebookHistory(ownerUri: string, jobId: string, jobName: string, targetDatabase: string): Thenable<AgentNotebookHistoryResult>;
getMaterializedNotebook(ownerUri: string, targetDatabase: string, notebookMaterializedId: number): Thenable<AgentNotebookMaterializedResult>;
getTemplateNotebook(ownerUri: string, targetDatabase: string, jobId: string): Thenable<AgentNotebookTemplateResult>;
createNotebook(ownerUri: string, notebook: AgentNotebookInfo, templateFilePath: string): Thenable<CreateAgentNotebookResult>;
deleteNotebook(ownerUri: string, notebook: AgentNotebookInfo): Thenable<ResultStatus>;
updateNotebook(ownerUri: string, originialNotebookName: string, notebook: AgentNotebookInfo, templateFilePath: string): Thenable<UpdateAgentNotebookResult>;
updateNotebookMaterializedName(ownerUri: string, agentNotebookHistory: AgentNotebookHistoryInfo, targetDatabase: string, name: string): Thenable<ResultStatus>;
updateNotebookMaterializedPin(ownerUri: string, agentNotebookHistory: AgentNotebookHistoryInfo, targetDatabase: string, pin: boolean): Thenable<ResultStatus>;
deleteMaterializedNotebook(ownerUri: string, agentNotebookHistory: AgentNotebookHistoryInfo, targetDatabase: string): Thenable<ResultStatus>;
// Job Step management methods
createJobStep(ownerUri: string, stepInfo: AgentJobStepInfo): Thenable<CreateAgentJobStepResult>;
updateJobStep(ownerUri: string, originalJobStepName: string, stepInfo: AgentJobStepInfo): Thenable<UpdateAgentJobStepResult>;
@@ -3006,6 +3073,7 @@ declare module 'azdata' {
editable?: boolean;
fireOnTextChange?: boolean;
ariaLabel?: string;
required?: boolean;
}
export interface DeclarativeTableColumn {

View File

@@ -74,7 +74,6 @@ declare module 'azdata' {
continueSerialization(requestParams: SerializeDataContinueRequestParams): Thenable<SerializeDataResult>;
}
export namespace dataprotocol {
export function registerSerializationProvider(provider: SerializationProvider): vscode.Disposable;
}

View File

@@ -20,6 +20,8 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import * as TelemetryKeys from 'sql/platform/telemetry/common/telemetryKeys';
import { IErrorMessageService } from 'sql/platform/errorMessage/common/errorMessageService';
import { JobManagementView } from 'sql/workbench/parts/jobManagement/browser/jobManagementView';
import { NotebooksViewComponent } from 'sql/workbench/parts/jobManagement/browser/notebooksView.component';
import { NotebookHistoryComponent } from 'sql/workbench/parts/jobManagement/browser/notebookHistory.component';
export const successLabel: string = nls.localize('jobaction.successLabel', "Success");
export const errorLabel: string = nls.localize('jobaction.faillabel', "Error");
@@ -171,6 +173,22 @@ export class EditJobAction extends Action {
}
}
export class OpenMaterializedNotebookAction extends Action {
public static ID = 'notebookAction.openNotebook';
public static LABEL = nls.localize('notebookAction.openNotebook', "Open");
constructor(
@ICommandService private _commandService: ICommandService
) {
super(OpenMaterializedNotebookAction.ID, OpenMaterializedNotebookAction.LABEL, 'open');
}
public run(context: any): Promise<boolean> {
context.component.openNotebook(context.history);
return Promise.resolve(true);
}
}
export class DeleteJobAction extends Action {
public static ID = 'jobaction.deleteJob';
public static LABEL = nls.localize('jobaction.deleteJob', "Delete Job");
@@ -282,7 +300,6 @@ export class DeleteStepAction extends Action {
}
}
// Alert Actions
export class NewAlertAction extends Action {
@@ -551,3 +568,167 @@ export class DeleteProxyAction extends Action {
return Promise.resolve(true);
}
}
//Notebook Actions
export class NewNotebookJobAction extends Action {
public static ID = 'notebookaction.newJob';
public static LABEL = nls.localize('notebookaction.newJob', "New Notebook Job");
constructor(
) {
super(NewNotebookJobAction.ID, NewNotebookJobAction.LABEL, 'newStepIcon');
}
public async run(context: IJobActionInfo): Promise<boolean> {
let component = context.component as NotebooksViewComponent;
await component.openCreateNotebookDialog();
return true;
}
}
export class EditNotebookJobAction extends Action {
public static ID = 'notebookaction.editNotebook';
public static LABEL = nls.localize('notebookaction.editJob', "Edit Notebook Job");
constructor(
@ICommandService private _commandService: ICommandService
) {
super(EditNotebookJobAction.ID, EditNotebookJobAction.LABEL, 'edit');
}
public run(actionInfo: IJobActionInfo): Promise<boolean> {
this._commandService.executeCommand(
'agent.openNotebookDialog',
actionInfo.ownerUri,
actionInfo.targetObject.job);
return Promise.resolve(true);
}
}
export class OpenTemplateNotebookAction extends Action {
public static ID = 'notebookaction.openTemplate';
public static LABEL = nls.localize('notebookaction.openNotebook', "Open Template Notebook");
constructor(
@ICommandService private _commandService: ICommandService
) {
super(OpenTemplateNotebookAction.ID, OpenTemplateNotebookAction.LABEL, 'opennotebook');
}
public run(actionInfo: any): Promise<boolean> {
actionInfo.component.openTemplateNotebook();
return Promise.resolve(true);
}
}
export class DeleteNotebookAction extends Action {
public static ID = 'notebookaction.deleteNotebook';
public static LABEL = nls.localize('notebookaction.deleteNotebook', "Delete Notebook");
constructor(
@INotificationService private _notificationService: INotificationService,
@IErrorMessageService private _errorMessageService: IErrorMessageService,
@IJobManagementService private _jobService: IJobManagementService,
@IInstantiationService private instantationService: IInstantiationService,
@ITelemetryService private _telemetryService: ITelemetryService
) {
super(DeleteNotebookAction.ID, DeleteNotebookAction.LABEL);
}
public run(actionInfo: IJobActionInfo): Promise<boolean> {
let self = this;
let notebook = actionInfo.targetObject.job as azdata.AgentNotebookInfo;
let refreshAction = this.instantationService.createInstance(JobsRefreshAction);
self._notificationService.prompt(
Severity.Info,
nls.localize('jobaction.deleteNotebookConfirm', "Are you sure you'd like to delete the notebook '{0}'?", notebook.name),
[{
label: DeleteNotebookAction.LABEL,
run: () => {
this._telemetryService.publicLog(TelemetryKeys.DeleteAgentJob);
self._jobService.deleteNotebook(actionInfo.ownerUri, actionInfo.targetObject.job).then(async (result) => {
if (!result || !result.success) {
await refreshAction.run(actionInfo);
let errorMessage = nls.localize("jobaction.failedToDeleteNotebook", "Could not delete notebook '{0}'.\nError: {1}",
notebook.name, result.errorMessage ? result.errorMessage : 'Unknown error');
self._errorMessageService.showDialog(Severity.Error, errorLabel, errorMessage);
} else {
let successMessage = nls.localize('jobaction.deletedNotebook', "The notebook was successfully deleted");
self._notificationService.info(successMessage);
}
});
}
}, {
label: DeleteAlertAction.CancelLabel,
run: () => { }
}]
);
return Promise.resolve(true);
}
}
export class PinNotebookMaterializedAction extends Action {
public static ID = 'notebookaction.openTemplate';
public static LABEL = nls.localize('notebookaction.pinNotebook', "Pin");
constructor(
@ICommandService private _commandService: ICommandService
) {
super(PinNotebookMaterializedAction.ID, PinNotebookMaterializedAction.LABEL);
}
public run(actionInfo: any): Promise<boolean> {
actionInfo.component.toggleNotebookPin(actionInfo.history, true);
return Promise.resolve(true);
}
}
export class DeleteMaterializedNotebookAction extends Action {
public static ID = 'notebookaction.deleteMaterializedNotebook';
public static LABEL = nls.localize('notebookaction.deleteMaterializedNotebook', "Delete");
constructor(
@ICommandService private _commandService: ICommandService
) {
super(DeleteMaterializedNotebookAction.ID, DeleteMaterializedNotebookAction.LABEL);
}
public run(actionInfo: any): Promise<boolean> {
actionInfo.component.deleteMaterializedNotebook(actionInfo.history);
return Promise.resolve(true);
}
}
export class UnpinNotebookMaterializedAction extends Action {
public static ID = 'notebookaction.unpinNotebook';
public static LABEL = nls.localize('notebookaction.unpinNotebook', "Unpin");
constructor(
@ICommandService private _commandService: ICommandService
) {
super(UnpinNotebookMaterializedAction.ID, UnpinNotebookMaterializedAction.LABEL);
}
public run(actionInfo: any): Promise<boolean> {
actionInfo.component.toggleNotebookPin(actionInfo.history, false);
return Promise.resolve(true);
}
}
export class RenameNotebookMaterializedAction extends Action {
public static ID = 'notebookaction.openTemplate';
public static LABEL = nls.localize('notebookaction.renameNotebook', "Rename");
constructor(
@ICommandService private _commandService: ICommandService,
) {
super(RenameNotebookMaterializedAction.ID, RenameNotebookMaterializedAction.LABEL);
}
public run(actionInfo: any): Promise<boolean> {
actionInfo.component.renameNotebook(actionInfo.history);
return Promise.resolve(true);
}
}

View File

@@ -5,7 +5,7 @@
import * as azdata from 'azdata';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { JobCacheObject, AlertsCacheObject, ProxiesCacheObject, OperatorsCacheObject } from './jobManagementService';
import { JobCacheObject, AlertsCacheObject, ProxiesCacheObject, OperatorsCacheObject, NotebookCacheObject } from './jobManagementService';
import { Event } from 'vs/base/common/event';
export const SERVICE_ID = 'jobManagementService';
@@ -25,6 +25,15 @@ export interface IJobManagementService {
deleteJobStep(connectionUri: string, step: azdata.AgentJobStepInfo): Thenable<azdata.ResultStatus>;
getNotebooks(connectionUri: string): Thenable<azdata.AgentNotebooksResult>;
getNotebookHistory(connectionUri: string, jobId: string, jobName: string, targetDatabase: string): Thenable<azdata.AgentNotebookHistoryResult>;
getMaterialziedNotebook(connectionUri: string, targetDatabase: string, notebookMaterializedId: number): Thenable<azdata.AgentNotebookMaterializedResult>;
getTemplateNotebook(connectionUri: string, targetDatabase: string, jobId: string): Thenable<azdata.AgentNotebookTemplateResult>;
deleteNotebook(connectionUri: string, notebook: azdata.AgentNotebookInfo): Thenable<azdata.ResultStatus>;
deleteMaterializedNotebook(connectionUri: string, agentNotebookHistory: azdata.AgentNotebookHistoryInfo, targetDatabase: string): Thenable<azdata.ResultStatus>;
updateNotebookMaterializedName(connectionUri: string, agentNotebookHistory: azdata.AgentNotebookHistoryInfo, targetDatabase: string, name: string);
updateNotebookMaterializedPin(connectionUri: string, agentNotebookHistory: azdata.AgentNotebookHistoryInfo, targetDatabase: string, pin: boolean);
getAlerts(connectionUri: string): Thenable<azdata.AgentAlertsResult>;
deleteAlert(connectionUri: string, alert: azdata.AgentAlertInfo): Thenable<azdata.ResultStatus>;
@@ -37,10 +46,11 @@ export interface IJobManagementService {
getCredentials(connectionUri: string): Thenable<azdata.GetCredentialsResult>;
jobAction(connectionUri: string, jobName: string, action: string): Thenable<azdata.ResultStatus>;
addToCache(server: string, cache: JobCacheObject | OperatorsCacheObject);
addToCache(server: string, cache: JobCacheObject | OperatorsCacheObject | NotebookCacheObject);
jobCacheObjectMap: { [server: string]: JobCacheObject; };
notebookCacheObjectMap: { [server: string]: NotebookCacheObject; };
operatorsCacheObjectMap: { [server: string]: OperatorsCacheObject; };
alertsCacheObjectMap: { [server: string]: AlertsCacheObject; };
proxiesCacheObjectMap: { [server: string]: ProxiesCacheObject };
addToCache(server: string, cache: JobCacheObject | ProxiesCacheObject | AlertsCacheObject | OperatorsCacheObject);
}
addToCache(server: string, cache: JobCacheObject | ProxiesCacheObject | AlertsCacheObject | OperatorsCacheObject | NotebookCacheObject);
}

View File

@@ -20,7 +20,7 @@ export class JobManagementService implements IJobManagementService {
private _operatorsCacheObjectMap: { [server: string]: OperatorsCacheObject; } = {};
private _alertsCacheObject: { [server: string]: AlertsCacheObject; } = {};
private _proxiesCacheObjectMap: { [server: string]: ProxiesCacheObject; } = {};
private _notebookCacheObjectMap: { [server: string]: NotebookCacheObject; } = {};
constructor(
@IConnectionManagementService private _connectionService: IConnectionManagementService
) {
@@ -62,6 +62,54 @@ export class JobManagementService implements IJobManagementService {
});
}
// Notebooks
public getNotebooks(connectionUri: string): Thenable<azdata.AgentNotebooksResult> {
return this._runAction(connectionUri, (runner) => {
return runner.getNotebooks(connectionUri);
});
}
public getNotebookHistory(connectionUri: string, jobID: string, jobName: string, targetDatabase: string): Thenable<azdata.AgentNotebookHistoryResult> {
return this._runAction(connectionUri, (runner) => {
return runner.getNotebookHistory(connectionUri, jobID, jobName, targetDatabase);
});
}
public getMaterialziedNotebook(connectionUri: string, targetDatabase: string, notebookMaterializedId: number): Thenable<azdata.AgentNotebookMaterializedResult> {
return this._runAction(connectionUri, (runner) => {
return runner.getMaterializedNotebook(connectionUri, targetDatabase, notebookMaterializedId);
});
}
public getTemplateNotebook(connectionUri: string, targetDatabase: string, jobId: string): Thenable<azdata.AgentNotebookTemplateResult> {
return this._runAction(connectionUri, (runner) => {
return runner.getTemplateNotebook(connectionUri, targetDatabase, jobId);
});
}
public deleteNotebook(connectionUri: string, notebook: azdata.AgentNotebookInfo): Thenable<azdata.ResultStatus> {
return this._runAction(connectionUri, (runner) => {
return runner.deleteNotebook(connectionUri, notebook);
});
}
public deleteMaterializedNotebook(connectionUri: string, agentNotebookHistory: azdata.AgentNotebookHistoryInfo, targetDatabase: string): Thenable<azdata.ResultStatus> {
return this._runAction(connectionUri, (runner) => {
return runner.deleteMaterializedNotebook(connectionUri, agentNotebookHistory, targetDatabase);
});
}
public updateNotebookMaterializedName(connectionUri: string, agentNotebookHistory: azdata.AgentNotebookHistoryInfo, targetDatabase: string, name: string): Thenable<azdata.ResultStatus> {
return this._runAction(connectionUri, (runner) => {
return runner.updateNotebookMaterializedName(connectionUri, agentNotebookHistory, targetDatabase, name);
});
}
public updateNotebookMaterializedPin(connectionUri: string, agentNotebookHistory: azdata.AgentNotebookHistoryInfo, targetDatabase: string, pin: boolean): Thenable<azdata.ResultStatus> {
return this._runAction(connectionUri, (runner) => {
return runner.updateNotebookMaterializedPin(connectionUri, agentNotebookHistory, targetDatabase, pin);
});
}
// Alerts
public getAlerts(connectionUri: string): Thenable<azdata.AgentAlertsResult> {
@@ -134,6 +182,10 @@ export class JobManagementService implements IJobManagementService {
return this._alertsCacheObject;
}
public get notebookCacheObjectMap(): { [server: string]: NotebookCacheObject; } {
return this._notebookCacheObjectMap;
}
public get proxiesCacheObjectMap(): { [server: string]: ProxiesCacheObject; } {
return this._proxiesCacheObjectMap;
}
@@ -142,7 +194,7 @@ export class JobManagementService implements IJobManagementService {
return this._operatorsCacheObjectMap;
}
public addToCache(server: string, cacheObject: JobCacheObject | OperatorsCacheObject | ProxiesCacheObject | AlertsCacheObject) {
public addToCache(server: string, cacheObject: JobCacheObject | OperatorsCacheObject | ProxiesCacheObject | AlertsCacheObject | NotebookCacheObject) {
if (cacheObject instanceof JobCacheObject) {
this._jobCacheObjectMap[server] = cacheObject;
} else if (cacheObject instanceof OperatorsCacheObject) {
@@ -151,6 +203,8 @@ export class JobManagementService implements IJobManagementService {
this._alertsCacheObject[server] = cacheObject;
} else if (cacheObject instanceof ProxiesCacheObject) {
this._proxiesCacheObjectMap[server] = cacheObject;
} else if (cacheObject instanceof NotebookCacheObject) {
this._notebookCacheObjectMap[server] = cacheObject;
}
}
}
@@ -252,6 +306,94 @@ export class JobCacheObject {
this._jobSchedules[jobID] = value;
}
}
/**
* Server level caching of Operators
*/
export class NotebookCacheObject {
_serviceBrand: any;
private _notebooks: azdata.AgentNotebookInfo[] = [];
private _notebookHistories: { [jobID: string]: azdata.AgentNotebookHistoryInfo[]; } = {};
private _jobSteps: { [jobID: string]: azdata.AgentJobStepInfo[]; } = {};
private _jobSchedules: { [jobID: string]: azdata.AgentJobScheduleInfo[]; } = {};
private _runCharts: { [jobID: string]: string[]; } = {};
private _prevJobID: string;
private _serverName: string;
private _dataView: Slick.Data.DataView<any>;
/* Getters */
public get notebooks(): azdata.AgentNotebookInfo[] {
return this._notebooks;
}
public get notebookHistories(): { [jobID: string]: azdata.AgentNotebookHistoryInfo[] } {
return this._notebookHistories;
}
public get prevJobID(): string {
return this._prevJobID;
}
public getNotebookHistory(jobID: string): azdata.AgentNotebookHistoryInfo[] {
return this._notebookHistories[jobID];
}
public get serverName(): string {
return this._serverName;
}
public get dataView(): Slick.Data.DataView<any> {
return this._dataView;
}
public getRunChart(jobID: string): string[] {
return this._runCharts[jobID];
}
public getJobSteps(jobID: string): azdata.AgentJobStepInfo[] {
return this._jobSteps[jobID];
}
public getJobSchedules(jobID: string): azdata.AgentJobScheduleInfo[] {
return this._jobSchedules[jobID];
}
/* Setters */
public set notebooks(value: azdata.AgentNotebookInfo[]) {
this._notebooks = value;
}
public set notebookHistories(value: { [jobID: string]: azdata.AgentNotebookHistoryInfo[]; }) {
this._notebookHistories = value;
}
public set prevJobID(value: string) {
this._prevJobID = value;
}
public setNotebookHistory(jobID: string, value: azdata.AgentNotebookHistoryInfo[]) {
this._notebookHistories[jobID] = value;
}
public setRunChart(jobID: string, value: string[]) {
this._runCharts[jobID] = value;
}
public set serverName(value: string) {
this._serverName = value;
}
public set dataView(value: Slick.Data.DataView<any>) {
this._dataView = value;
}
public setJobSteps(jobID: string, value: azdata.AgentJobStepInfo[]) {
this._jobSteps[jobID] = value;
}
public setJobSchedules(jobID: string, value: azdata.AgentJobScheduleInfo[]) {
this._jobSchedules[jobID] = value;
}
}
/**
* Server level caching of Operators
@@ -364,4 +506,4 @@ export class ProxiesCacheObject {
public set serverName(value: string) {
this._serverName = value;
}
}
}

View File

@@ -631,6 +631,7 @@ declare module 'sqlops' {
isFile?: boolean;
fileContent?: string;
title?: string;
fileType?: string;
}
export interface LoadingComponentProperties {

View File

@@ -395,6 +395,30 @@ export class MainThreadDataProtocol extends Disposable implements MainThreadData
deleteJobStep(connectionUri: string, stepInfo: azdata.AgentJobStepInfo): Thenable<azdata.ResultStatus> {
return self._proxy.$deleteJobStep(handle, connectionUri, stepInfo);
},
getNotebooks(connectionUri: string): Thenable<azdata.AgentNotebooksResult> {
return self._proxy.$getNotebooks(handle, connectionUri);
},
getNotebookHistory(connectionUri: string, jobID: string, jobName: string, targetDatabase: string): Thenable<azdata.AgentNotebookHistoryResult> {
return self._proxy.$getNotebookHistory(handle, connectionUri, jobID, jobName, targetDatabase);
},
getMaterializedNotebook(connectionUri: string, targetDatabase: string, notebookMaterializedId: number): Thenable<azdata.AgentNotebookMaterializedResult> {
return self._proxy.$getMaterializedNotebook(handle, connectionUri, targetDatabase, notebookMaterializedId);
},
updateNotebookMaterializedName(connectionUri: string, agentNotebookHistory: azdata.AgentNotebookHistoryInfo, targetDatabase: string, name: string): Thenable<azdata.ResultStatus> {
return self._proxy.$updateNotebookMaterializedName(handle, connectionUri, agentNotebookHistory, targetDatabase, name);
},
deleteMaterializedNotebook(connectionUri: string, agentNotebookHistory: azdata.AgentNotebookHistoryInfo, targetDatabase: string): Thenable<azdata.ResultStatus> {
return self._proxy.$deleteMaterializedNotebook(handle, connectionUri, agentNotebookHistory, targetDatabase);
},
updateNotebookMaterializedPin(connectionUri: string, agentNotebookHistory: azdata.AgentNotebookHistoryInfo, targetDatabase: string, pin: boolean): Thenable<azdata.ResultStatus> {
return self._proxy.$updateNotebookMaterializedPin(handle, connectionUri, agentNotebookHistory, targetDatabase, pin);
},
getTemplateNotebook(connectionUri: string, targetDatabase: string, jobId: string): Thenable<azdata.AgentNotebookTemplateResult> {
return self._proxy.$getTemplateNotebook(handle, connectionUri, targetDatabase, jobId);
},
deleteNotebook(connectionUri: string, notebook: azdata.AgentNotebookInfo): Thenable<azdata.ResultStatus> {
return self._proxy.$deleteNotebook(handle, connectionUri, notebook);
},
getAlerts(connectionUri: string): Thenable<azdata.AgentAlertsResult> {
return self._proxy.$getAlerts(handle, connectionUri);
},

View File

@@ -98,7 +98,7 @@ class MainThreadNotebookEditor extends Disposable {
if (!input) {
return false;
}
return input === this.editor.notebookParams.input;
return input.notebookUri.toString() === this.editor.notebookParams.input.notebookUri.toString();
}
public applyEdits(versionIdCheck: number, edits: ISingleNotebookEditOperation[], opts: IUndoStopOptions): boolean {

View File

@@ -679,14 +679,14 @@ export class ExtHostDataProtocol extends ExtHostDataProtocolShape {
* Deletes a job
*/
$deleteJob(handle: number, ownerUri: string, job: azdata.AgentJobInfo): Thenable<azdata.ResultStatus> {
throw this._resolveProvider<azdata.AgentServicesProvider>(handle).deleteJob(ownerUri, job);
return this._resolveProvider<azdata.AgentServicesProvider>(handle).deleteJob(ownerUri, job);
}
/**
* Deletes a job step
*/
$deleteJobStep(handle: number, ownerUri: string, step: azdata.AgentJobStepInfo): Thenable<azdata.ResultStatus> {
throw this._resolveProvider<azdata.AgentServicesProvider>(handle).deleteJobStep(ownerUri, step);
return this._resolveProvider<azdata.AgentServicesProvider>(handle).deleteJobStep(ownerUri, step);
}
/**
@@ -703,6 +703,62 @@ export class ExtHostDataProtocol extends ExtHostDataProtocolShape {
return this._resolveProvider<azdata.AgentServicesProvider>(handle).deleteAlert(ownerUri, alert);
}
/**
* Get Agent Notebook list
*/
public $getNotebooks(handle: number, ownerUri: string): Thenable<azdata.AgentNotebooksResult> {
return this._resolveProvider<azdata.AgentServicesProvider>(handle).getNotebooks(ownerUri);
}
/**
* Get a Agent Notebook's history
*/
public $getNotebookHistory(handle: number, ownerUri: string, jobID: string, jobName: string, targetDatabase: string): Thenable<azdata.AgentNotebookHistoryResult> {
return this._resolveProvider<azdata.AgentServicesProvider>(handle).getNotebookHistory(ownerUri, jobID, jobName, targetDatabase);
}
/**
* Get a Agent Materialized Notebook
*/
public $getMaterializedNotebook(handle: number, ownerUri: string, targetDatabase: string, notebookMaterializedId: number): Thenable<azdata.AgentNotebookMaterializedResult> {
return this._resolveProvider<azdata.AgentServicesProvider>(handle).getMaterializedNotebook(ownerUri, targetDatabase, notebookMaterializedId);
}
/**
* Get a Agent Template Notebook
*/
public $getTemplateNotebook(handle: number, ownerUri: string, targetDatabase: string, jobId: string): Thenable<azdata.AgentNotebookTemplateResult> {
return this._resolveProvider<azdata.AgentServicesProvider>(handle).getTemplateNotebook(ownerUri, targetDatabase, jobId);
}
/**
* Delete a Agent Notebook
*/
public $deleteNotebook(handle: number, ownerUri: string, notebook: azdata.AgentNotebookInfo): Thenable<azdata.ResultStatus> {
return this._resolveProvider<azdata.AgentServicesProvider>(handle).deleteNotebook(ownerUri, notebook);
}
/**
* Update a Agent Materialized Notebook Name
*/
public $updateNotebookMaterializedName(handle: number, ownerUri: string, agentNotebookHistory: azdata.AgentNotebookHistoryInfo, targetDatabase: string, name: string): Thenable<azdata.ResultStatus> {
return this._resolveProvider<azdata.AgentServicesProvider>(handle).updateNotebookMaterializedName(ownerUri, agentNotebookHistory, targetDatabase, name);
}
/**
* Get a Agent Materialized Notebook
*/
public $deleteMaterializedNotebook(handle: number, ownerUri: string, agentNotebookHistory: azdata.AgentNotebookHistoryInfo, targetDatabase: string): Thenable<azdata.ResultStatus> {
return this._resolveProvider<azdata.AgentServicesProvider>(handle).deleteMaterializedNotebook(ownerUri, agentNotebookHistory, targetDatabase);
}
/**
* Update a Agent Materialized Notebook Pin
*/
public $updateNotebookMaterializedPin(handle: number, ownerUri: string, agentNotebookHistory: azdata.AgentNotebookHistoryInfo, targetDatabase: string, pin: boolean): Thenable<azdata.ResultStatus> {
return this._resolveProvider<azdata.AgentServicesProvider>(handle).updateNotebookMaterializedPin(ownerUri, agentNotebookHistory, targetDatabase, pin);
}
/**
* Get Agent Oeprators list
*/

View File

@@ -397,6 +397,47 @@ export abstract class ExtHostDataProtocolShape {
*/
$deleteJobStep(handle: number, ownerUri: string, step: azdata.AgentJobStepInfo): Thenable<azdata.ResultStatus> { throw ni(); }
/**
* Get Agent Notebook list
*/
$getNotebooks(handle: number, ownerUri: string): Thenable<azdata.AgentNotebooksResult> { throw ni(); }
/**
* Get a Agent Notebook's history
*/
$getNotebookHistory(handle: number, ownerUri: string, jobID: string, jobName: string, targetDatabase: string): Thenable<azdata.AgentNotebookHistoryResult> { throw ni(); }
/**
* Get a Agent materialized notebook
*/
$getMaterializedNotebook(handle: number, ownerUri: string, targetDatabase: string, notebookMaterializedId: number): Thenable<azdata.AgentNotebookMaterializedResult> { throw ni(); }
/**
* Get a Agent Template notebook
*/
$getTemplateNotebook(handle: number, ownerUri: string, targetDatabase: string, jobId: string): Thenable<azdata.AgentNotebookTemplateResult> { throw ni(); }
/**
* Deletes a notebook
*/
$deleteNotebook(handle: number, ownerUri: string, notebook: azdata.AgentNotebookInfo): Thenable<azdata.ResultStatus> { throw ni(); }
/**
* Update materialzied Notebook Name
*/
$updateNotebookMaterializedName(handle: number, ownerUri: string, agentNotebookHistory: azdata.AgentNotebookHistoryInfo, targetDatabase: string, name: string): Thenable<azdata.ResultStatus> { throw ni(); }
/**
* Update materialzied Notebook Name
*/
$deleteMaterializedNotebook(handle: number, ownerUri: string, agentNotebookHistory: azdata.AgentNotebookHistoryInfo, targetDatabase: string): Thenable<azdata.ResultStatus> { throw ni(); }
/**
* Update materialzied Notebook Pin
*/
$updateNotebookMaterializedPin(handle: number, ownerUri: string, agentNotebookHistory: azdata.AgentNotebookHistoryInfo, targetDatabase: string, pin: boolean): Thenable<azdata.ResultStatus> { throw ni(); }
/**
* Get Agent Alerts list
*/

View File

@@ -20,14 +20,13 @@ import { focusBorder, foreground } from 'vs/platform/theme/common/colorRegistry'
import { Button } from 'sql/base/browser/ui/button/button';
import { Color } from 'vs/base/common/color';
@Component({
selector: 'modelview-button',
template: `
<div>
<label for={{this.label}}>
<div #input style="width: 100%">
<input #fileInput *ngIf="this.isFile === true" id={{this.label}} type="file" accept=".sql" style="display: none">
<input #fileInput *ngIf="this.isFile === true" id={{this.label}} type="file" accept="{{ this.fileType }}" style="display: none">
</div>
</label>
</div>
@@ -37,6 +36,7 @@ export default class ButtonComponent extends ComponentWithIconBase implements IC
@Input() descriptor: IComponentDescriptor;
@Input() modelStore: IModelStore;
private _button: Button;
private fileType: string = '.sql';
@ViewChild('input', { read: ElementRef }) private _inputContainer: ElementRef;
@ViewChild('fileInput', { read: ElementRef }) private _fileInputContainer: ElementRef;
@@ -71,7 +71,10 @@ export default class ButtonComponent extends ComponentWithIconBase implements IC
self.fileContent = text.toString();
self.fireEvent({
eventType: ComponentEventType.onDidClick,
args: self.fileContent
args: {
filePath: file.path,
fileContent: self.fileContent
}
});
};
reader.readAsText(file);
@@ -101,6 +104,9 @@ export default class ButtonComponent extends ComponentWithIconBase implements IC
super.setProperties(properties);
this._button.enabled = this.enabled;
this._button.label = this.label;
if (this.properties.fileType) {
this.fileType = properties.fileType;
}
this._button.title = this.title;
// Button's ariaLabel gets set to the label by default.
@@ -116,6 +122,7 @@ export default class ButtonComponent extends ComponentWithIconBase implements IC
this._button.setWidth(this.convertSize(this.height.toString()));
}
this.updateIcon();
this._changeRef.detectChanges();
}
protected updateIcon() {
@@ -182,6 +189,10 @@ export default class ButtonComponent extends ComponentWithIconBase implements IC
this.setPropertyFromUI<azdata.ButtonProperties, string>((properties, title) => { properties.title = title; }, newValue);
}
private setFileType(value: string) {
this.properties.fileType = value;
}
private get ariaLabel(): string {
return this.getPropertyOrDefault<azdata.ButtonProperties, string>((properties) => properties.ariaLabel, '');
}

View File

@@ -56,6 +56,8 @@ import { AlertsViewComponent } from 'sql/workbench/parts/jobManagement/browser/a
import { JobHistoryComponent } from 'sql/workbench/parts/jobManagement/browser/jobHistory.component';
import { OperatorsViewComponent } from 'sql/workbench/parts/jobManagement/browser/operatorsView.component';
import { ProxiesViewComponent } from 'sql/workbench/parts/jobManagement/browser/proxiesView.component';
import { NotebooksViewComponent } from 'sql/workbench/parts/jobManagement/browser/notebooksView.component';
import { NotebookHistoryComponent } from 'sql/workbench/parts/jobManagement/browser/notebookHistory.component';
import LoadingSpinner from 'sql/workbench/browser/modelComponents/loadingSpinner.component';
import { Checkbox } from 'sql/base/browser/ui/checkbox/checkbox.component';
import { SelectBox } from 'sql/platform/browser/selectBox/selectBox.component';
@@ -65,7 +67,7 @@ import { EditableDropDown } from 'sql/platform/browser/editableDropdown/editable
const baseComponents = [DashboardHomeContainer, DashboardComponent, DashboardWidgetWrapper, DashboardWebviewContainer,
DashboardWidgetContainer, DashboardGridContainer, DashboardErrorContainer, DashboardNavSection, ModelViewContent, WebviewContent, WidgetContent,
ComponentHostDirective, BreadcrumbComponent, ControlHostContent, DashboardControlHostContainer,
JobsViewComponent, AgentViewComponent, JobHistoryComponent, JobStepsViewComponent, AlertsViewComponent, ProxiesViewComponent, OperatorsViewComponent,
JobsViewComponent, NotebooksViewComponent, AgentViewComponent, JobHistoryComponent, NotebookHistoryComponent, JobStepsViewComponent, AlertsViewComponent, ProxiesViewComponent, OperatorsViewComponent,
DashboardModelViewContainer, ModelComponentWrapper, Checkbox, EditableDropDown, SelectBox, InputBox, LoadingSpinner];
/* Panel */
@@ -89,6 +91,7 @@ import { JobStepsViewComponent } from 'sql/workbench/parts/jobManagement/browser
import { IInstantiationService, _util } from 'vs/platform/instantiation/common/instantiation';
import { ILogService } from 'vs/platform/log/common/log';
const widgetComponents = [
PropertiesWidgetComponent,
ExplorerWidget,

View File

@@ -17,6 +17,17 @@
</div>
</ng-template>
</tab>
<tab [title]="notebooksComponentTitle" class="fullsize" [identifier]="notebooksTabIdentifier"
[iconClass]="notebooksIconClass">
<ng-template>
<div id="notebooksDiv" class="fullsize" *ngIf="showNotebookHistory === false">
<notebooksview-component></notebooksview-component>
</div>
<div id="historyDiv" class="fullsize" *ngIf="showNotebookHistory === true">
<notebookhistory-component></notebookhistory-component>
</div>
</ng-template>
</tab>
<tab [title]="alertsComponentTitle" class="fullsize" [identifier]="alertsTabIdentifier"
[iconClass]="alertsIconClass">
<ng-template>

View File

@@ -7,7 +7,7 @@ import 'vs/css!./media/jobs';
import * as nls from 'vs/nls';
import { Component, Inject, forwardRef, ChangeDetectorRef, ViewChild, Injectable } from '@angular/core';
import { AgentJobInfo } from 'azdata';
import { AgentJobInfo, AgentNotebookInfo } from 'azdata';
import { PanelComponent, IPanelOptions, NavigationBarLayout } from 'sql/base/browser/ui/panel/panel.component';
import { IJobManagementService } from 'sql/platform/jobManagement/common/interfaces';
import { IDashboardService } from 'sql/platform/dashboard/browser/dashboardService';
@@ -25,17 +25,23 @@ export class AgentViewComponent {
@ViewChild(PanelComponent) private _panel: PanelComponent;
private _showHistory: boolean = false;
private _showNotebookHistory: boolean = false;
private _jobId: string = null;
private _notebookId: string = null;
private _agentJobInfo: AgentJobInfo = null;
private _agentNotebookInfo: AgentNotebookInfo = null;
private _refresh: boolean = undefined;
private _expanded: Map<string, string>;
private _expandedNotebook: Map<string, string>;
public jobsIconClass: string = 'jobsview-icon';
public notebooksIconClass: string = 'notebooksview-icon';
public alertsIconClass: string = 'alertsview-icon';
public proxiesIconClass: string = 'proxiesview-icon';
public operatorsIconClass: string = 'operatorsview-icon';
private readonly jobsComponentTitle: string = nls.localize('jobview.Jobs', "Jobs");
private readonly notebooksComponentTitle: string = nls.localize('jobview.Notebooks', "Notebooks");
private readonly alertsComponentTitle: string = nls.localize('jobview.Alerts', "Alerts");
private readonly proxiesComponentTitle: string = nls.localize('jobview.Proxies', "Proxies");
private readonly operatorsComponentTitle: string = nls.localize('jobview.Operators', "Operators");
@@ -67,14 +73,26 @@ export class AgentViewComponent {
return this._jobId;
}
public get notebookId(): string {
return this._notebookId;
}
public get showHistory(): boolean {
return this._showHistory;
}
public get showNotebookHistory(): boolean {
return this._showNotebookHistory;
}
public get agentJobInfo(): AgentJobInfo {
return this._agentJobInfo;
}
public get agentNotebookInfo(): AgentNotebookInfo {
return this._agentNotebookInfo;
}
public get refresh(): boolean {
return this._refresh;
}
@@ -83,6 +101,10 @@ export class AgentViewComponent {
return this._expanded;
}
public get expandedNotebook(): Map<string, string> {
return this._expandedNotebook;
}
/**
* Public Setters
*/
@@ -91,15 +113,28 @@ export class AgentViewComponent {
this._jobId = value;
}
public set notebookId(value: string) {
this._notebookId = value;
}
public set showHistory(value: boolean) {
this._showHistory = value;
this._cd.detectChanges();
}
public set showNotebookHistory(value: boolean) {
this._showNotebookHistory = value;
this._cd.detectChanges();
}
public set agentJobInfo(value: AgentJobInfo) {
this._agentJobInfo = value;
}
public set agentNotebookInfo(value: AgentNotebookInfo) {
this._agentNotebookInfo = value;
}
public set refresh(value: boolean) {
this._refresh = value;
this._cd.detectChanges();
@@ -109,10 +144,18 @@ export class AgentViewComponent {
this._expanded.set(jobId, errorMessage);
}
public setExpandedNotebook(jobId: string, errorMessage: string) {
this._expandedNotebook.set(jobId, errorMessage);
}
public set expanded(value: Map<string, string>) {
this._expanded = value;
}
public set expandedNotebook(value: Map<string, string>) {
this._expandedNotebook = value;
}
public layout() {
this._panel.layout();
}

View File

@@ -130,3 +130,8 @@ export interface JobActionContext {
canEdit: boolean;
job: azdata.AgentJobInfo;
}
export interface NotebookActionContext {
canEdit: boolean;
notebook: azdata.AgentNotebookInfo;
}

View File

@@ -507,11 +507,11 @@ export class JobsViewComponent extends JobManagementView implements OnInit, OnDe
if (runChart && runChart.length > 0) {
return `<table class="jobprevruns" id="${dataContext.id}">
<tr>
<td>${runChart[0] ? runChart[0] : '<div></div>'}</td>
<td>${runChart[1] ? runChart[1] : '<div></div>'}</td>
<td>${runChart[2] ? runChart[2] : '<div></div>'}</td>
<td>${runChart[3] ? runChart[3] : '<div></div>'}</td>
<td>${runChart[4] ? runChart[4] : '<div></div>'}</td>
<td>${runChart[0] ? runChart[0] : '<div class="bar0"></div>'}</td>
<td>${runChart[1] ? runChart[1] : '<div class="bar1"></div>'}</td>
<td>${runChart[2] ? runChart[2] : '<div class="bar2"></div>'}</td>
<td>${runChart[3] ? runChart[3] : '<div class="bar3"></div>'}</td>
<td>${runChart[4] ? runChart[4] : '<div class="bar4"></div>'}</td>
</tr>
</table>`;
} else {

View File

@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<title>Artboard 240</title>
<g>
<path d="M11.5,7,7,16h9Zm.5,8H11V14h1Zm-1-1.5V11h1v2.5Z" fill="#db7500"/>
<path d="M5,6h6V3H5ZM6,4h4V5H6Z"/>
<polygon points="5.882 16 6.382 15 2 15 2 13 3 13 3 12 2 12 2 10.023 3 10.023 3 9.023 2 9.023 2 7.039 3 7.039 3 6.039 2 6.039 2 4 3 4 3 3 2 3 2 1 13 1 13 7.764 14 9.764 14 0 1 0 1 3 0 3 0 4 1 4 1 6.039 0 6.039 0 7.039 1 7.039 1 9.023 0 9.023 0 10.023 1 10.023 1 12 0 12 0 13 1 13 1 16 5.882 16"/>
<path d="M11,15h1V14H11Zm0-4v2.5h1V11Z" fill="#fff"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 610 B

View File

@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<title>Artboard 210</title>
<g>
<path d="M11.5,7,7,16h9Zm.5,8H11V14h1Zm-1-1.5V11h1v2.5Z" fill="#db7500"/>
<path d="M5,6h6V3H5ZM6,4h4V5H6Z" fill="#fff"/>
<polygon points="5.882 16 6.382 15 2 15 2 13 3 13 3 12 2 12 2 10.023 3 10.023 3 9.023 2 9.023 2 7.039 3 7.039 3 6.039 2 6.039 2 4 3 4 3 3 2 3 2 1 13 1 13 7.764 14 9.764 14 0 1 0 1 3 0 3 0 4 1 4 1 6.039 0 6.039 0 7.039 1 7.039 1 9.023 0 9.023 0 10.023 1 10.023 1 12 0 12 0 13 1 13 1 16 5.882 16" fill="#fff"/>
<path d="M11,15h1V14H11Zm0-4v2.5h1V11Z" fill="#fff"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 634 B

View File

@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<title>Artboard 260</title>
<g>
<path d="M15.645,9.75A4.528,4.528,0,0,0,13.25,7.356a4.479,4.479,0,0,0-3.5,0A4.528,4.528,0,0,0,7.355,9.75a4.491,4.491,0,0,0,0,3.5A4.526,4.526,0,0,0,9.75,15.645a4.491,4.491,0,0,0,3.5,0,4.526,4.526,0,0,0,2.395-2.395,4.491,4.491,0,0,0,0-3.5Z" fill="#e00b1c"/>
<path d="M5,6h6V3H5ZM6,4h4V5H6Z"/>
<path d="M1,0V3H0V4H1V6.039H0v1H1V9.023H0v1H1V12H0v1H1v3H7.938a6.581,6.581,0,0,1-.524-.464A3.812,3.812,0,0,1,6.961,15H2V13H3V12H2V10.023H3v-1H2V7.039H3v-1H2V4H3V3H2V1H13V5.961a4.87,4.87,0,0,1,1,.375V0Z"/>
<polygon points="12.648 9.649 11.5 10.797 10.352 9.649 9.648 10.352 10.797 11.5 9.648 12.649 10.352 13.352 11.5 12.204 12.648 13.352 13.352 12.649 12.203 11.5 13.352 10.352 12.648 9.649" fill="#fff"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 847 B

View File

@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<title>Artboard 230</title>
<g>
<path d="M15.645,9.75A4.528,4.528,0,0,0,13.25,7.356a4.479,4.479,0,0,0-3.5,0A4.528,4.528,0,0,0,7.355,9.75a4.491,4.491,0,0,0,0,3.5A4.526,4.526,0,0,0,9.75,15.645a4.491,4.491,0,0,0,3.5,0,4.526,4.526,0,0,0,2.395-2.395,4.491,4.491,0,0,0,0-3.5Z" fill="#e00b1c"/>
<path d="M5,6h6V3H5ZM6,4h4V5H6Z" fill="#fff"/>
<path d="M1,0V3H0V4H1V6.039H0v1H1V9.023H0v1H1V12H0v1H1v3H7.938a6.581,6.581,0,0,1-.524-.464A3.812,3.812,0,0,1,6.961,15H2V13H3V12H2V10.023H3v-1H2V7.039H3v-1H2V4H3V3H2V1H13V5.961a4.87,4.87,0,0,1,1,.375V0Z" fill="#fff"/>
<polygon points="12.648 9.649 11.5 10.797 10.352 9.649 9.648 10.352 10.797 11.5 9.648 12.649 10.352 13.352 11.5 12.204 12.648 13.352 13.352 12.649 12.203 11.5 13.352 10.352 12.648 9.649" fill="#fff"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 871 B

View File

@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<title>Artboard 250</title>
<g>
<path d="M15.645,9.75A4.528,4.528,0,0,0,13.25,7.356a4.479,4.479,0,0,0-3.5,0A4.528,4.528,0,0,0,7.355,9.75a4.491,4.491,0,0,0,0,3.5A4.526,4.526,0,0,0,9.75,15.645a4.491,4.491,0,0,0,3.5,0,4.526,4.526,0,0,0,2.395-2.395,4.491,4.491,0,0,0,0-3.5Z" fill="#57a300"/>
<path d="M5,6h6V3H5ZM6,4h4V5H6Z"/>
<path d="M1,0V3H0V4H1V6.039H0v1H1V9.023H0v1H1V12H0v1H1v3H7.938a6.581,6.581,0,0,1-.524-.464A3.812,3.812,0,0,1,6.961,15H2V13H3V12H2V10.023H3v-1H2V7.039H3v-1H2V4H3V3H2V1H13V5.961a4.87,4.87,0,0,1,1,.375V0Z"/>
<path d="M13.224,9.794l.7.7-3,3L9.073,11.648l.7-.7,1.148,1.149Z" fill="#fff"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 725 B

View File

@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<title>Artboard 220</title>
<g>
<path d="M15.645,9.75A4.528,4.528,0,0,0,13.25,7.356a4.479,4.479,0,0,0-3.5,0A4.528,4.528,0,0,0,7.355,9.75a4.491,4.491,0,0,0,0,3.5A4.526,4.526,0,0,0,9.75,15.645a4.491,4.491,0,0,0,3.5,0,4.526,4.526,0,0,0,2.395-2.395,4.491,4.491,0,0,0,0-3.5Z" fill="#57a300"/>
<path d="M5,6h6V3H5ZM6,4h4V5H6Z" fill="#fff"/>
<path d="M1,0V3H0V4H1V6.039H0v1H1V9.023H0v1H1V12H0v1H1v3H7.938a6.581,6.581,0,0,1-.524-.464A3.812,3.812,0,0,1,6.961,15H2V13H3V12H2V10.023H3v-1H2V7.039H3v-1H2V4H3V3H2V1H13V5.961a4.87,4.87,0,0,1,1,.375V0Z" fill="#fff"/>
<path d="M13.224,9.794l.7.7-3,3L9.073,11.648l.7-.7,1.148,1.149Z" fill="#fff"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 749 B

View File

@@ -3,15 +3,22 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
jobhistory-component .all-jobs {
display: inline-block;
cursor: pointer;
font-size: 15px;
width: 10%;
margin-top: 10px;
}
notebookhistory-component .all-jobs {
display: inline-block;
cursor: pointer;
font-size: 15px;
width: 100%;
}
.overview-container .overview-tab .resultsViewCollapsible {
.overview-container .overview-tab .resultsViewCollapsible,
.overview-container .overview-tab .notebooksgridViewCollapsible {
padding: 15px;
display: inline;
}
@@ -26,6 +33,11 @@ jobhistory-component .all-jobs {
padding-top: 10px;
}
.history-container {
position: relative;
overflow-y: auto;
}
.vs-dark .overview-container .overview-tab {
color: #fff;
}
@@ -42,7 +54,13 @@ jobhistory-component .all-jobs {
overflow: hidden;
}
input#accordion {
input#accordion{
position: absolute;
opacity: 0;
z-index: -1;
}
.grid-arrow{
position: absolute;
opacity: 0;
z-index: -1;
@@ -72,7 +90,8 @@ input#accordion {
background: #333333;
}
.hc-black .overview-tab .accordion-content {
.hc-black .overview-tab .accordion-content,
.grid-arrow {
background: #000000;
border: 1px solid #2b56f2;
}
@@ -81,9 +100,9 @@ input#accordion {
max-height: 0;
overflow: hidden;
background: #eaeaea;
-webkit-transition: max-height .35s;
-o-transition: max-height .35s;
transition: max-height .35s;
-webkit-transition: max-height 0.35s;
-o-transition: max-height 0.35s;
transition: max-height 0.35s;
width: 100%;
}
@@ -92,7 +111,13 @@ input#accordion {
}
/* :checked */
input#accordion:checked ~ .accordion-content {
input#accordion:checked ~ .accordion-content,
.grid-arrow:checked {
max-height: 10em;
}
input#accordion:checked ~ .accordion-content,
.grid-arrow:checked {
max-height: 10em;
}
@@ -106,13 +131,13 @@ input#accordion:checked ~ .accordion-content {
height: 3em;
line-height: 3;
text-align: center;
-webkit-transition: all .3s;
-o-transition: all .3s;
transition: all .3s;
-webkit-transition: all 0.3s;
-o-transition: all 0.3s;
transition: all 0.3s;
}
.all-jobs > .back-button-icon {
content: url('back.svg');
content: url("back.svg");
width: 20px;
margin-right: 10px;
float: left;
@@ -121,30 +146,43 @@ input#accordion:checked ~ .accordion-content {
padding-bottom: 10px;
}
.vs-dark .all-jobs >.back-button-icon,
.hc-black .all-jobs >.back-button-icon {
content: url('back_inverse.svg');
.vs-dark .all-jobs > .back-button-icon,
.hc-black .all-jobs > .back-button-icon {
content: url("back_inverse.svg");
}
.vs .action-label.icon.newStepIcon {
background-image: url('new.svg');
background-image: url("new.svg");
}
.vs-dark .action-label.icon.newStepIcon,
.hc-black .action-label.icon.newStepIcon {
background-image: url('new_inverse.svg');
background-image: url("new_inverse.svg");
}
.vs .action-label.icon.opennotebook {
background-image: url("open_notebook.svg");
}
.vs-dark .action-label.icon.opennotebook,
.hc-black .action-label.icon.newStepIcon {
background-image: url("open_notebook_inverse.svg");
}
jobhistory-component .hc-black .icon.edit,
jobhistory-component .vs-dark .icon.edit {
background-image: url('edit_inverse.svg');
jobhistory-component .vs-dark .icon.edit,
notebookhistory-component .hc-black .icon.edit,
notebookhistory-component .vs-dark .icon.edit {
background-image: url("edit_inverse.svg");
}
jobhistory-component .vs .icon.edit {
background-image: url('edit.svg');
jobhistory-component .vs .icon.edit,
notebookhistory-component .vs .icon.edit {
background-image: url("edit.svg");
}
jobhistory-component .actions-container .icon.edit {
jobhistory-component .actions-container .icon.edit,
notebookhistory-component .actions-container .icon.edit {
background-position: 0% 50%;
background-repeat: no-repeat;
background-size: 12px;
@@ -210,12 +248,20 @@ table.step-list tr.step-row td {
max-height: 200px;
}
.step-table .monaco-tree .monaco-tree-rows.show-twisties > .monaco-tree-row.has-children > .content:before {
.step-table
.monaco-tree
.monaco-tree-rows.show-twisties
> .monaco-tree-row.has-children
> .content:before {
background: none;
background-image: none;
}
.step-table .monaco-tree.focused .monaco-tree-rows.show-twisties > .monaco-tree-row.has-children.selected:not(.loading) > .content:before {
.step-table
.monaco-tree.focused
.monaco-tree-rows.show-twisties
> .monaco-tree-row.has-children.selected:not(.loading)
> .content:before {
background-image: none;
}
@@ -258,40 +304,182 @@ table.step-list tr.step-row td {
flex-direction: column;
}
jobhistory-component {
jobhistory-component,
notebookhistory-component {
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
}
jobhistory-component > .jobhistory-heading-container {
jobhistory-component > .jobhistory-heading-container,
notebookhistory-component > .jobhistory-heading-container {
display: flex;
}
jobhistory-component > .jobhistory-heading-container > .icon.in-progress {
jobhistory-component > .jobhistory-heading-container > .icon.in-progress,
notebookhistory-component > .jobhistory-heading-container > .icon.in-progress {
width: 20px;
height: 20px;
padding-top: 16px;
padding-left: 20px;
}
jobhistory-component > .agent-actionbar-container {
jobhistory-component > .agent-actionbar-container,
notebookhistory-component > .agent-actionbar-container {
border-top: 3px solid #f4f4f4;
}
.vs-dark jobhistory-component > .agent-actionbar-container {
.vs-dark jobhistory-component > .agent-actionbar-container,
.vs-dark notebookhistory-component > .agent-actionbar-container {
border-top: 3px solid #444444;
}
.hc-black jobhistory-component > .agent-actionbar-container {
.hc-black jobhistory-component > .agent-actionbar-container,
.hc-black notebookhistory-component > .agent-actionbar-container {
border-top: 3px solid #2b56f2;
}
jobhistory-component .step-table.prev-run-list .monaco-tree-wrapper .monaco-tree-row {
jobhistory-component
.step-table.prev-run-list
.monaco-tree-wrapper
.monaco-tree-row,
notebookhistory-component
.step-table.prev-run-list
.monaco-tree-wrapper
.monaco-tree-row {
width: 96%;
}
jobhistory-component .agent-actionbar-container > .monaco-toolbar.carbon-taskbar {
jobhistory-component
.agent-actionbar-container
> .monaco-toolbar.carbon-taskbar,
notebookhistory-component
.agent-actionbar-container
> .monaco-toolbar.carbon-taskbar {
margin: 10px 0px 5px 0px;
}
.notebook-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
grid-gap: 10px;
padding: 10px;
}
.notebook-grid-item {
border-radius: 5px;
padding-bottom: 5px;
height: 95px;
display: block;
border-radius: 0px;
}
.notebook-grid-item > img {
display: block;
margin-left: auto;
margin-right: auto;
margin-top: 10px;
width: 50px;
height: 50px;
}
.notebook-grid-item:hover {
background: #dcdcdc !important;
}
.vs-dark .notebook-grid-item:hover,
.hc-black .notebook-grid-item:hover {
background: #444444 !important;
}
.notebook-grid-item > .img-success {
background-image: url(./NotebookSuccess_16x.svg);
background-position: center;
display: block;
margin-left: auto;
margin-right: auto;
margin-top: 10px;
width: 40px;
height: 40px;
background-size: 40px 40px;
background-repeat: no-repeat;
}
.vs-dark .notebook-grid-item > .img-success,
.hc-black .notebook-grid-item > .img-success {
background-image: url(./NotebookSuccess_16x_white.svg);
background-position: center;
}
.notebook-grid-item > .img-failure {
background-image: url(./NotebookFail_16x.svg);
background-position: center;
display: block;
margin-left: auto;
margin-right: auto;
margin-top: 10px;
width: 40px;
height: 40px;
background-size: 40px 40px;
background-repeat: no-repeat;
}
.vs-dark .notebook-grid-item > .img-failure,
.hc-black .notebook-grid-item > .img-failure {
background-image: url(./NotebookFail_16x_white.svg);
background-position: center;
}
.notebook-grid-item > .img-error {
background-image: url(./NotebookError_16x.svg);
background-position: center;
display: block;
margin-left: auto;
margin-right: auto;
margin-top: 10px;
width: 40px;
height: 40px;
background-size: 40px 40px;
background-repeat: no-repeat;
}
.vs-dark .notebook-grid-item > .img-error,
.hc-black .notebook-grid-item > .img-error {
background-image: url(./NotebookError_16x_white.svg);
background-position: center;
}
.notebook-grid-item > p {
display: absolute;
margin-left: auto;
margin-right: auto;
margin-top: 10px;
width: 100%;
text-align: center;
}
.notebook-history-container {
flex: 1 1 auto;
position: relative;
overflow-y: auto;
}
.grid-title {
position: relative;
display: block;
padding: 0 0 0 1em;
background: #f4f4f4;
font-weight: bold;
line-height: 3;
cursor: pointer;
width: auto;
}
.vs-dark .grid-title {
background: #333333;
}
.hc-black .grid-title {
background: #000000;
border: 1px solid #2b56f2;
}

View File

@@ -9,12 +9,14 @@ agentview-component {
display: block;
}
jobsview-component {
jobsview-component,
notebooksview-component {
height: 100%;
width : 100%;
display: block;
}
.job-heading-container {
height: 50px;
border-bottom: 3px solid #f4f4f4;
@@ -35,6 +37,12 @@ jobsview-component {
display: block;
}
.jobnotebooksview-grid {
height: calc(100% - 75px);
width : 100%;
display: block;
}
.vs-dark #agentViewDiv .slick-header-column {
background: #333333 !important;
}
@@ -45,52 +53,63 @@ jobsview-component {
font-weight: bold;
}
.hc-black #jobsDiv jobsview-component .jobview-grid .grid-canvas .ui-widget-content.slick-row .slick-cell {
.hc-black #jobsDiv jobsview-component .jobview-grid .grid-canvas .ui-widget-content.slick-row .slick-cell,
.hc-black #notebooksDiv notebooks-component .jobnotebooksview-grid .grid-canvas .ui-widget-content.slick-row .slick-cell {
border: 1px solid #2b56f2;
}
#jobsDiv jobsview-component .jobview-grid .grid-canvas .ui-widget-content.slick-row .slick-cell {
#jobsDiv jobsview-component .jobview-grid .grid-canvas .ui-widget-content.slick-row .slick-cell,
#notebooksDiv notebooksview-component .jobnotebooksview-grid .grid-canvas .ui-widget-content.slick-row .slick-cell {
border-right: transparent !important;
border-left: transparent !important;
line-height: 33px !important;
}
#jobsDiv .jobview-joblist {
#jobsDiv .jobview-joblist,
#notebooksDiv .jobview-joblist {
height: 100%;
width: 100%;
}
#jobsDiv .jobview-jobnametable {
#jobsDiv .jobview-jobnametable,
#notebooksDiv .jobview-jobnametable {
border: 0px;
width: 100%;
height: 100%;
}
#jobsDiv .jobview-jobnameindicatorsuccess {
#jobsDiv .jobview-jobnameindicatorsuccess,
#notebooksDiv .jobview-jobnameindicatorsuccess {
width: 5px;
background: green;
}
#jobsDiv .slick-cell.l1.r1 .jobview-jobnameindicatorfailure {
#jobsDiv .slick-cell.l1.r1 .jobview-jobnameindicatorfailure,
#notebooksDiv .slick-cell.l1.r1 .jobview-jobnameindicatorfailure {
width: 5px;
background: red;
}
#jobsDiv .jobview-jobnameindicatorcancel {
#jobsDiv .jobview-jobnameindicatorcancel,
#notebooksDiv .jobview-jobnameindicatorcancel {
width: 5px;
background: orange;
}
#jobsDiv .jobview-grid .jobview-jobnameindicatorunknown {
#jobsDiv .jobview-grid .jobview-jobnameindicatorunknown,
#notebooks .jobnotebooksview-grid .jobview-jobnameindicatorunknown {
width: 5px;
background: grey;
}
#jobsDiv .jobview-grid .slick-cell.l1.r1.error-row .jobview-jobnametext {
#jobsDiv .jobview-grid .slick-cell.l1.r1.error-row .jobview-jobnametext,
#notebooksDiv .jobnotebooksview-grid .slick-cell.l1.r1.error-row .jobview-jobnametext,
#notebooksDiv .jobnotebooksview-grid .slick-cell.l1.r1.notebook-error-row .jobview-jobnametext {
width: 100%;
}
#jobsDiv .jobview-grid .slick-cell.l1.r1 .jobview-jobnametext {
#jobsDiv .jobview-grid .slick-cell.l1.r1 .jobview-jobnametext,
#notebooksDiv .jobnotebooksview-grid .slick-cell.l1.r1 .jobview-jobnametext {
text-overflow: ellipsis;
width: 250px;
overflow: hidden;
@@ -108,28 +127,44 @@ jobsview-component {
display: inline-block;
}
#jobsDiv .job-with-error {
#jobsDiv .job-with-error,
#notebooksDiv .job-with-error {
border-bottom: none;
}
.jobview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row .slick-cell.l1.r1.error-row {
.jobview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row .slick-cell.l1.r1.error-row,
.jobnotebooksview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row .slick-cell.l1.r1.error-row {
width: 100%;
opacity: 1;
font-weight: 700;
text-overflow: ellipsis;
color: orangered;
}
.jobview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row .slick-cell._detail_selector.error-row {
.jobnotebooksview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row .slick-cell.l1.r1.notebook-error-row {
width: 100%;
opacity: 1;
font-weight: 700;
overflow: hidden;
text-overflow: ellipsis;
color: orange;
}
.jobview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row .slick-cell._detail_selector.error-row,
.jobnotebooksview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row .slick-cell._detail_selector.error-row,
.jobnotebooksview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row .slick-cell._detail_selector.notebook-error-row {
opacity: 1;
}
#jobsDiv .jobview-splitter {
#jobsDiv .jobview-splitter,
#notebooksDiv .jobview-splitter {
height: 1px;
width: 100%;
background-color: gray;
}
#jobsDiv .jobview-jobitem {
#jobsDiv .jobview-jobitem,
#notebooksDiv .jobview-jobitem {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
@@ -137,30 +172,36 @@ jobsview-component {
white-space: nowrap;
}
#jobsDiv .jobview-label {
#jobsDiv .jobview-label,
#notebooksDiv .jobview-label {
padding-bottom: 10px;
padding-top: 10px;
}
#jobsDiv .jobview-highlight-none {
#jobsDiv .jobview-highlight-none,
#notebooksDiv .jobview-highlight-none {
width: 5px;
margin-right: 10px;
}
#jobsDiv .detail-container {
#jobsDiv .detail-container,
#notebooksDiv .detail-container {
max-height: 100px !important;
line-height: 20px;
}
#jobsDiv .detail {
#jobsDiv .detail,
#notebooksDiv .detail {
padding: 5px
}
#jobsDiv .preload {
#jobsDiv .preload,
#notebooksDiv .preload {
font-size: 13px;
}
#jobsDiv .dynamic-cell-detail > :first-child {
#jobsDiv .dynamic-cell-detail > :first-child,
#notebooksDiv .dynamic-cell-detail > :first-child {
vertical-align: middle;
line-height: 13px;
padding: 10px;
@@ -171,11 +212,22 @@ jobsview-component {
background-image: url('./job.svg');
}
.vs-dark .jobsview-icon,
.hc-black .jobsview-icon {
background-image: url('./job_inverse.svg');
}
.notebooksview-icon {
background-image: url('./notebook.svg');
}
.vs-dark .notebooksview-icon,
.hc-black .notebooksview-icon {
background-image: url('./notebook_inverse.svg');
}
.alertsview-icon {
background-image: url('./alert.svg');
}
@@ -204,7 +256,7 @@ jobsview-component {
}
agentview-component .jobview-grid .grid-canvas > .ui-widget-content.slick-row.even > .slick-cell,
agentview-component .jobview-grid .grid-canvas > .ui-widget-content.slick-row.odd > .slick-cell {
agentview-component .jobnotebooksview-grid .grid-canvas > .ui-widget-content.slick-row.odd > .slick-cell {
cursor: pointer;
}
@@ -227,26 +279,37 @@ agentview-component .jobview-grid .grid-canvas > .ui-widget-content.slick-row.od
padding-left: 15px;
}
#jobsDiv jobsview-component .jobview-grid .slick-cell.l1.r1.error-row td.jobview-jobnameindicatorfailure {
#jobsDiv jobsview-component .jobview-grid .slick-cell.l1.r1.error-row td.jobview-jobnameindicatorfailure,
#notebooksDiv notebooksview-component .jobnotebooksview-grid .slick-cell.l1.r1.error-row td.jobview-jobnameindicatorfailure,
#notebooksDiv notebooksview-component .jobnotebooksview-grid .slick-cell.l1.r1.notebook-error-row td.jobview-jobnameindicatorfailure {
width: 0;
background: none;
}
#jobsDiv .jobview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row:hover > .slick-cell,
#jobsDiv .jobview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row.hovered > .slick-cell,
#jobsDiv .jobview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row> .slick-cell.hovered {
#jobsDiv .jobview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row> .slick-cell.hovered,
#notebooksDiv .jobnotebooksview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row:hover > .slick-cell,
#notebooksDiv .jobnotebooksview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row.hovered > .slick-cell,
#notebooksDiv .jobnotebooksview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row> .slick-cell.hovered {
background: #dcdcdc !important;
}
.vs-dark #jobsDiv .jobview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row:hover > .slick-cell,
.vs-dark #jobsDiv .jobview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row > .slick-cell.hovered,
.vs-dark #jobsDiv .jobview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row.hovered > .slick-cell {
.vs-dark #jobsDiv .jobview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row.hovered > .slick-cell,
.vs-dark #notebooksDiv .jobnotebooksview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row:hover > .slick-cell,
.vs-dark #notebooksDiv .jobnotebooksview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row > .slick-cell.hovered,
.vs-dark #notebooksDiv .jobnotebooksview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row.hovered > .slick-cell {
background: #444444 !important;
}
.hc-black #jobsDiv .jobview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row:hover > .slick-cell,
.hc-black #jobsDiv .jobview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row > .slick-cell.hovered,
.hc-black #jobsDiv .jobview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row.hovered > .slick-cell {
.hc-black #jobsDiv .jobview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row.hovered > .slick-cell,
.hc-black #notebooksDiv .jobnotebooksview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row:hover > .slick-cell,
.hc-black #notebooksDiv .jobnotebooksview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row > .slick-cell.hovered,
.hc-black #notebooksDiv .jobnotebooksview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row.hovered > .slick-cell {
background: none !important;
}
@@ -257,7 +320,8 @@ table.jobprevruns div.bar3, table.jobprevruns div.bar4, table.jobprevruns div.ba
width: 10px;
}
.jobview-grid .slick-cell.l10.r10 {
.jobview-grid .slick-cell.l10.r10,
.jobnotebooksview-grid .slick-cell.l10.r10 {
text-align: center;
display: inline-flex;
}
@@ -270,6 +334,12 @@ table.jobprevruns > tbody {
vertical-align: bottom;
}
#notebooksDiv .jobnotebooksview-grid {
height: calc(100% - 75px);
width : 100%;
display: block;
}
#alertsDiv .jobalertsview-grid {
height: calc(100% - 75px);
width : 100%;
@@ -289,6 +359,8 @@ table.jobprevruns > tbody {
display: block;
}
.vs .action-label.icon.refreshIcon {
background-image: url('refresh.svg');
}
@@ -298,14 +370,52 @@ table.jobprevruns > tbody {
background-image: url('refresh_inverse.svg');
}
.vs .action-label.icon.openNotebook {
background-image: url('open_notebook.svg');
}
.vs-dark .action-label.icon.openNotebook,
.hc-black .action-label.icon.openNotebook {
background-image: url('open_notebook_inverse.svg');
}
.agent-actionbar-container .monaco-action-bar > ul.actions-container > li.action-item {
padding-left: 20px;
}
jobsview-component .jobview-grid .slick-cell.error-row {
jobsview-component .jobview-grid .slick-cell.error-row,
notebooksview-component .jobnotebooksview-grid .slick-cell.error-row,
notebooksview-component .jobnotebooksview-grid .slick-cell.notebook-error-row {
opacity: 0;
}
#notebooksDiv notebooksview-component .jobnotebooksview-grid .grid-canvas .ui-widget-content.slick-row .slick-cell {
border-right: transparent !important;
border-left: transparent !important;
line-height: 33px !important;
}
#notebooksDiv .jobnotebooksview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row:hover > .slick-cell,
#notebooksDiv .jobnotebooksview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row.hovered > .slick-cell,
#notebooksDiv .jobnotebooksview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row> .slick-cell.hovered {
background: #dcdcdc !important;
}
.vs-dark #notebooksDiv .jobnotebooksview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row:hover > .slick-cell,
.vs-dark #notebooksDiv .jobnotebooksview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row > .slick-cell.hovered,
.vs-dark #notebooksDiv .jobnotebooksview-grid > .monaco-table .slick-viewport > .grid-canvas > .ui-widget-content.slick-row.hovered > .slick-cell {
background: #444444 !important;
}
.vs-dark .jobnotebooksview-grid > .monaco-table .slick-header-columns .slick-resizable-handle {
border-left: 1px dotted white;
}
.jobnotebooksview-grid > .monaco-table .slick-header-columns .slick-resizable-handle {
border-left: 1px dotted #444444;
}
#alertsDiv jobalertsview-component .jobalertsview-grid .grid-canvas .ui-widget-content.slick-row .slick-cell {
border-right: transparent !important;
border-left: transparent !important;
@@ -403,8 +513,9 @@ jobsview-component .jobview-grid .slick-cell.error-row {
}
#jobsDiv jobsview-component .monaco-toolbar.carbon-taskbar,
#notebooksDiv notebooksview-component .monaco-toolbar.carbon-taskbar,
#operatorsDiv joboperatorsview-component .monaco-toolbar.carbon-taskbar,
#alertsDiv jobalertsview-component .monaco-toolbar.carbon-taskbar,
#proxiesDiv jobproxiesview-component .monaco-toolbar.carbon-taskbar {
margin: 10px 0px 10px 0px;
}
}

View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><title>notebook</title><path d="M15.5,2V15H.5V2h2V1h3a4.19,4.19,0,0,1,1.32.21A3.87,3.87,0,0,1,8,1.84a3.87,3.87,0,0,1,1.18-.63A4.19,4.19,0,0,1,10.5,1h3V2ZM1.5,14H7.8a4.43,4.43,0,0,0-.51-.43,3.41,3.41,0,0,0-.54-.31,2.74,2.74,0,0,0-.59-.2A3.2,3.2,0,0,0,5.5,13h-3V3h-1Zm2-2h2a4.18,4.18,0,0,1,1,.13,4,4,0,0,1,1,.39V2.72a3,3,0,0,0-.94-.54A3.15,3.15,0,0,0,5.5,2h-2Zm11-9h-1V13h-3a3.2,3.2,0,0,0-.67.07,2.74,2.74,0,0,0-.59.2,3.41,3.41,0,0,0-.54.31A4.43,4.43,0,0,0,8.2,14h6.3Zm-4-1a3.15,3.15,0,0,0-1.06.18,3,3,0,0,0-.94.54v9.8a4,4,0,0,1,1-.39,4.18,4.18,0,0,1,1-.13h2V2Z"/></svg>

After

Width:  |  Height:  |  Size: 661 B

View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{fill:#fff;}</style></defs><title>notebook_inverse</title><path class="cls-1" d="M15.46,2V15H.46V2h2V1h3a4.19,4.19,0,0,1,1.32.21A3.87,3.87,0,0,1,8,1.84a3.87,3.87,0,0,1,1.18-.63A4.19,4.19,0,0,1,10.46,1h3V2Zm-14,12h6.3a4.43,4.43,0,0,0-.51-.43,3.41,3.41,0,0,0-.54-.31,2.74,2.74,0,0,0-.59-.2A3.2,3.2,0,0,0,5.46,13h-3V3h-1Zm2-2h2a4.18,4.18,0,0,1,1,.13,4,4,0,0,1,1,.39V2.72a3,3,0,0,0-.94-.54A3.15,3.15,0,0,0,5.46,2h-2Zm11-9h-1V13h-3a3.2,3.2,0,0,0-.67.07,2.74,2.74,0,0,0-.59.2,3.41,3.41,0,0,0-.54.31,4.43,4.43,0,0,0-.51.43h6.3Zm-4-1a3.15,3.15,0,0,0-1.06.18,3,3,0,0,0-.94.54v9.8a4,4,0,0,1,1-.39,4.18,4.18,0,0,1,1-.13h2V2Z"/></svg>

After

Width:  |  Height:  |  Size: 734 B

View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{fill:#00539c;}</style></defs><title>open_notebook</title><path d="M12.4,4.21l-.08-.11h-.56l-.69.06a1.54,1.54,0,0,0-.23.29v8.69H9a3.32,3.32,0,0,0-.93.13,3.34,3.34,0,0,0-.87.34V4.76a2.88,2.88,0,0,1,.43-.31A5.58,5.58,0,0,1,8.14,3.3a2.63,2.63,0,0,0-.3.09A3.62,3.62,0,0,0,6.78,4a3.68,3.68,0,0,0-1.07-.57A3.58,3.58,0,0,0,4.52,3.2H1.81v.9H0V15.85H13.57V5.48ZM2.71,4.1H4.52a2.61,2.61,0,0,1,1,.17,2.32,2.32,0,0,1,.86.49v8.85a3.27,3.27,0,0,0-.88-.34,3.22,3.22,0,0,0-.93-.13H2.71ZM.9,15V5h.91v9H4.52a3.94,3.94,0,0,1,.61.06,3.2,3.2,0,0,1,.52.18,4.19,4.19,0,0,1,.49.29,2.28,2.28,0,0,1,.45.39Zm11.75,0H7a2.7,2.7,0,0,1,.47-.39,2.83,2.83,0,0,1,.47-.29,3.42,3.42,0,0,1,.54-.18A3.81,3.81,0,0,1,9,14h2.73V5h.89Z"/><polygon class="cls-1" points="13.05 3.56 13.05 3.58 13.04 3.57 13.05 3.56"/><path class="cls-1" d="M13,3.57h0v0Z"/><polygon class="cls-1" points="13.05 3.56 13.05 3.58 13.04 3.57 13.05 3.56"/><polygon class="cls-1" points="14.06 1.65 14.04 1.65 14.04 1.63 14.06 1.65"/><path class="cls-1" d="M15.76,2.1,14,3.81l-.38.38L13,3.58v0l1-1H12.64a3.35,3.35,0,0,0-1.09.26h0a3.94,3.94,0,0,0-.86.52l-.24.21s0,0,0,0a3.3,3.3,0,0,0-.51.67,3.1,3.1,0,0,0-.26.47,3.41,3.41,0,0,0-.27,1.39h-.9a4.68,4.68,0,0,1,.16-1.19,4.74,4.74,0,0,1,.25-.66,2.21,2.21,0,0,1,.2-.41,4.66,4.66,0,0,1,.36-.51c.1-.13.22-.26.34-.39a4.14,4.14,0,0,1,.66-.53,1.19,1.19,0,0,1,.23-.16A2.79,2.79,0,0,1,11,2.08l.31-.13.42-.14a4.32,4.32,0,0,1,1.19-.16h1.15l-1-1L13.67,0Z"/></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{fill:#fff;}.cls-2{fill:#0095d7;}</style></defs><title>open_notebook_inverse</title><path class="cls-1" d="M12.55,4.21l-.08-.11h-.56l-.69.06a1.54,1.54,0,0,0-.23.29v8.69H9.18a3.32,3.32,0,0,0-.93.13,3.34,3.34,0,0,0-.87.34V4.76a2.88,2.88,0,0,1,.43-.31A5.58,5.58,0,0,1,8.29,3.3a2.63,2.63,0,0,0-.3.09A3.62,3.62,0,0,0,6.93,4a3.68,3.68,0,0,0-1.07-.57A3.58,3.58,0,0,0,4.67,3.2H2v.9H.15V15.85H13.72V5.48ZM2.86,4.1H4.67a2.61,2.61,0,0,1,1,.17,2.32,2.32,0,0,1,.86.49v8.85a3.27,3.27,0,0,0-.88-.34,3.22,3.22,0,0,0-.93-.13H2.86ZM1,15V5H2v9H4.67a3.94,3.94,0,0,1,.61.06,3.2,3.2,0,0,1,.52.18,4.19,4.19,0,0,1,.49.29,2.28,2.28,0,0,1,.45.39ZM12.8,15H7.11a2.7,2.7,0,0,1,.47-.39A2.83,2.83,0,0,1,8,14.28a3.42,3.42,0,0,1,.54-.18A3.81,3.81,0,0,1,9.18,14h2.73V5h.89Z"/><polygon class="cls-2" points="13.2 3.56 13.2 3.58 13.19 3.57 13.2 3.56"/><path class="cls-2" d="M13.19,3.57h0v0Z"/><polygon class="cls-2" points="13.2 3.56 13.2 3.58 13.19 3.57 13.2 3.56"/><polygon class="cls-2" points="14.21 1.65 14.19 1.65 14.19 1.63 14.21 1.65"/><path class="cls-2" d="M15.91,2.1,14.2,3.81l-.38.38-.62-.61v0l1-1H12.79a3.35,3.35,0,0,0-1.09.26h0a3.94,3.94,0,0,0-.86.52l-.24.21s0,0,0,0a3.3,3.3,0,0,0-.51.67,3.1,3.1,0,0,0-.26.47A3.41,3.41,0,0,0,9.5,6.11H8.6a4.68,4.68,0,0,1,.16-1.19A4.74,4.74,0,0,1,9,4.26a2.21,2.21,0,0,1,.2-.41,4.66,4.66,0,0,1,.36-.51c.1-.13.22-.26.34-.39a4.14,4.14,0,0,1,.66-.53,1.19,1.19,0,0,1,.23-.16,2.79,2.79,0,0,1,.34-.18l.31-.13.42-.14a4.32,4.32,0,0,1,1.19-.16h1.15l-1-1L13.82,0Z"/></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{fill:#d02e00;}.cls-2{fill:#fff;}</style></defs><title>error_16x16</title><circle class="cls-1" cx="8.07" cy="8.07" r="7.93"/><polygon class="cls-2" points="8.82 8.07 11.53 10.78 10.83 11.48 8.12 8.78 5.41 11.48 4.7 10.78 7.42 8.07 4.65 5.31 5.36 4.61 8.12 7.37 10.83 4.67 11.53 5.36 8.82 8.07"/></svg>

After

Width:  |  Height:  |  Size: 414 B

View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{fill:#3bb44a;}</style></defs><title>success_16x16</title><path class="cls-1" d="M16,3.16,5.48,13.69,0,8.2l.89-.89,4.6,4.59,9.63-9.62Z"/></svg>

After

Width:  |  Height:  |  Size: 255 B

View File

@@ -0,0 +1,163 @@
<!--
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
-->
<div class="jobhistory-heading-container">
<h1 class="job-heading">Notebook | {{ this._agentNotebookInfo?.name }}</h1>
<div class="icon in-progress" *ngIf="showProgressWheel()"></div>
</div>
<!-- Back -->
<div
class="all-jobs"
tabindex="0"
(click)="goToJobs()"
(keyup.enter)="goToJobs()"
>
<div
class="back-button-icon"
(click)="goToJobs()"
(keyup.enter)="goToJobs()"
></div>
All Notebooks
</div>
<!-- Actions -->
<div #actionbarContainer class="agent-actionbar-container"></div>
<!-- Overview -->
<div class="overview-container">
<div class="overview-tab" (click)="toggleCollapse()" tabindex="0">
<input id="accordion" type="checkbox" />
<label for="accordion">
<div
class="resultsViewCollapsible collapsed"
(click)="toggleCollapse()"
></div>
Overview
</label>
<div class="accordion-content">
<table align="left">
<tr>
<td id="col1">
TargetDatabase:
</td>
<td id="col2">
{{ this._agentNotebookInfo?.targetDatabase }}
</td>
<td id="col3">
Enabled:
</td>
<td id="col4">
{{ this._agentNotebookInfo?.enabled }}
</td>
</tr>
<tr>
<td id="col1">
Last Run:
</td>
<td id="col2">
{{ this._agentNotebookInfo?.lastRun }}
</td>
<td id="col3">
Next Run:
</td>
<td id="col4">
{{ this._agentNotebookInfo?.nextRun }}
</td>
</tr>
</table>
</div>
</div>
</div>
<!-- Overview -->
<div class="history-container">
<div
class="overview-container"
*ngFor="let grid of this._grids; let i = index"
>
<div
class="overview-tab"
(click)="toggleGridCollapse(i)"
tabindex="{{i}}"
*ngIf="grid.histories?.length"
>
<input id="accordion{{ i }}" type="checkbox" class="grid-arrow" />
<label for="accordion{{ i }}">
<div id= "history-grid-icon{{i}}" (click)="toggleGridCollapse(i)"
class="resultsViewCollapsible"></div>
{{ grid.title }}
</label>
<div id="notebook-grid{{ i }}" class="notebook-grid {{ i }}" >
<div
*ngFor="let history of grid.histories"
class="notebook-grid-item"
(dblclick)="openNotebook(history)"
title="{{ createdTooltip(history) }}"
(contextmenu)="
openHistoryContextMenu($event, history, grid.contextMenuType)
"
>
<div
*ngIf="history.materializedNotebookErrorInfo"
class="img-error"
></div>
<div
*ngIf="
history.runStatus === 1 && !history.materializedNotebookErrorInfo
"
class="img-success"
></div>
<div
*ngIf="
history.runStatus === 0 && !history.materializedNotebookErrorInfo
"
class="img-failure"
></div>
<div
*ngIf="
!history.materializedNotebookName || history.materializedNotebookName === '';
else notebookRunName
"
>
<p style="text-align: center;">
{{ formatDateTimetoLocaleDate(history.runDate) }}
<br />
{{ formatDateTimetoLocaleTime(history.runDate) }}
</p>
</div>
<div
*ngIf="
history.materializedNotebookName !== '';
else notebookRunName
"
>
<p
style="text-align: center; text-overflow: ellipsis; overflow:hidden; white-space: nowrap"
>
{{ history.materializedNotebookName }}
</p>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Job History details -->
<div class="history-details">
<!-- Previous run list -->
<div class="prev-run-list-container" style="min-width: 270px">
<h3 *ngIf="_showPreviousRuns === false" style="text-align: center">
No Previous Runs Available
</h3>
<div
class="step-table prev-run-list"
style="position: relative; width: 100%"
>
<div #table style="position: absolute; width: 100%; height: 100%"></div>
</div>
</div>
</div>

View File

@@ -0,0 +1,608 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./media/jobHistory';
import * as azdata from 'azdata';
import * as nls from 'vs/nls';
import * as dom from 'vs/base/browser/dom';
import { OnInit, Component, Inject, Input, forwardRef, ElementRef, ChangeDetectorRef, ViewChild, ChangeDetectionStrategy, Injectable, PipeTransform, Pipe } from '@angular/core';
import { Taskbar } from 'sql/base/browser/ui/taskbar/taskbar';
import { AgentViewComponent } from 'sql/workbench/parts/jobManagement/browser/agentView.component';
import { CommonServiceInterface } from 'sql/platform/bootstrap/browser/commonServiceInterface.service';
import { RunJobAction, StopJobAction, JobsRefreshAction, EditNotebookJobAction, EditJobAction, OpenMaterializedNotebookAction, OpenTemplateNotebookAction, RenameNotebookMaterializedAction, PinNotebookMaterializedAction, UnpinNotebookMaterializedAction, DeleteMaterializedNotebookAction } from 'sql/platform/jobManagement/browser/jobActions';
import { NotebookCacheObject } from 'sql/platform/jobManagement/common/jobManagementService';
import { JobManagementUtilities } from 'sql/platform/jobManagement/browser/jobManagementUtilities';
import { IJobManagementService } from 'sql/platform/jobManagement/common/interfaces';
import { JobHistoryRow } from 'sql/workbench/parts/jobManagement/browser/jobHistoryTree';
import { JobStepsViewRow } from 'sql/workbench/parts/jobManagement/browser/jobStepsViewTree';
import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IAction } from 'vs/base/common/actions';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { JobManagementView, JobActionContext } from 'sql/workbench/parts/jobManagement/browser/jobManagementView';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { TabChild } from 'sql/base/browser/ui/panel/tab.component';
import { IDashboardService } from 'sql/platform/dashboard/browser/dashboardService';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import * as TelemetryKeys from 'sql/platform/telemetry/common/telemetryKeys';
import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput';
export const DASHBOARD_SELECTOR: string = 'notebookhistory-component';
export class GridSection {
title: string;
histories: azdata.AgentNotebookHistoryInfo[];
contextMenuType: number;
style: string;
}
@Component({
selector: DASHBOARD_SELECTOR,
templateUrl: decodeURI(require.toUrl('./notebookHistory.component.html')),
providers: [{ provide: TabChild, useExisting: forwardRef(() => NotebookHistoryComponent) }],
changeDetection: ChangeDetectionStrategy.OnPush
})
@Injectable()
export class NotebookHistoryComponent extends JobManagementView implements OnInit {
@ViewChild('table') private _tableContainer: ElementRef;
@ViewChild('jobsteps') private _jobStepsView: ElementRef;
@ViewChild('notebookHistoryActionbarContainer') private _notebookHistoryActionbarView: ElementRef;
@ViewChild('notebookgriditem') private _notebookGridItem: ElementRef;
@Input() public agentNotebookInfo: azdata.AgentNotebookInfo = undefined;
@Input() public agentJobHistories: azdata.AgentJobHistoryInfo[] = undefined;
public notebookHistories: azdata.AgentNotebookHistoryInfo[] = undefined;
public agentNotebookHistoryInfo: azdata.AgentNotebookHistoryInfo = undefined;
private _isVisible: boolean = false;
private _stepRows: JobStepsViewRow[] = [];
private _showSteps: boolean = undefined;
private _showPreviousRuns: boolean = undefined;
private _runStatus: string = undefined;
private _notebookCacheObject: NotebookCacheObject;
private _agentNotebookInfo: azdata.AgentNotebookInfo;
private _noJobsAvailable: boolean = false;
protected _notebookHistoryActionBar: Taskbar;
// Job Actions
private _editNotebookJobAction: EditNotebookJobAction;
private _runJobAction: RunJobAction;
private _stopJobAction: StopJobAction;
private _refreshAction: JobsRefreshAction;
private _openNotebookTemplateAction: OpenTemplateNotebookAction;
private static readonly HEADING_HEIGHT: number = 24;
private _grids: GridSection[] = [];
constructor(
@Inject(forwardRef(() => ChangeDetectorRef)) private _cd: ChangeDetectorRef,
@Inject(forwardRef(() => CommonServiceInterface)) commonService: CommonServiceInterface,
@Inject(forwardRef(() => AgentViewComponent)) _agentViewComponent: AgentViewComponent,
@Inject(IWorkbenchThemeService) private themeService: IWorkbenchThemeService,
@Inject(IInstantiationService) private instantiationService: IInstantiationService,
@Inject(IContextMenuService) contextMenuService: IContextMenuService,
@Inject(IJobManagementService) private _jobManagementService: IJobManagementService,
@Inject(ICommandService) private _commandService: ICommandService,
@Inject(IKeybindingService) keybindingService: IKeybindingService,
@Inject(IDashboardService) dashboardService: IDashboardService,
@Inject(ITelemetryService) private _telemetryService: ITelemetryService,
@Inject(IQuickInputService) private _quickInputService: IQuickInputService
) {
super(commonService, dashboardService, contextMenuService, keybindingService, instantiationService, _agentViewComponent);
let notebookCacheObjectMap = this._jobManagementService.notebookCacheObjectMap;
this._serverName = commonService.connectionManagementService.connectionInfo.connectionProfile.serverName;
let notebookCache = notebookCacheObjectMap[this._serverName];
if (notebookCache) {
this._notebookCacheObject = notebookCache;
} else {
this._notebookCacheObject = new NotebookCacheObject();
this._notebookCacheObject.serverName = this._serverName;
this._jobManagementService.addToCache(this._serverName, this._notebookCacheObject);
}
}
ngOnInit() {
// set base class elements
this._visibilityElement = this._tableContainer;
this._parentComponent = this._agentViewComponent;
this._agentNotebookInfo = this._agentViewComponent.agentNotebookInfo;
this.initActionBar();
const self = this;
this._telemetryService.publicLog(TelemetryKeys.JobHistoryView);
}
private loadHistory() {
const self = this;
let ownerUri: string = this._commonService.connectionManagementService.connectionInfo.ownerUri;
let jobName = this._agentViewComponent.agentNotebookInfo.name;
let jobId = this._agentViewComponent.notebookId;
let targetDatabase = this._agentViewComponent.agentNotebookInfo.targetDatabase;
this._jobManagementService.getNotebookHistory(ownerUri, jobId, jobName, targetDatabase).then((result) => {
if (result && result.histories) {
this.notebookHistories = result.histories;
self._notebookCacheObject.setNotebookHistory(jobId, this.notebookHistories);
self._notebookCacheObject.setJobSchedules(jobId, result.schedules);
self._notebookCacheObject.setJobSteps(jobId, result.steps);
this._agentViewComponent.agentNotebookInfo.jobSteps = this._notebookCacheObject.getJobSteps(jobId);
this._agentViewComponent.agentNotebookInfo.jobSchedules = this._notebookCacheObject.getJobSchedules(jobId);
this._agentNotebookInfo = this._agentViewComponent.agentNotebookInfo;
if (result.histories.length > 0) {
self._noJobsAvailable = false;
self._showPreviousRuns = true;
} else {
self._notebookCacheObject.setNotebookHistory(self._agentViewComponent.notebookId, result.histories);
self._noJobsAvailable = true;
self._showPreviousRuns = false;
}
} else {
self._noJobsAvailable = true;
self._showPreviousRuns = false;
self._showSteps = false;
}
this._actionBar.context = { targetObject: { canEdit: true, notebook: this._agentNotebookInfo, job: this._agentNotebookInfo }, ownerUri: this.ownerUri, component: this };
this._editNotebookJobAction.enabled = true;
this._actionBar.setContent([
{ action: this._runJobAction },
{ action: this._stopJobAction },
{ action: this._refreshAction },
{ action: this._editNotebookJobAction },
{ action: this._openNotebookTemplateAction }
]);
this.createGrid();
if (self._agentViewComponent.showNotebookHistory) {
self._cd.detectChanges();
this.collapseGrid();
}
});
}
private setStepsTree(element: JobHistoryRow) {
const self = this;
let cachedHistory = self._notebookCacheObject.getNotebookHistory(element.jobID);
if (cachedHistory) {
self.agentNotebookHistoryInfo = cachedHistory.find(
history => self.formatTime(history.runDate) === self.formatTime(element.runDate));
}
if (self.agentNotebookHistoryInfo) {
self.agentNotebookHistoryInfo.runDate = self.formatTime(self.agentNotebookHistoryInfo.runDate);
if (self.agentNotebookHistoryInfo.steps) {
let jobStepStatus = this.didJobFail(self.agentNotebookHistoryInfo);
self._stepRows = self.agentNotebookHistoryInfo.steps.map(step => {
let stepViewRow = new JobStepsViewRow();
stepViewRow.message = step.message;
stepViewRow.runStatus = jobStepStatus ? JobManagementUtilities.convertToStatusString(0) :
JobManagementUtilities.convertToStatusString(step.runStatus);
self._runStatus = JobManagementUtilities.convertToStatusString(self.agentNotebookHistoryInfo.runStatus);
stepViewRow.stepName = step.stepDetails.stepName;
stepViewRow.stepId = step.stepDetails.id.toString();
return stepViewRow;
});
self._stepRows.unshift(new JobStepsViewRow());
self._stepRows[0].rowID = 'stepsColumn' + self._agentNotebookInfo.jobId;
self._stepRows[0].stepId = nls.localize('stepRow.stepID', "Step ID");
self._stepRows[0].stepName = nls.localize('stepRow.stepName', "Step Name");
self._stepRows[0].message = nls.localize('stepRow.message', "Message");
this._showSteps = self._stepRows.length > 1;
} else {
self._showSteps = false;
}
if (self._agentViewComponent.showNotebookHistory) {
self._cd.detectChanges();
this.collapseGrid();
}
}
}
private didJobFail(job: azdata.AgentJobHistoryInfo): boolean {
for (let i = 0; i < job.steps.length; i++) {
if (job.steps[i].runStatus === 0) {
return true;
}
}
return false;
}
private toggleCollapse(): void {
let arrow: HTMLElement = jQuery('.resultsViewCollapsible').get(0);
let checkbox: any = document.getElementById('accordion');
if (arrow.className === 'resultsViewCollapsible' && checkbox.checked === false) {
arrow.className = 'resultsViewCollapsible collapsed';
} else if (arrow.className === 'resultsViewCollapsible collapsed' && checkbox.checked === true) {
arrow.className = 'resultsViewCollapsible';
}
}
private toggleGridCollapse(i): void {
let notebookGrid = document.getElementById('notebook-grid' + i);
let checkbox: any = document.getElementById('accordion' + i);
let arrow = document.getElementById('history-grid-icon' + i);
if (notebookGrid.className === 'notebook-grid ' + i && checkbox.checked === true) {
notebookGrid.className = 'notebook-grid ' + i + ' collapsed';
notebookGrid.style.display = 'none';
arrow.className = 'resultsViewCollapsible collapsed';
} else if (notebookGrid.className === 'notebook-grid ' + i + ' collapsed' && checkbox.checked === false) {
notebookGrid.className = 'notebook-grid ' + i;
notebookGrid.style.display = 'grid';
arrow.className = 'resultsViewCollapsible';
}
}
private toggleHistoryDisplay(event): void {
let header = event.srcElement.attributes;
}
private goToJobs(): void {
this._isVisible = false;
this._agentViewComponent.showNotebookHistory = false;
}
private convertToJobHistoryRow(historyInfo: azdata.AgentJobHistoryInfo): JobHistoryRow {
let jobHistoryRow = new JobHistoryRow();
jobHistoryRow.runDate = this.formatTime(historyInfo.runDate);
jobHistoryRow.runStatus = JobManagementUtilities.convertToStatusString(historyInfo.runStatus);
jobHistoryRow.instanceID = historyInfo.instanceId;
jobHistoryRow.jobID = historyInfo.jobId;
return jobHistoryRow;
}
private formatTime(time: string): string {
return time.replace('T', ' ');
}
private formatDateTimetoLocaleDate(time: string) {
let dateInstance = new Date(time);
return dateInstance.toLocaleDateString();
}
private formatDateTimetoLocaleTime(time: string) {
let dateInstance = new Date(time);
return dateInstance.toLocaleTimeString();
}
private showProgressWheel(): boolean {
return this._showPreviousRuns !== true && this._noJobsAvailable === false;
}
public onFirstVisible() {
this._agentNotebookInfo = this._agentViewComponent.agentNotebookInfo;
if (!this.agentNotebookInfo) {
this.agentNotebookInfo = this._agentNotebookInfo;
}
if (this.isRefreshing) {
this.loadHistory();
return;
}
else {
this.createGrid();
}
let notebookHistories = this._notebookCacheObject.notebookHistories[this._agentViewComponent.notebookId];
if (notebookHistories) {
if (notebookHistories.length > 0) {
const self = this;
this._noJobsAvailable = false;
if (this._notebookCacheObject.prevJobID === this._agentViewComponent.notebookId || notebookHistories[0].jobId === this._agentViewComponent.notebookId) {
this._showPreviousRuns = true;
this._agentViewComponent.agentNotebookInfo.jobSteps = this._notebookCacheObject.getJobSteps(this._agentNotebookInfo.jobId);
this._agentViewComponent.agentNotebookInfo.jobSchedules = this._notebookCacheObject.getJobSchedules(this._agentNotebookInfo.jobId);
this._agentNotebookInfo = this._agentViewComponent.agentNotebookInfo;
}
} else if (notebookHistories.length === 0) {
this._showPreviousRuns = false;
this._showSteps = false;
this._noJobsAvailable = true;
}
this._editNotebookJobAction.enabled = true;
this._actionBar.setContent([
{ action: this._runJobAction },
{ action: this._stopJobAction },
{ action: this._refreshAction },
{ action: this._editNotebookJobAction },
{ action: this._openNotebookTemplateAction }
]);
this._cd.detectChanges();
this.collapseGrid();
} else {
this.loadHistory();
}
this._notebookCacheObject.prevJobID = this._agentViewComponent.notebookId;
}
public layout() {
let historyDetails = jQuery('.overview-container').get(0);
let statusBar = jQuery('.part.statusbar').get(0);
if (historyDetails && statusBar) {
let historyBottom = historyDetails.getBoundingClientRect().bottom;
let statusTop = statusBar.getBoundingClientRect().top;
let height: number = statusTop - historyBottom - NotebookHistoryComponent.HEADING_HEIGHT;
if (this._table) {
this._table.layout(new dom.Dimension(
dom.getContentWidth(this._tableContainer.nativeElement),
height));
}
}
}
protected initActionBar() {
this._runJobAction = this.instantiationService.createInstance(RunJobAction);
this._stopJobAction = this.instantiationService.createInstance(StopJobAction);
this._editNotebookJobAction = this.instantiationService.createInstance(EditNotebookJobAction);
this._refreshAction = this.instantiationService.createInstance(JobsRefreshAction);
this._openNotebookTemplateAction = this.instantiationService.createInstance(OpenTemplateNotebookAction);
let taskbar = <HTMLElement>this.actionBarContainer.nativeElement;
this._actionBar = new Taskbar(taskbar);
this._editNotebookJobAction.enabled = !this.showProgressWheel();
let targetObject: JobActionContext = { canEdit: !this.showProgressWheel(), job: this._agentNotebookInfo };
this._actionBar.context = { targetObject: targetObject, ownerUri: this.ownerUri, component: this };
this._actionBar.setContent([
{ action: this._runJobAction },
{ action: this._stopJobAction },
{ action: this._refreshAction },
{ action: this._editNotebookJobAction },
{ action: this._openNotebookTemplateAction }
]);
}
public openNotebook(history: azdata.AgentNotebookHistoryInfo) {
if (history.runStatus === 0) {
return;
}
let ownerUri: string = this._commonService.connectionManagementService.connectionInfo.ownerUri;
let targetDatabase = this._agentViewComponent.agentNotebookInfo.targetDatabase;
this._jobManagementService.getMaterialziedNotebook(ownerUri, targetDatabase, history.materializedNotebookId).then(async (result) => {
if (result) {
let regex = /:|-/gi;
let readableDataTimeString = history.runDate.replace(regex, '').replace(' ', '');
let tempNotebookFileName = this._agentViewComponent.agentNotebookInfo.name + '_' + readableDataTimeString;
await this._commandService.executeCommand('agent.openNotebookEditorFromJsonString', tempNotebookFileName, result.notebookMaterialized);
}
});
}
public deleteMaterializedNotebook(history: azdata.AgentNotebookHistoryInfo) {
//TODO: Implement deletenotebook context menu action
let ownerUri: string = this._commonService.connectionManagementService.connectionInfo.ownerUri;
let targetDatabase = this._agentViewComponent.agentNotebookInfo.targetDatabase;
this._jobManagementService.deleteMaterializedNotebook(ownerUri, history, targetDatabase).then(async (result) => {
if (result) {
this.loadHistory();
}
});
}
public openTemplateNotebook() {
let ownerUri: string = this._commonService.connectionManagementService.connectionInfo.ownerUri;
let targetDatabase = this._agentViewComponent.agentNotebookInfo.targetDatabase;
let jobId = this._agentViewComponent.agentNotebookInfo.jobId;
this._jobManagementService.getTemplateNotebook(ownerUri, targetDatabase, jobId).then(async (result) => {
if (result) {
await this._commandService.executeCommand('agent.openNotebookEditorFromJsonString', this._agentViewComponent.agentNotebookInfo.name, result.notebookTemplate, this.agentNotebookInfo, ownerUri);
}
});
}
public renameNotebook(history: azdata.AgentNotebookHistoryInfo) {
const defaultDateTime = new Date(history.runDate).toLocaleDateString() + ' ' + new Date(history.runDate).toLocaleTimeString();
let notebookRunName = (history.materializedNotebookName === '') ? defaultDateTime : history.materializedNotebookName;
let ownerUri: string = this._commonService.connectionManagementService.connectionInfo.ownerUri;
let targetDatabase = this._agentViewComponent.agentNotebookInfo.targetDatabase;
let materializedNotebookId = history.materializedNotebookId;
this._quickInputService.input({ placeHolder: notebookRunName }).then(async (value) => {
if (value) {
if (!/\S/.test(value)) {
value = '';
}
await this._jobManagementService.updateNotebookMaterializedName(ownerUri, history, targetDatabase, value).then(async (result) => {
if (result) {
history.materializedNotebookName = value;
this.loadHistory();
}
});
}
});
}
public toggleNotebookPin(history: azdata.AgentNotebookHistoryInfo, pin: boolean) {
let ownerUri: string = this._commonService.connectionManagementService.connectionInfo.ownerUri;
let targetDatabase = this._agentViewComponent.agentNotebookInfo.targetDatabase;
let materializedNotebookId = history.materializedNotebookId;
this._jobManagementService.updateNotebookMaterializedPin(ownerUri, history, targetDatabase, pin).then(async (result) => {
if (result) {
history.materializedNotebookPin = pin;
this.loadHistory();
}
});
}
public openHistoryContextMenu(event: MouseEvent, history: azdata.AgentNotebookHistoryInfo, contextMenuType: number) {
let anchor = {
x: event.clientX,
y: event.clientY
};
let runDate = event.target['runDate'];
let gridActions = this.getGridActions();
let actionContext = {
component: this,
history: history
};
this._contextMenuService.showContextMenu({
getAnchor: () => anchor,
getActions: () => (contextMenuType === 1) ? this.getPinnedGridActions() : this.getGridActions(),
getKeyBinding: (action) => this._keybindingFor(action),
getActionsContext: () => (actionContext)
});
}
protected getGridActions(): IAction[] {
const openNotebookAction = this._instantiationService.createInstance(OpenMaterializedNotebookAction);
const renameNotebookAction = this._instantiationService.createInstance(RenameNotebookMaterializedAction);
const pinNotebookAction = this._instantiationService.createInstance(PinNotebookMaterializedAction);
const deleteMaterializedNotebookAction = this._instantiationService.createInstance(DeleteMaterializedNotebookAction);
return [
openNotebookAction,
renameNotebookAction,
pinNotebookAction,
deleteMaterializedNotebookAction
];
}
protected getPinnedGridActions(): IAction[] {
const openNotebookAction = this._instantiationService.createInstance(OpenMaterializedNotebookAction);
const renameNotebookAction = this._instantiationService.createInstance(RenameNotebookMaterializedAction);
const unpinNotebookAction = this._instantiationService.createInstance(UnpinNotebookMaterializedAction);
const deleteMaterializedNotebookAction = this._instantiationService.createInstance(DeleteMaterializedNotebookAction);
return [
openNotebookAction,
renameNotebookAction,
unpinNotebookAction,
deleteMaterializedNotebookAction
];
}
public createdTooltip(history: azdata.AgentNotebookHistoryInfo) {
let tooltipString: string = '';
if (history.materializedNotebookName && history.materializedNotebookName !== '') {
tooltipString = history.materializedNotebookName;
}
let dateOptions = {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric'
};
tooltipString += '\n' + nls.localize('notebookHistory.dateCreatedTooltip', "Date Created: ") + new Date(history.runDate).toLocaleDateString(undefined, dateOptions);
if (history.materializedNotebookErrorInfo && /\S/.test(history.materializedNotebookErrorInfo)) {
tooltipString += '\n' + nls.localize('notebookHistory.notebookErrorTooltip', "Notebook Error: ") + history.materializedNotebookErrorInfo;
}
if (history.runStatus === 0 && history.message && /\S/.test(history.message)) {
tooltipString += '\n' + nls.localize('notebookHistory.ErrorTooltip', "Job Error: ") + history.message;
}
return tooltipString;
}
public createGrid() {
let histories = this._notebookCacheObject.getNotebookHistory(this._agentViewComponent.notebookId);
histories = histories.sort((h1, h2) => {
return new Date(h2.runDate).getTime() - new Date(h1.runDate).getTime();
});
this._grids = [];
let tempHistory: azdata.AgentNotebookHistoryInfo[] = [];
for (let i = 0; i < histories.length; i++) {
if (histories[i].materializedNotebookPin) {
tempHistory.push(histories[i]);
}
}
this._grids.push({
title: nls.localize('notebookHistory.pinnedTitle', "Pinned"),
histories: tempHistory,
contextMenuType: 1,
style: 'grid'
});
// Pushing the pinned notebooks grid
tempHistory = [];
let count = 0;
let i = 0;
for (; i < histories.length; i++) {
if (!histories[i].materializedNotebookPin && count < 10) {
tempHistory.push(histories[i]);
count++;
}
if (count === 10) {
break;
}
}
this._grids.push({
title: nls.localize('notebookHistory.recentRunsTitle', "Recent Runs"),
histories: tempHistory,
contextMenuType: 0,
style: 'grid'
});
tempHistory = [];
for (i += 1; i < histories.length; i++) {
if (!histories[i].materializedNotebookPin) {
tempHistory.push(histories[i]);
}
}
this._grids.push({
title: nls.localize('notebookHistory.pastRunsTitle', "Past Runs"),
histories: tempHistory,
contextMenuType: 0,
style: 'none'
});
}
public collapseGrid() {
for (let i = 0; i < this._grids.length; i++) {
let notebookGrid = document.getElementById('notebook-grid' + i);
let arrow = document.getElementById('history-grid-icon' + i);
if (notebookGrid) {
let checkbox: any = document.getElementById('accordion' + i);
if (this._grids[i].style === 'none') {
notebookGrid.className = 'notebook-grid ' + i + ' collapsed';
arrow.className = 'resultsViewCollapsible collapsed';
notebookGrid.style.display = 'none';
checkbox.checked = true;
}
else {
notebookGrid.className = 'notebook-grid ' + i;
notebookGrid.style.display = 'grid';
arrow.className = 'resultsViewCollapsible';
checkbox.checked = false;
}
}
}
}
public refreshJobs() {
this._agentViewComponent.refresh = true;
this.loadHistory();
}
/** GETTERS */
public get showSteps(): boolean {
return this._showSteps;
}
public get stepRows() {
return this._stepRows;
}
public get ownerUri(): string {
return this._commonService.connectionManagementService.connectionInfo.ownerUri;
}
public get serverName(): string {
return this._serverName;
}
/** SETTERS */
public set showSteps(value: boolean) {
this._showSteps = value;
this._cd.detectChanges();
}
}

View File

@@ -0,0 +1,15 @@
<!--
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
-->
<div class="job-heading-container">
<h1 class="job-heading" *ngIf="_isCloud === false">Notebook Jobs</h1>
<h1 class="job-heading" *ngIf="_isCloud === true">No Notebooks Jobs Available</h1>
<div class="icon in-progress" *ngIf="_showProgressWheel === true"></div>
</div>
<div #actionbarContainer class="agent-actionbar-container"></div>
<div #notebooksgrid class="jobnotebooksview-grid"></div>

View File

@@ -0,0 +1,984 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./media/jobs';
import * as azdata from 'azdata';
import * as nls from 'vs/nls';
import * as dom from 'vs/base/browser/dom';
import { Component, Inject, forwardRef, ElementRef, ChangeDetectorRef, ViewChild, OnInit, OnDestroy } from '@angular/core';
import { TabChild } from 'sql/base/browser/ui/panel/tab.component';
import { Table } from 'sql/base/browser/ui/table/table';
import { AgentViewComponent } from 'sql/workbench/parts/jobManagement/browser/agentView.component';
import { RowDetailView } from 'sql/base/browser/ui/table/plugins/rowDetailView';
import { NotebookCacheObject } from 'sql/platform/jobManagement/common/jobManagementService';
import { EditJobAction, NewNotebookJobAction, RunJobAction, EditNotebookJobAction, JobsRefreshAction, IJobActionInfo, DeleteNotebookAction } from 'sql/platform/jobManagement/browser/jobActions';
import { JobManagementUtilities } from 'sql/platform/jobManagement/browser/jobManagementUtilities';
import { HeaderFilter } from 'sql/base/browser/ui/table/plugins/headerFilter.plugin';
import { IJobManagementService } from 'sql/platform/jobManagement/common/interfaces';
import { JobManagementView, JobActionContext } from 'sql/workbench/parts/jobManagement/browser/jobManagementView';
import { CommonServiceInterface } from 'sql/platform/bootstrap/browser/commonServiceInterface.service';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IAction } from 'vs/base/common/actions';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IDashboardService } from 'sql/platform/dashboard/browser/dashboardService';
import { escape } from 'sql/base/common/strings';
import { IWorkbenchThemeService, IColorTheme } from 'vs/workbench/services/themes/common/workbenchThemeService';
import { tableBackground, cellBackground, cellBorderColor } from 'sql/platform/theme/common/colors';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import * as TelemetryKeys from 'sql/platform/telemetry/common/telemetryKeys';
import { attachButtonStyler } from 'sql/platform/theme/common/styler';
import { Taskbar } from 'sql/base/browser/ui/taskbar/taskbar';
export const NOTEBOOKSVIEW_SELECTOR: string = 'notebooksview-component';
export const ROW_HEIGHT: number = 45;
export const ACTIONBAR_PADDING: number = 10;
interface IItem extends Slick.SlickData {
notebookId?: string;
id: string;
}
@Component({
selector: NOTEBOOKSVIEW_SELECTOR,
templateUrl: decodeURI(require.toUrl('./notebooksView.component.html')),
providers: [{ provide: TabChild, useExisting: forwardRef(() => NotebooksViewComponent) }],
})
export class NotebooksViewComponent extends JobManagementView implements OnInit, OnDestroy {
private columns: Array<Slick.Column<any>> = [
{
name: nls.localize('notebookColumns.name', "Name"),
field: 'name',
formatter: (row, cell, value, columnDef, dataContext) => this.renderName(row, cell, value, columnDef, dataContext),
width: 150,
id: 'name'
},
{ name: nls.localize('notebookColumns.targetDatbase', "Target Database"), field: 'targetDatabase', width: 80, id: 'targetDatabase' },
{ name: nls.localize('notebookColumns.lastRun', "Last Run"), field: 'lastRun', width: 80, id: 'lastRun' },
{ name: nls.localize('notebookColumns.nextRun', "Next Run"), field: 'nextRun', width: 80, id: 'nextRun' },
{ name: nls.localize('notebookColumns.status', "Status"), field: 'currentExecutionStatus', width: 50, id: 'currentExecutionStatus' },
{ name: nls.localize('notebookColumns.lastRunOutcome', "Last Run Outcome"), field: 'lastRunOutcome', width: 100, id: 'lastRunOutcome' },
{
name: nls.localize('notebookColumns.previousRuns', "Previous Runs"),
formatter: (row, cell, value, columnDef, dataContext) => this.renderChartsPostHistory(row, cell, value, columnDef, dataContext),
field: 'previousRuns',
width: 100,
id: 'previousRuns'
}
];
private _notebookCacheObject: NotebookCacheObject;
private rowDetail: RowDetailView<IItem>;
private filterPlugin: any;
private dataView: any;
private _isCloud: boolean;
private filterStylingMap: { [columnName: string]: [any]; } = {};
private filterStack = ['start'];
private filterValueMap: { [columnName: string]: string[]; } = {};
private sortingStylingMap: { [columnName: string]: any; } = {};
public notebooks: azdata.AgentNotebookInfo[];
private notebookHistories: { [jobId: string]: azdata.AgentNotebookHistoryInfo[]; } = Object.create(null);
private jobSteps: { [jobId: string]: azdata.AgentJobStepInfo[]; } = Object.create(null);
private jobAlerts: { [jobId: string]: azdata.AgentAlertInfo[]; } = Object.create(null);
private jobSchedules: { [jobId: string]: azdata.AgentJobScheduleInfo[]; } = Object.create(null);
public contextAction = NewNotebookJobAction;
@ViewChild('notebooksgrid') _gridEl: ElementRef;
constructor(
@Inject(forwardRef(() => CommonServiceInterface)) commonService: CommonServiceInterface,
@Inject(forwardRef(() => ChangeDetectorRef)) private _cd: ChangeDetectorRef,
@Inject(forwardRef(() => ElementRef)) private _el: ElementRef,
@Inject(forwardRef(() => AgentViewComponent)) _agentViewComponent: AgentViewComponent,
@Inject(IJobManagementService) private _jobManagementService: IJobManagementService,
@Inject(IWorkbenchThemeService) private _themeService: IWorkbenchThemeService,
@Inject(ICommandService) private _commandService: ICommandService,
@Inject(IInstantiationService) instantiationService: IInstantiationService,
@Inject(IContextMenuService) contextMenuService: IContextMenuService,
@Inject(IKeybindingService) keybindingService: IKeybindingService,
@Inject(IDashboardService) _dashboardService: IDashboardService,
@Inject(ITelemetryService) private _telemetryService: ITelemetryService
) {
super(commonService, _dashboardService, contextMenuService, keybindingService, instantiationService, _agentViewComponent);
let notebookCacheObjectMap = this._jobManagementService.notebookCacheObjectMap;
let jobCache = notebookCacheObjectMap[this._serverName];
if (jobCache) {
this._notebookCacheObject = jobCache;
} else {
this._notebookCacheObject = new NotebookCacheObject();
this._notebookCacheObject.serverName = this._serverName;
this._jobManagementService.addToCache(this._serverName, this._notebookCacheObject);
}
this._isCloud = commonService.connectionManagementService.connectionInfo.serverInfo.isCloud;
}
ngOnInit() {
// set base class elements
this._visibilityElement = this._gridEl;
this._parentComponent = this._agentViewComponent;
this._register(this._themeService.onDidColorThemeChange(e => this.updateTheme(e)));
this._telemetryService.publicLog(TelemetryKeys.JobsView);
}
ngOnDestroy() {
}
public layout() {
let jobsViewToolbar = jQuery('notebooksview-component .agent-actionbar-container').get(0);
let statusBar = jQuery('.part.statusbar').get(0);
if (jobsViewToolbar && statusBar) {
let toolbarBottom = jobsViewToolbar.getBoundingClientRect().bottom + ACTIONBAR_PADDING;
let statusTop = statusBar.getBoundingClientRect().top;
this._table.layout(new dom.Dimension(
dom.getContentWidth(this._gridEl.nativeElement),
statusTop - toolbarBottom));
}
}
onFirstVisible() {
let self = this;
let cached: boolean = false;
if (this._notebookCacheObject.serverName === this._serverName && this._notebookCacheObject.notebooks.length > 0) {
cached = true;
this.notebooks = this._notebookCacheObject.notebooks;
}
let columns = this.columns.map((column) => {
column.rerenderOnResize = true;
return column;
});
let options = <Slick.GridOptions<any>>{
syncColumnCellResize: true,
enableColumnReorder: false,
rowHeight: ROW_HEIGHT,
enableCellNavigation: true,
forceFitColumns: false
};
this.dataView = new Slick.Data.DataView({ inlineFilters: false });
let rowDetail = new RowDetailView<IItem>({
cssClass: '_detail_selector',
process: (job) => {
(<any>rowDetail).onAsyncResponse.notify({
'itemDetail': job
}, undefined, this);
},
useRowClick: false,
panelRows: 1,
postTemplate: () => '', // I'm assuming these code paths are just never hit...
preTemplate: () => '',
});
this.rowDetail = rowDetail;
columns.unshift(this.rowDetail.getColumnDefinition());
let filterPlugin = new HeaderFilter<{ inlineFilters: false }>();
this._register(attachButtonStyler(filterPlugin, this._themeService));
this.filterPlugin = filterPlugin;
jQuery(this._gridEl.nativeElement).empty();
jQuery(this.actionBarContainer.nativeElement).empty();
this.initActionBar();
this._table = this._register(new Table(this._gridEl.nativeElement, { columns }, options));
this._table.grid.setData(this.dataView, true);
this._table.grid.onClick.subscribe((e, args) => {
let notebook = self.getNotebook(args);
self._agentViewComponent.notebookId = notebook.jobId;
self._agentViewComponent.agentNotebookInfo = notebook;
self._agentViewComponent.showNotebookHistory = true;
});
this._register(this._table.onContextMenu(e => {
self.openContextMenu(e);
}));
if (cached && this._agentViewComponent.refresh !== true) {
this.onNotebooksAvailable(null);
this._showProgressWheel = false;
if (this.isVisible) {
this._cd.detectChanges();
}
} else {
let ownerUri: string = this._commonService.connectionManagementService.connectionInfo.ownerUri;
this._jobManagementService.getNotebooks(ownerUri).then((result) => {
if (result && result.notebooks) {
self.notebooks = result.notebooks;
self._notebookCacheObject.notebooks = self.notebooks;
self.onNotebooksAvailable(result.notebooks);
}
this._showProgressWheel = false;
if (this.isVisible) {
this._cd.detectChanges();
}
});
}
}
protected initActionBar() {
let refreshAction = this._instantiationService.createInstance(JobsRefreshAction);
let newAction = this._instantiationService.createInstance(NewNotebookJobAction);
let taskbar = <HTMLElement>this.actionBarContainer.nativeElement;
this._actionBar = new Taskbar(taskbar);
this._actionBar.setContent([
{ action: refreshAction },
{ action: newAction }
]);
let context: IJobActionInfo = { component: this, ownerUri: this._commonService.connectionManagementService.connectionInfo.ownerUri };
this._actionBar.context = context;
}
private onNotebooksAvailable(notebooks: azdata.AgentNotebookInfo[]) {
let jobViews: any;
let start: boolean = true;
if (!notebooks) {
let dataView = this._notebookCacheObject.dataView;
jobViews = dataView.getItems();
start = false;
} else {
jobViews = notebooks.map((job) => {
return {
id: 'notebook' + job.jobId,
notebookId: job.jobId,
name: job.name,
targetDatabase: job.targetDatabase,
lastRun: JobManagementUtilities.convertToLastRun(job.lastRun),
nextRun: JobManagementUtilities.convertToNextRun(job.nextRun),
currentExecutionStatus: JobManagementUtilities.convertToExecutionStatusString(job.currentExecutionStatus),
lastRunOutcome: (job.lastRunNotebookError === '') ? JobManagementUtilities.convertToStatusString(job.lastRunOutcome) : 'Notebook Error'
};
});
}
this._table.registerPlugin(<any>this.rowDetail);
this.filterPlugin.onFilterApplied.subscribe((e, args) => {
this.dataView.refresh();
this._table.grid.resetActiveCell();
let filterValues = args.column.filterValues;
if (filterValues) {
if (filterValues.length === 0) {
// if an associated styling exists with the current filters
if (this.filterStylingMap[args.column.name]) {
let filterLength = this.filterStylingMap[args.column.name].length;
// then remove the filtered styling
for (let i = 0; i < filterLength; i++) {
let lastAppliedStyle = this.filterStylingMap[args.column.name].pop();
this._table.grid.removeCellCssStyles(lastAppliedStyle[0]);
}
delete this.filterStylingMap[args.column.name];
let index = this.filterStack.indexOf(args.column.name, 0);
if (index > -1) {
this.filterStack.splice(index, 1);
delete this.filterValueMap[args.column.name];
}
// apply the previous filter styling
let currentItems = this.dataView.getFilteredItems();
let styledItems = this.filterValueMap[this.filterStack[this.filterStack.length - 1]][1];
if (styledItems === currentItems) {
let lastColStyle = this.filterStylingMap[this.filterStack[this.filterStack.length - 1]];
for (let i = 0; i < lastColStyle.length; i++) {
this._table.grid.setCellCssStyles(lastColStyle[i][0], lastColStyle[i][1]);
}
} else {
// style it all over again
let seenJobs = 0;
for (let i = 0; i < currentItems.length; i++) {
this._table.grid.removeCellCssStyles('error-row' + i.toString());
this._table.grid.removeCellCssStyles('notebook-error-row' + i.toString());
let item = this.dataView.getFilteredItems()[i];
if (item.lastRunOutcome === 'Failed') {
this.addToStyleHash(seenJobs, false, this.filterStylingMap, args.column.name);
if (this.filterStack.indexOf(args.column.name) < 0) {
this.filterStack.push(args.column.name);
this.filterValueMap[args.column.name] = [filterValues];
}
// one expansion for the row and one for
// the error detail
seenJobs++;
i++;
}
seenJobs++;
}
this.dataView.refresh();
this.filterValueMap[args.column.name].push(this.dataView.getFilteredItems());
this._table.grid.resetActiveCell();
}
if (this.filterStack.length === 0) {
this.filterStack = ['start'];
}
}
} else {
let seenNotebooks = 0;
for (let i = 0; i < this.notebooks.length; i++) {
this._table.grid.removeCellCssStyles('error-row' + i.toString());
this._table.grid.removeCellCssStyles('notebook-error-row' + i.toString());
let item = this.dataView.getItemByIdx(i);
// current filter
if (_.contains(filterValues, item[args.column.field])) {
// check all previous filters
if (this.checkPreviousFilters(item)) {
if (item.lastRunOutcome === 'Failed') {
this.addToStyleHash(seenNotebooks, false, this.filterStylingMap, args.column.name);
if (this.filterStack.indexOf(args.column.name) < 0) {
this.filterStack.push(args.column.name);
this.filterValueMap[args.column.name] = [filterValues];
}
// one expansion for the row and one for
// the error detail
seenNotebooks++;
i++;
}
seenNotebooks++;
}
}
}
this.dataView.refresh();
if (this.filterValueMap[args.column.name]) {
this.filterValueMap[args.column.name].push(this.dataView.getFilteredItems());
} else {
this.filterValueMap[args.column.name] = this.dataView.getFilteredItems();
}
this._table.grid.resetActiveCell();
}
} else {
this.expandJobs(false);
}
});
this.filterPlugin.onCommand.subscribe((e, args: any) => {
this.columnSort(args.column.name, args.command === 'sort-asc');
});
this._table.registerPlugin(this.filterPlugin);
this.dataView.beginUpdate();
this.dataView.setItems(jobViews);
this.dataView.setFilter((item) => this.filter(item));
this.dataView.endUpdate();
this._table.autosizeColumns();
this._table.resizeCanvas();
this.expandJobs(start);
// tooltip for job name
jQuery('.jobview-jobnamerow').hover(e => {
let currentTarget = e.currentTarget;
currentTarget.title = currentTarget.innerText;
});
const self = this;
this._table.grid.onColumnsResized.subscribe((e, data: any) => {
let nameWidth: number = data.grid.getColumns()[1].width;
// adjust job name when resized
jQuery('#notebooksDiv .jobnotebooksview-grid .slick-cell.l1.r1 .jobview-jobnametext').css('width', `${nameWidth - 10}px`);
// adjust error message when resized
jQuery('#notebooksDiv .jobnotebooksview-grid .slick-cell.l1.r1.error-row .jobview-jobnametext').css('width', '100%');
jQuery('#notebooksDiv .jobnotebooksview-grid .slick-cell.l1.r1.notebook-error-row .jobview-jobnametext').css('width', '100%');
// generate job charts again
self.notebooks.forEach(job => {
let jobHistories = self._notebookCacheObject.getNotebookHistory(job.jobId);
if (jobHistories) {
let previousRuns = jobHistories.slice(jobHistories.length - 5, jobHistories.length);
self.createJobChart('notebook' + job.jobId, previousRuns);
}
});
});
jQuery('#notebooksDiv .jobnotebooksview-grid .monaco-table .slick-viewport .grid-canvas .ui-widget-content.slick-row').hover((e1) =>
this.highlightErrorRows(e1), (e2) => this.hightlightNonErrorRows(e2));
this._table.grid.onScroll.subscribe((e) => {
jQuery('#notebooksDiv .jobnotebooksview-grid .monaco-table .slick-viewport .grid-canvas .ui-widget-content.slick-row').hover((e1) =>
this.highlightErrorRows(e1), (e2) => this.hightlightNonErrorRows(e2));
});
// cache the dataview for future use
this._notebookCacheObject.dataView = this.dataView;
this.filterValueMap['start'] = [[], this.dataView.getItems()];
this.loadJobHistories();
}
private highlightErrorRows(e) {
// highlight the error row as well if a failing job row is hovered
if (e.currentTarget.children.item(0).classList.contains('job-with-error')) {
let target = jQuery(e.currentTarget);
let targetChildren = jQuery(e.currentTarget.children);
let siblings = target.nextAll().toArray();
let top = parseInt(target.css('top'), 10);
for (let i = 0; i < siblings.length; i++) {
let sibling = siblings[i];
let siblingTop = parseInt(jQuery(sibling).css('top'), 10);
if (siblingTop === top + ROW_HEIGHT) {
jQuery(sibling.children).addClass('hovered');
sibling.onmouseenter = (e) => {
targetChildren.addClass('hovered');
};
sibling.onmouseleave = (e) => {
targetChildren.removeClass('hovered');
};
break;
}
}
}
}
private hightlightNonErrorRows(e) {
// switch back to original background
if (e.currentTarget.children.item(0).classList.contains('job-with-error')) {
let target = jQuery(e.currentTarget);
let siblings = target.nextAll().toArray();
let top = parseInt(target.css('top'), 10);
for (let i = 0; i < siblings.length; i++) {
let sibling = siblings[i];
let siblingTop = parseInt(jQuery(sibling).css('top'), 10);
if (siblingTop === top + ROW_HEIGHT) {
jQuery(sibling.children).removeClass('hovered');
break;
}
}
}
}
private setRowWithErrorClass(hash: { [index: number]: { [id: string]: string; } }, row: number, errorClass: string) {
hash[row] = {
'_detail_selector': errorClass,
'id': errorClass,
'jobId': errorClass,
'name': errorClass,
'targetDatabase': errorClass,
'lastRun': errorClass,
'nextRun': errorClass,
'currentExecutionStatus': errorClass,
'lastRunOutcome': errorClass,
'previousRuns': errorClass
};
return hash;
}
private addToStyleHash(row: number, start: boolean, map: any, columnName: string) {
let hash: {
[index: number]: {
[id: string]: string;
}
} = {};
hash = this.setRowWithErrorClass(hash, row, 'job-with-error');
hash = this.setRowWithErrorClass(hash, row + 1, 'error-row');
if (start) {
if (map['start']) {
map['start'].push(['error-row' + row.toString(), hash]);
} else {
map['start'] = [['error-row' + row.toString(), hash]];
}
} else {
if (map[columnName]) {
map[columnName].push(['error-row' + row.toString(), hash]);
} else {
map[columnName] = [['error-row' + row.toString(), hash]];
}
}
this._table.grid.setCellCssStyles('error-row' + row.toString(), hash);
}
private addToErrorStyleHash(row: number, start: boolean, map: any, columnName: string) {
let hash: {
[index: number]: {
[id: string]: string;
}
} = {};
hash = this.setRowWithErrorClass(hash, row, 'job-with-error');
hash = this.setRowWithErrorClass(hash, row + 1, 'notebook-error-row');
if (start) {
if (map['start']) {
map['start'].push(['notebook-error-row' + row.toString(), hash]);
} else {
map['start'] = [['notebook-error-row' + row.toString(), hash]];
}
} else {
if (map[columnName]) {
map[columnName].push(['notebook-error-row' + row.toString(), hash]);
} else {
map[columnName] = [['notebook-error-row' + row.toString(), hash]];
}
}
this._table.grid.setCellCssStyles('notebook-error-row' + row.toString(), hash);
}
private renderName(row, cell, value, columnDef, dataContext) {
let resultIndicatorClass: string;
switch (dataContext.lastRunOutcome) {
case ('Succeeded'):
resultIndicatorClass = 'jobview-jobnameindicatorsuccess';
break;
case ('Failed'):
resultIndicatorClass = 'jobview-jobnameindicatorfailure';
break;
case ('Cancelled'):
resultIndicatorClass = 'jobview-jobnameindicatorcancel';
break;
case ('Status Unknown'):
resultIndicatorClass = 'jobview-jobnameindicatorunknown';
break;
case ('Notebook Error'):
resultIndicatorClass = 'jobview-jobnameindicatorcancel';
break;
default:
resultIndicatorClass = 'jobview-jobnameindicatorfailure';
break;
}
return '<table class="jobview-jobnametable"><tr class="jobview-jobnamerow">' +
'<td nowrap class=' + resultIndicatorClass + '></td>' +
'<td nowrap class="jobview-jobnametext">' + escape(dataContext.name) + '</td>' +
'</tr></table>';
}
private renderChartsPostHistory(row, cell, value, columnDef, dataContext) {
let runChart = this._notebookCacheObject.getRunChart(dataContext.id);
if (runChart && runChart.length > 0) {
return `<table class="jobprevruns" id="${dataContext.id}">
<tr>
<td>${runChart[0] ? runChart[0] : '<div class="bar0"></div>'}</td>
<td>${runChart[1] ? runChart[1] : '<div class="bar1"></div>'}</td>
<td>${runChart[2] ? runChart[2] : '<div class="bar2"></div>'}</td>
<td>${runChart[3] ? runChart[3] : '<div class="bar3"></div>'}</td>
<td>${runChart[4] ? runChart[4] : '<div class="bar4"></div>'}</td>
</tr>
</table>`;
} else {
return `<table class="jobprevruns" id="${dataContext.id}">
<tr>
<td><div class="bar0"></div></td>
<td><div class="bar1"></div></td>
<td><div class="bar2"></div></td>
<td><div class="bar3"></div></td>
<td><div class="bar4"></div></td>
</tr>
</table>`;
}
}
private expandJobRowDetails(rowIdx: number, message?: string): void {
let item = this.dataView.getItemByIdx(rowIdx);
item.message = this._agentViewComponent.expandedNotebook.get(item.notebookId);
this.rowDetail.applyTemplateNewLineHeight(item, true);
}
private async loadJobHistories() {
if (this.notebooks) {
let ownerUri: string = this._commonService.connectionManagementService.connectionInfo.ownerUri;
let separatedJobs = this.separateFailingJobs();
// grab histories of the failing jobs first
// so they can be expanded quicker
let failing = separatedJobs[0];
let passing = separatedJobs[1];
Promise.all([this.curateJobHistory(failing, ownerUri), this.curateJobHistory(passing, ownerUri)]);
}
}
private separateFailingJobs(): azdata.AgentNotebookInfo[][] {
let failing = [];
let nonFailing = [];
for (let i = 0; i < this.notebooks.length; i++) {
if (this.notebooks[i].lastRunOutcome === 0) {
failing.push(this.notebooks[i]);
} else {
nonFailing.push(this.notebooks[i]);
}
}
return [failing, nonFailing];
}
private checkPreviousFilters(item): boolean {
for (let column in this.filterValueMap) {
if (column !== 'start' && this.filterValueMap[column][0].length > 0) {
if (!_.contains(this.filterValueMap[column][0], item[JobManagementUtilities.convertColNameToField(column)])) {
return false;
}
}
}
return true;
}
private isErrorRow(cell: HTMLElement) {
return cell.classList.contains('error-row') || cell.classList.contains('notebook-error-row');
}
private getNotebook(args: Slick.OnClickEventArgs<any>): azdata.AgentNotebookInfo {
let row = args.row;
let notebookName: string;
let cell = args.grid.getCellNode(row, 1);
if (this.isErrorRow(cell)) {
notebookName = args.grid.getCellNode(row - 1, 1).innerText.trim();
} else {
notebookName = cell.innerText.trim();
}
let notebook = this.notebooks.filter(job => job.name === notebookName)[0];
return notebook;
}
private async curateJobHistory(notebooks: azdata.AgentNotebookInfo[], ownerUri: string) {
const self = this;
for (let notebook of notebooks) {
let result = await this._jobManagementService.getNotebookHistory(ownerUri, notebook.jobId, notebook.name, notebook.targetDatabase);
if (result) {
self.jobSteps[notebook.jobId] = result.steps ? result.steps : [];
self.jobSchedules[notebook.jobId] = result.schedules ? result.schedules : [];
self.notebookHistories[notebook.jobId] = result.histories ? result.histories : [];
self._notebookCacheObject.setJobSteps(notebook.jobId, self.jobSteps[notebook.jobId]);
self._notebookCacheObject.setNotebookHistory(notebook.jobId, self.notebookHistories[notebook.jobId]);
self._notebookCacheObject.setJobSchedules(notebook.jobId, self.jobSchedules[notebook.jobId]);
let notebookHistories = self._notebookCacheObject.getNotebookHistory(notebook.jobId);
let previousRuns: azdata.AgentNotebookHistoryInfo[];
if (notebookHistories.length >= 5) {
previousRuns = notebookHistories.slice(notebookHistories.length - 5, notebookHistories.length);
} else {
previousRuns = notebookHistories;
}
if (self._agentViewComponent.expandedNotebook.has(notebook.jobId)) {
let lastJobHistory = notebookHistories[notebookHistories.length - 1];
let item = self.dataView.getItemById('notebook' + notebook.jobId + '.error');
let noStepsMessage = nls.localize('notebooksView.noSteps', "No Steps available for this job.");
let errorMessage = lastJobHistory ? lastJobHistory.message : noStepsMessage;
if (item) {
if (notebook.lastRunNotebookError.length === 0) {
item['name'] = nls.localize('notebooksView.error', "Error: ") + errorMessage;
}
else {
item['name'] = nls.localize('notebooksView.notebookError', "Notebook Error: ") + notebook.lastRunNotebookError;
}
self._agentViewComponent.setExpandedNotebook(notebook.jobId, item['name']);
self.dataView.updateItem('notebook' + notebook.jobId + '.error', item);
}
}
self.createJobChart('notebook' + notebook.jobId, previousRuns);
}
}
}
private createJobChart(jobId: string, jobHistories: azdata.AgentNotebookHistoryInfo[]): void {
let chartHeights = this.getChartHeights(jobHistories);
let runCharts = [];
for (let i = 0; i < chartHeights.length; i++) {
let bgColor = jobHistories[i].runStatus === 0 ? 'red' : 'green';
if (jobHistories[i].materializedNotebookErrorInfo !== null && jobHistories[i].materializedNotebookErrorInfo.length > 0) {
bgColor = 'orange';
}
let runGraph = jQuery(`table.jobprevruns#${jobId} > tbody > tr > td > div.bar${i}`);
if (runGraph.length > 0) {
runGraph.css('height', chartHeights[i]);
runGraph.css('background', bgColor);
runGraph.hover((e) => {
let currentTarget = e.currentTarget;
currentTarget.title = jobHistories[i].runDuration;
});
runCharts.push(runGraph.get(0).outerHTML);
}
}
if (runCharts.length > 0) {
this._notebookCacheObject.setRunChart(jobId, runCharts);
}
this._cd.detectChanges();
}
// chart height normalization logic
private getChartHeights(jobHistories: azdata.AgentJobHistoryInfo[]): string[] {
if (!jobHistories || jobHistories.length === 0) {
return [];
}
let maxDuration: number = 0;
jobHistories.forEach(history => {
let historyDuration = JobManagementUtilities.convertDurationToSeconds(history.runDuration);
if (historyDuration > maxDuration) {
maxDuration = historyDuration;
}
});
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 Array(jobHistories.length).fill('5px');
} else {
return chartHeights;
}
}
private expandJobs(start: boolean): void {
if (start) {
this._agentViewComponent.expandedNotebook = new Map<string, string>();
}
let expandedJobs = this._agentViewComponent.expandedNotebook;
let expansions = 0;
for (let i = 0; i < this.notebooks.length; i++) {
let notebook = this.notebooks[i];
if (notebook.lastRunOutcome === 0 && !expandedJobs.get(notebook.jobId)) {
this.expandJobRowDetails(i + expandedJobs.size);
this.addToStyleHash(i + expandedJobs.size, start, this.filterStylingMap, undefined);
this._agentViewComponent.setExpandedNotebook(notebook.jobId, 'Loading Error...');
} else if (notebook.lastRunOutcome === 0 && expandedJobs.get(notebook.jobId)) {
this.addToStyleHash(i + expansions, start, this.filterStylingMap, undefined);
expansions++;
} else if (notebook.lastRunNotebookError !== '' && !expandedJobs.get(notebook.jobId)) {
this.expandJobRowDetails(i + expandedJobs.size);
this.addToErrorStyleHash(i + expandedJobs.size, start, this.filterStylingMap, undefined);
this._agentViewComponent.setExpandedNotebook(notebook.jobId, notebook.lastRunNotebookError);
} else if (notebook.lastRunNotebookError !== '' && expandedJobs.get(notebook.jobId)) {
this.addToErrorStyleHash(i + expansions, start, this.filterStylingMap, undefined);
expansions++;
}
}
}
private filter(item: any) {
let columns = this._table.grid.getColumns();
let value = true;
for (let i = 0; i < columns.length; i++) {
let col: any = columns[i];
let filterValues = col.filterValues;
if (filterValues && filterValues.length > 0) {
if (item._parent) {
value = value && _.contains(filterValues, item._parent[col.field]);
} else {
value = value && _.contains(filterValues, item[col.field]);
}
}
}
return value;
}
private columnSort(column: string, isAscending: boolean) {
let items = this.dataView.getItems();
// get error items here and remove them
let jobItems = items.filter(x => x._parent === undefined);
let errorItems = items.filter(x => x._parent !== undefined);
this.sortingStylingMap[column] = items;
switch (column) {
case ('Name'): {
this.dataView.setItems(jobItems);
// sort the actual jobs
this.dataView.sort((item1, item2) => {
return item1.name.localeCompare(item2.name);
}, isAscending);
break;
}
case ('Last Run'): {
this.dataView.setItems(jobItems);
// sort the actual jobs
this.dataView.sort((item1, item2) => this.dateCompare(item1, item2, true), isAscending);
break;
}
case ('Next Run'): {
this.dataView.setItems(jobItems);
// sort the actual jobs
this.dataView.sort((item1, item2) => this.dateCompare(item1, item2, false), isAscending);
break;
}
case ('Status'): {
this.dataView.setItems(jobItems);
// sort the actual jobs
this.dataView.sort((item1, item2) => {
return item1.currentExecutionStatus.localeCompare(item2.currentExecutionStatus);
}, isAscending);
break;
}
case ('Last Run Outcome'): {
this.dataView.setItems(jobItems);
// sort the actual jobs
this.dataView.sort((item1, item2) => {
return item1.lastRunOutcome.localeCompare(item2.lastRunOutcome);
}, isAscending);
break;
}
}
// insert the errors back again
let jobItemsLength = jobItems.length;
for (let i = 0; i < jobItemsLength; i++) {
let item = jobItems[i];
if (item._child) {
let child = errorItems.find(error => error === item._child);
jobItems.splice(i + 1, 0, child);
jobItemsLength++;
}
}
this.dataView.setItems(jobItems);
// remove old style
if (this.filterStylingMap[column]) {
let filterLength = this.filterStylingMap[column].length;
for (let i = 0; i < filterLength; i++) {
let lastAppliedStyle = this.filterStylingMap[column].pop();
this._table.grid.removeCellCssStyles(lastAppliedStyle[0]);
}
} else {
for (let i = 0; i < this.notebooks.length; i++) {
this._table.grid.removeCellCssStyles('error-row' + i.toString());
this._table.grid.removeCellCssStyles('notebook-error-row' + i.toString());
}
}
// add new style to the items back again
items = this.filterStack.length > 1 ? this.dataView.getFilteredItems() : this.dataView.getItems();
for (let i = 0; i < items.length; i++) {
let item = items[i];
if (item.lastRunOutcome === 'Failed') {
this.addToStyleHash(i, false, this.sortingStylingMap, column);
}
}
}
private dateCompare(item1: any, item2: any, lastRun: boolean): number {
let exceptionString = lastRun ? 'Never Run' : 'Not Scheduled';
if (item2.lastRun === exceptionString && item1.lastRun !== exceptionString) {
return -1;
} else if (item1.lastRun === exceptionString && item2.lastRun !== exceptionString) {
return 1;
} else if (item1.lastRun === exceptionString && item2.lastRun === exceptionString) {
return 0;
} else {
let date1 = new Date(item1.lastRun);
let date2 = new Date(item2.lastRun);
if (date1 > date2) {
return 1;
} else if (date1 === date2) {
return 0;
} else {
return -1;
}
}
}
private updateTheme(theme: IColorTheme) {
let bgColor = theme.getColor(tableBackground);
let cellColor = theme.getColor(cellBackground);
let borderColor = theme.getColor(cellBorderColor);
let headerColumns = jQuery('#agentViewDiv .slick-header-column');
let cells = jQuery('.grid-canvas .ui-widget-content.slick-row .slick-cell');
let cellDetails = jQuery('#notebooksDiv .dynamic-cell-detail');
headerColumns.toArray().forEach(col => {
col.style.background = bgColor.toString();
});
cells.toArray().forEach(cell => {
cell.style.background = bgColor.toString();
cell.style.border = borderColor ? '1px solid ' + borderColor.toString() : null;
});
cellDetails.toArray().forEach(cellDetail => {
cellDetail.style.background = cellColor.toString();
});
}
protected getTableActions(targetObject: JobActionContext): IAction[] {
const editAction = this._instantiationService.createInstance(EditJobAction);
const editNotebookAction = this._instantiationService.createInstance(EditNotebookJobAction);
const runJobAction = this._instantiationService.createInstance(RunJobAction);
return [
runJobAction,
editNotebookAction,
editAction,
this._instantiationService.createInstance(DeleteNotebookAction)
];
}
protected convertStepsToStepInfos(steps: azdata.AgentJobStep[], job: azdata.AgentJobInfo): azdata.AgentJobStepInfo[] {
let result = [];
steps.forEach(step => {
let stepInfo: azdata.AgentJobStepInfo = {
jobId: job.jobId,
jobName: job.name,
script: null,
scriptName: null,
stepName: step.stepName,
subSystem: null,
id: +step.stepId,
failureAction: null,
successAction: null,
failStepId: null,
successStepId: null,
command: null,
commandExecutionSuccessCode: null,
databaseName: null,
databaseUserName: null,
server: null,
outputFileName: null,
appendToLogFile: null,
appendToStepHist: null,
writeLogToTable: null,
appendLogToTable: null,
retryAttempts: null,
retryInterval: null,
proxyName: null
};
result.push(stepInfo);
});
return result;
}
protected getCurrentTableObject(rowIndex: number): JobActionContext {
let data = this._table.grid.getData() as Slick.DataProvider<IItem>;
if (!data || rowIndex >= data.getLength()) {
return undefined;
}
let notebookID = data.getItem(rowIndex).notebookId;
if (!notebookID) {
// if we couldn't find the ID, check if it's an
// error row
let isErrorRow: boolean = data.getItem(rowIndex).id.indexOf('error') >= 0;
if (isErrorRow) {
notebookID = data.getItem(rowIndex - 1).notebookId;
}
}
let notebook: azdata.AgentNotebookInfo[] = this.notebooks.filter(job => {
return job.jobId === notebookID;
});
if (notebook && notebook.length > 0) {
// add steps
if (this.jobSteps && this.jobSteps[notebookID]) {
let steps = this.jobSteps[notebookID];
notebook[0].jobSteps = steps;
}
// add schedules
if (this.jobSchedules && this.jobSchedules[notebookID]) {
let schedules = this.jobSchedules[notebookID];
notebook[0].jobSchedules = schedules;
}
// add alerts
if (this.jobAlerts && this.jobAlerts[notebookID]) {
let alerts = this.jobAlerts[notebookID];
notebook[0].alerts = alerts;
}
if (notebook[0].jobSteps && notebook[0].jobSchedules && notebook[0].alerts) {
return { job: notebook[0], canEdit: true };
}
return { job: notebook[0], canEdit: false };
}
return undefined;
}
public async openCreateJobDialog() {
let ownerUri: string = this._commonService.connectionManagementService.connectionInfo.ownerUri;
await this._commandService.executeCommand('agent.openJobDialog', ownerUri);
}
public async openCreateNotebookDialog() {
let ownerUri: string = this._commonService.connectionManagementService.connectionInfo.ownerUri;
await this._commandService.executeCommand('agent.openNotebookDialog', ownerUri);
}
}