Sql DB project dashboard (#14899)

* First set of changes for workspace dashboard implementing the toolbar

* Workspace dashboard container implementation (#14813)

* First set of changes for workspace dashboard implementing the toolbar (#14160)

* First set of changes for workspace dashboard implementing the toolbar

* Addressed comments

* Addressed one remaining comment

* Removed an extra comma in interfaces file

* Addressed comments

* Addressed comments

* Refactored a bit of code

* Remove unnecessary await

* Addressed comments

* First set of changes for workspace dashboard container

* Update targetPlatform icon+add Time column to deploy table

* Addressed comments

* Removed redundant class definition

* Addressed comments

* Addressed comments

* Change enum to union type in dataworkspace typings

* Fix tests

* Addressed comments
This commit is contained in:
Sakshi Sharma
2021-03-30 17:37:53 -07:00
committed by GitHub
parent 4df77c73bf
commit b774f09b6c
25 changed files with 855 additions and 25 deletions

View File

@@ -30,6 +30,31 @@ export const edgeSqlDatabaseProjectTypeId = 'SqlDbEdgeProj';
export const edgeProjectTypeDisplayName = localize('edgeProjectTypeDisplayName', "SQL Edge");
export const edgeProjectTypeDescription = localize('edgeProjectTypeDescription', "Start with the core pieces to develop and publish schemas for SQL Edge");
// Dashboard
export const addItemAction = localize('addItemAction', "Add Item");
export const schemaCompareAction = localize('schemaCompareAction', "Schema Compare");
export const buildAction = localize('buildAction', "Build");
export const publishAction = localize('publishAction', "Publish");
export const changeTargetPlatformAction = localize('changeTargetPlatformAction', "Change Target Platform");
export const ID = localize('ID', "ID");
export const Status = localize('Status', "Status");
export const Time = localize('Time', "Time");
export const Date = localize('Date', "Date");
export const Builds = localize('Builds', "Builds");
export const Deployments = localize('Deployments', "Deployments");
export const Success = localize('Success', "Success");
export const Failed = localize('Failed', "Failed");
export const InProgress = localize('InProgress', "In progress");
export const hr = localize('hr', "hr");
export const min = localize('min', "min");
export const sec = localize('sec', "sec");
export const msec = localize('msec', "msec");
export const at = localize('at', "at");
// commands
export const revealFileInOsCommand = 'revealFileInOS';
export const schemaCompareStartCommand = 'schemaCompare.start';

View File

@@ -29,6 +29,16 @@ export class IconPathHelper {
public static folder: IconPath;
public static add: IconPath;
public static build: IconPath;
public static publish: IconPath;
public static schemaCompare: IconPath;
public static targetPlatform: IconPath;
public static success: IconPath;
public static error: IconPath;
public static inProgress: IconPath;
public static setExtensionContext(extensionContext: vscode.ExtensionContext) {
IconPathHelper.extensionContext = extensionContext;
@@ -48,6 +58,16 @@ export class IconPathHelper {
IconPathHelper.connect = IconPathHelper.makeIcon('connect', true);
IconPathHelper.folder = IconPathHelper.makeIcon('folder');
IconPathHelper.add = IconPathHelper.makeIcon('add', true);
IconPathHelper.build = IconPathHelper.makeIcon('build', true);
IconPathHelper.publish = IconPathHelper.makeIcon('publish', true);
IconPathHelper.schemaCompare = IconPathHelper.makeIcon('schemaCompare', true);
IconPathHelper.targetPlatform = IconPathHelper.makeIcon('targetPlatform', true);
IconPathHelper.success = IconPathHelper.makeIcon('success', true);
IconPathHelper.error = IconPathHelper.makeIcon('error', true);
IconPathHelper.inProgress = IconPathHelper.makeIcon('inProgress', true);
}
private static makeIcon(name: string, sameIcon: boolean = false) {

View File

@@ -284,3 +284,37 @@ export function getPackageInfo(packageJson?: any): IPackageInfo | undefined {
return undefined;
}
/**
* Converts time in milliseconds to hr, min, sec
* @param duration time in milliseconds
* @returns string in "hr, min, sec" or "msec" format
*/
export function timeConversion(duration: number): string {
const portions: string[] = [];
const msInHour = 1000 * 60 * 60;
const hours = Math.trunc(duration / msInHour);
if (hours > 0) {
portions.push(`${hours} ${constants.hr}`);
duration = duration - (hours * msInHour);
}
const msInMinute = 1000 * 60;
const minutes = Math.trunc(duration / msInMinute);
if (minutes > 0) {
portions.push(`${minutes} ${constants.min}`);
duration = duration - (minutes * msInMinute);
}
const seconds = Math.trunc(duration / 1000);
if (seconds > 0) {
portions.push(`${seconds} ${constants.sec}`);
}
if (hours === 0 && minutes === 0 && seconds === 0) {
portions.push(`${duration} ${constants.msec}`);
}
return portions.join(', ');
}

View File

@@ -31,6 +31,10 @@ import { ISystemDatabaseReferenceSettings, IDacpacReferenceSettings, IProjectRef
import { DatabaseReferenceTreeItem } from '../models/tree/databaseReferencesTreeItem';
import { CreateProjectFromDatabaseDialog } from '../dialogs/createProjectFromDatabaseDialog';
import { TelemetryActions, TelemetryReporter, TelemetryViews } from '../common/telemetry';
import { IconPathHelper } from '../common/iconHelper';
import { DashboardData, Status } from '../models/dashboardData/dashboardData';
const maxTableLength = 10;
/**
* Controller for managing lifecycle of projects
@@ -38,6 +42,8 @@ import { TelemetryActions, TelemetryReporter, TelemetryViews } from '../common/t
export class ProjectsController {
private netCoreTool: NetCoreTool;
private buildHelper: BuildHelper;
private buildInfo: DashboardData[] = [];
private deployInfo: DashboardData[] = [];
projFileWatchers = new Map<string, vscode.FileSystemWatcher>();
@@ -46,6 +52,66 @@ export class ProjectsController {
this.buildHelper = new BuildHelper();
}
public get dashboardDeployData(): (string | dataworkspace.IconCellValue)[][] {
const infoRows: (string | dataworkspace.IconCellValue)[][] = [];
let count = 0;
for (let i = this.deployInfo.length - 1; i >= 0; i--) {
let icon: azdata.IconPath;
let text: string;
if (this.deployInfo[i].status === Status.success) {
icon = IconPathHelper.success;
text = constants.Success;
} else if (this.deployInfo[i].status === Status.failed) {
icon = IconPathHelper.error;
text = constants.Failed;
} else {
icon = IconPathHelper.inProgress;
text = constants.InProgress;
}
let infoRow: (string | dataworkspace.IconCellValue)[] = [count.toString(),
{ text: text, icon: icon },
this.deployInfo[i].target,
this.deployInfo[i].timeToCompleteAction,
this.deployInfo[i].startDate];
infoRows.push(infoRow);
count++;
}
return infoRows;
}
public get dashboardBuildData(): (string | dataworkspace.IconCellValue)[][] {
const infoRows: (string | dataworkspace.IconCellValue)[][] = [];
let count = 0;
for (let i = this.buildInfo.length - 1; i >= 0; i--) {
let icon: azdata.IconPath;
let text: string;
if (this.buildInfo[i].status === Status.success) {
icon = IconPathHelper.success;
text = constants.Success;
} else if (this.buildInfo[i].status === Status.failed) {
icon = IconPathHelper.error;
text = constants.Failed;
} else {
icon = IconPathHelper.inProgress;
text = constants.InProgress;
}
let infoRow: (string | dataworkspace.IconCellValue)[] = [count.toString(),
{ text: text, icon: icon },
this.buildInfo[i].target,
this.buildInfo[i].timeToCompleteAction,
this.buildInfo[i].startDate];
infoRows.push(infoRow);
count++;
}
return infoRows;
}
public refreshProjectsTree(workspaceTreeItem: dataworkspace.WorkspaceTreeItem): void {
(workspaceTreeItem.treeDataProvider as SqlDatabaseProjectTreeViewProvider).notifyTreeDataChanged();
}
@@ -108,6 +174,14 @@ export class ProjectsController {
const project: Project = this.getProjectFromContext(context);
const startTime = new Date();
const currentBuildTimeInfo = `${startTime.toLocaleDateString()} ${constants.at} ${startTime.toLocaleTimeString()}`;
let buildInfoNew = new DashboardData(Status.inProgress, project.getProjectTargetVersion(), currentBuildTimeInfo);
this.buildInfo.push(buildInfoNew);
if (this.buildInfo.length - 1 === maxTableLength) {
this.buildInfo.shift(); // Remove the first element to maintain the length
}
// Check mssql extension for project dlls (tracking issue #10273)
await this.buildHelper.createBuildDirFolder();
@@ -120,15 +194,26 @@ export class ProjectsController {
try {
await this.netCoreTool.runDotnetCommand(options);
const timeToBuild = new Date().getTime() - startTime.getTime();
const currentBuildIndex = this.buildInfo.findIndex(b => b.startDate === currentBuildTimeInfo);
this.buildInfo[currentBuildIndex].status = Status.success;
this.buildInfo[currentBuildIndex].timeToCompleteAction = utils.timeConversion(timeToBuild);
TelemetryReporter.createActionEvent(TelemetryViews.ProjectController, TelemetryActions.build)
.withAdditionalMeasurements({ duration: new Date().getTime() - startTime.getTime() })
.withAdditionalMeasurements({ duration: timeToBuild })
.send();
return project.dacpacOutputPath;
} catch (err) {
const timeToFailureBuild = new Date().getTime() - startTime.getTime();
const currentBuildIndex = this.buildInfo.findIndex(b => b.startDate === currentBuildTimeInfo);
this.buildInfo[currentBuildIndex].status = Status.failed;
this.buildInfo[currentBuildIndex].timeToCompleteAction = utils.timeConversion(timeToFailureBuild);
TelemetryReporter.createErrorEvent(TelemetryViews.ProjectController, TelemetryActions.build)
.withAdditionalMeasurements({ duration: new Date().getTime() - startTime.getTime() })
.withAdditionalMeasurements({ duration: timeToFailureBuild })
.send();
vscode.window.showErrorMessage(constants.projBuildFailed(utils.getErrorMessage(err)));
@@ -187,7 +272,16 @@ export class ProjectsController {
let result: mssql.DacFxResult;
telemetryProps.profileUsed = (settings.profileUsed ?? false).toString();
const actionStartTime = new Date().getTime();
const currentDate = new Date();
const actionStartTime = currentDate.getTime();
const currentDeployTimeInfo = `${currentDate.toLocaleDateString()} ${constants.at} ${currentDate.toLocaleTimeString()}`;
let deployInfoNew = new DashboardData(Status.inProgress, project.getProjectTargetVersion(), currentDeployTimeInfo);
this.deployInfo.push(deployInfoNew);
if (this.deployInfo.length - 1 === maxTableLength) {
this.deployInfo.shift(); // Remove the first element to maintain the length
}
try {
if ((<IPublishSettings>settings).upgradeExisting) {
@@ -200,20 +294,30 @@ export class ProjectsController {
}
} catch (err) {
const actionEndTime = new Date().getTime();
telemetryProps.actionDuration = (actionEndTime - actionStartTime).toString();
const timeToFailureDeploy = actionEndTime - actionStartTime;
telemetryProps.actionDuration = timeToFailureDeploy.toString();
telemetryProps.totalDuration = (actionEndTime - buildStartTime).toString();
TelemetryReporter.createErrorEvent(TelemetryViews.ProjectController, TelemetryActions.publishProject)
.withAdditionalProperties(telemetryProps)
.send();
const currentDeployIndex = this.deployInfo.findIndex(d => d.startDate === currentDeployTimeInfo);
this.deployInfo[currentDeployIndex].status = Status.failed;
this.deployInfo[currentDeployIndex].timeToCompleteAction = utils.timeConversion(timeToFailureDeploy);
throw err;
}
const actionEndTime = new Date().getTime();
telemetryProps.actionDuration = (actionEndTime - actionStartTime).toString();
const timeToDeploy = actionEndTime - actionStartTime;
telemetryProps.actionDuration = timeToDeploy.toString();
telemetryProps.totalDuration = (actionEndTime - buildStartTime).toString();
const currentDeployIndex = this.deployInfo.findIndex(d => d.startDate === currentDeployTimeInfo);
this.deployInfo[currentDeployIndex].status = Status.success;
this.deployInfo[currentDeployIndex].timeToCompleteAction = utils.timeConversion(timeToDeploy);
TelemetryReporter.createActionEvent(TelemetryViews.ProjectController, TelemetryActions.publishProject)
.withAdditionalProperties(telemetryProps)
.send();

View File

@@ -0,0 +1,24 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export class DashboardData {
public status: Status;
public target: string;
public timeToCompleteAction: string;
public startDate: string;
constructor(status: Status, target: string, startDate: string) {
this.status = status;
this.target = target;
this.timeToCompleteAction = '';
this.startDate = startDate;
}
}
export enum Status {
success,
failed,
inProgress
}

View File

@@ -77,7 +77,45 @@ export class SqlDatabaseProjectProvider implements dataworkspace.IProjectProvide
}
/**
* Adds the list of files and directories to the project, and saves the project file
* Gets the supported project types
*/
get projectActions(): (dataworkspace.IProjectAction | dataworkspace.IProjectActionGroup)[] {
const addItemAction: dataworkspace.IProjectAction = {
id: constants.addItemAction,
icon: IconPathHelper.add,
run: (treeItem: dataworkspace.WorkspaceTreeItem) => this.projectController.addItemPromptFromNode(treeItem)
};
const schemaCompareAction: dataworkspace.IProjectAction = {
id: constants.schemaCompareAction,
icon: IconPathHelper.schemaCompare,
run: (treeItem: dataworkspace.WorkspaceTreeItem) => this.projectController.schemaCompare(treeItem)
};
const buildAction: dataworkspace.IProjectAction = {
id: constants.buildAction,
icon: IconPathHelper.build,
run: (treeItem: dataworkspace.WorkspaceTreeItem) => this.projectController.buildProject(treeItem)
};
const publishAction: dataworkspace.IProjectAction = {
id: constants.publishAction,
icon: IconPathHelper.publish,
run: (treeItem: dataworkspace.WorkspaceTreeItem) => this.projectController.publishProject(treeItem)
};
const changeTargetPlatformAction: dataworkspace.IProjectAction = {
id: constants.changeTargetPlatformAction,
icon: IconPathHelper.targetPlatform,
run: (treeItem: dataworkspace.WorkspaceTreeItem) => this.projectController.changeTargetPlatform(treeItem)
};
let group: dataworkspace.IProjectActionGroup = { actions: [addItemAction, schemaCompareAction, buildAction, publishAction] };
return [group, changeTargetPlatformAction];
}
/** Adds the list of files and directories to the project, and saves the project file
* @param projectFile The Uri of the project file
* @param list list of uris of files and folders to add. Files and folders must already exist. Files and folders must already exist. No files or folders will be added if any do not exist.
*/
@@ -85,4 +123,31 @@ export class SqlDatabaseProjectProvider implements dataworkspace.IProjectProvide
const project = await Project.openProject(projectFile.fsPath);
await project.addToProject(list);
}
/**
* Gets the data to be displayed in the project dashboard
*/
get dashboardComponents(): dataworkspace.IDashboardTable[] {
const deployInfo: dataworkspace.IDashboardTable = {
name: constants.Deployments,
columns: [{ displayName: constants.ID, width: 75 },
{ displayName: constants.Status, width: 180, type: 'icon' },
{ displayName: constants.Target, width: 180 },
{ displayName: constants.Time, width: 180 },
{ displayName: constants.Date, width: 180 }],
data: this.projectController.dashboardDeployData
};
const buildInfo: dataworkspace.IDashboardTable = {
name: constants.Builds,
columns: [{ displayName: constants.ID, width: 75 },
{ displayName: constants.Status, width: 180, type: 'icon' },
{ displayName: constants.Target, width: 180 },
{ displayName: constants.Time, width: 180 },
{ displayName: constants.Date, width: 180 }],
data: this.projectController.dashboardBuildData
};
return [deployInfo, buildInfo];
}
}

View File

@@ -409,7 +409,9 @@ describe('ProjectsController', function (): void {
projController.setup(x => x.getDaxFxService()).returns(() => Promise.resolve(testContext.dacFxService.object));
await projController.object.publishProjectCallback(new Project(''), { connectionUri: '', databaseName: '' });
const proj = await testUtils.createTestProject(baselines.openProjectFileBaseline);
await projController.object.publishProjectCallback(proj, { connectionUri: '', databaseName: '' });
should(builtDacpacPath).not.equal('', 'built dacpac path should be set');
should(publishedDacpacPath).not.equal('', 'published dacpac path should be set');

View File

@@ -7,7 +7,7 @@ import * as should from 'should';
import * as path from 'path';
import * as os from 'os';
import { createDummyFileStructure } from './testUtils';
import { exists, trimUri, removeSqlCmdVariableFormatting, formatSqlCmdVariable, isValidSqlCmdVariableName } from '../common/utils';
import { exists, trimUri, removeSqlCmdVariableFormatting, formatSqlCmdVariable, isValidSqlCmdVariableName, timeConversion } from '../common/utils';
import { Uri } from 'vscode';
describe('Tests to verify utils functions', function (): void {
@@ -78,5 +78,15 @@ describe('Tests to verify utils functions', function (): void {
should(isValidSqlCmdVariableName('test\'')).equal(false);
should(isValidSqlCmdVariableName('test-1')).equal(false);
});
it('Should convert from milliseconds to hr min sec correctly', () => {
should(timeConversion((60 * 60 * 1000) + (59 * 60 * 1000) + (59 * 1000))).equal('1 hr, 59 min, 59 sec');
should(timeConversion((60 * 60 * 1000) + (59 * 60 * 1000) )).equal('1 hr, 59 min');
should(timeConversion((60 * 60 * 1000) )).equal('1 hr');
should(timeConversion((60 * 60 * 1000) + (59 * 1000))).equal('1 hr, 59 sec');
should(timeConversion( (59 * 60 * 1000) + (59 * 1000))).equal('59 min, 59 sec');
should(timeConversion( (59 * 1000))).equal('59 sec');
should(timeConversion( (59))).equal('59 msec');
});
});