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

@@ -71,6 +71,10 @@
{
"command": "projects.removeProject",
"title": "%remove-project-command%"
},
{
"command": "projects.manageProject",
"title": "%manage-project-command%"
}
],
"menus": {
@@ -114,9 +118,18 @@
},
{
"command": "projects.openExisting"
},
{
"command": "projects.manageProject",
"when": "false"
}
],
"view/item/context": [
{
"command": "projects.manageProject",
"when": "view == dataworkspace.views.main",
"group": "0_projectsFirst@1"
},
{
"command": "projects.removeProject",
"when": "view == dataworkspace.views.main && viewItem == databaseProject.itemType.project",

View File

@@ -11,5 +11,6 @@
"projects-view-no-project-content": "No projects open in current workspace.\n[Create new](command:projects.new)\n[Open existing](command:projects.openExisting)\nTo learn more about projects [read our docs](https://aka.ms/azuredatastudio-projects).\n",
"open-existing-command": "Open existing",
"projects.defaultProjectSaveLocation": "Full path to folder where new projects are saved by default.",
"projects.showNotAddedProjectsInWorkspacePrompt": "Always show information message when the current workspace folders contain projects that have not been added to the workspace's projects."
"projects.showNotAddedProjectsInWorkspacePrompt": "Always show information message when the current workspace folders contain projects that have not been added to the workspace's projects.",
"manage-project-command": "Manage"
}

View File

@@ -21,6 +21,8 @@ export const WorkspaceContainsNotAddedProjects = localize('dataworkspace.workspa
export const LaunchOpenExisitingDialog = localize('dataworkspace.launchOpenExistingDialog', "Launch Open existing dialog");
export const DoNotShowAgain = localize('dataworkspace.doNotShowAgain', "Do not show again");
export const ProjectsFailedToLoad = localize('dataworkspace.projectsFailedToLoad', "Some projects failed to load. Please open console for more information");
export const fileDoesNotExist = (name: string): string => { return localize('fileDoesNotExist', "File '{0}' doesn't exist", name); };
export const projectNameNull = localize('projectNameNull', "Project name is null");
// config settings
export const projectsConfigurationKey = 'projects';
@@ -72,3 +74,9 @@ export const LocalClonePathPlaceholder = localize('dataworkspace.localClonePathP
// Workspace settings for saving new projects
export const ProjectConfigurationKey = 'projects';
export const ProjectSaveLocationKey = 'defaultProjectSaveLocation';
export namespace cssStyles {
export const title = { 'font-size': '18px', 'font-weight': '600' };
export const tableHeader = { 'text-align': 'left', 'font-weight': '500', 'font-size': '13px', 'user-select': 'text' };
export const tableRow = { 'border-top': 'solid 1px #ccc', 'border-bottom': 'solid 1px #ccc', 'border-left': 'none', 'border-right': 'none' };
}

View File

@@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/
declare module 'dataworkspace' {
import * as azdata from 'azdata';
import * as vscode from 'vscode';
export const enum extension {
name = 'Microsoft.data-workspace'
@@ -37,8 +38,8 @@ declare module 'dataworkspace' {
defaultProjectSaveLocation: vscode.Uri | undefined;
/**
* Verifies that a workspace is open or if it should be automatically created
*/
* Verifies that a workspace is open or if it should be automatically created
*/
validateWorkspace(): Promise<boolean>;
}
@@ -70,6 +71,16 @@ declare module 'dataworkspace' {
* Gets the supported project types
*/
readonly supportedProjectTypes: IProjectType[];
/**
* Gets the project actions to be placed on the dashboard toolbar
*/
readonly projectActions: (IProjectAction | IProjectActionGroup)[];
/**
* Gets the project data to be placed in the dashboard container
*/
readonly dashboardComponents: IDashboardTable[];
}
/**
@@ -116,4 +127,71 @@ declare module 'dataworkspace' {
*/
element: any;
}
export interface IProjectAction {
/**
* id of the project action
*/
readonly id: string;
/**
* icon path of the project action
*/
readonly icon?: azdata.IconPath;
/**
* Run context for each project action
* @param treeItem The treeItem in a project's hierarchy, to be used to obtain a Project
*/
run(treeItem: WorkspaceTreeItem): void;
}
/**
* List of project actions that should be grouped and have a separator after the last action
*/
export interface IProjectActionGroup {
actions: IProjectAction[];
}
/**
* Defines table to be presented in the dashboard container
*/
export interface IDashboardTable {
/**
* name of the table
*/
name: string;
/**
* column definitions
*/
columns: IDashboardColumnInfo[];
/**
* project data
*/
data: (string | IconCellValue)[][];
}
/**
* Project dashboard table's column information
*/
export interface IDashboardColumnInfo {
displayName: string;
width: number;
type?: IDashboardColumnType;
}
/**
* Cell value of an icon for the table data
*/
export interface IconCellValue {
text: string;
icon: azdata.IconPath;
}
/**
* Union type representing data types in dashboard table
*/
export type IDashboardColumnType = 'string' | 'icon';
}

View File

@@ -0,0 +1,188 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import { IDashboardColumnInfo, IDashboardTable, IProjectAction, IProjectActionGroup, IProjectProvider, WorkspaceTreeItem } from 'dataworkspace';
import * as path from 'path';
import * as vscode from 'vscode';
import * as constants from '../common/constants';
import { IWorkspaceService } from '../common/interfaces';
import { fileExist } from '../common/utils';
export class ProjectDashboard {
private dashboard: azdata.window.ModelViewDashboard | undefined;
private modelView: azdata.ModelView | undefined;
private projectProvider: IProjectProvider | undefined;
private overviewTab: azdata.DashboardTab | undefined;
constructor(private _workspaceService: IWorkspaceService, private _treeItem: WorkspaceTreeItem) {
}
public async showDashboard(): Promise<void> {
const project = this._treeItem.element.project;
if (!(await fileExist(project.projectFilePath))) {
throw new Error(constants.fileDoesNotExist(project.projectFilePath));
}
if (!project.projectFileName) {
throw new Error(constants.projectNameNull);
}
this.projectProvider = await this._workspaceService.getProjectProvider(vscode.Uri.file(project.projectFilePath));
if (!this.projectProvider) {
throw new Error(constants.ProviderNotFoundForProjectTypeError(project.projectFilePath));
}
await this.createDashboard(project.projectFileName, project.projectFilePath);
await this.dashboard!.open();
}
private async createDashboard(title: string, location: string): Promise<void> {
this.dashboard = azdata.window.createModelViewDashboard(title, 'ProjectDashboard', { alwaysShowTabs: false });
this.dashboard.registerTabs(async (modelView: azdata.ModelView) => {
this.modelView = modelView;
this.overviewTab = {
title: '',
id: 'overview-tab',
content: this.createContainer(title, location),
toolbar: this.createToolbarContainer()
};
return [
this.overviewTab
];
});
}
private createToolbarContainer(): azdata.ToolbarContainer {
const projectActions: (IProjectAction | IProjectActionGroup)[] = this.projectProvider!.projectActions;
// Add actions as buttons
const buttons: azdata.ToolbarComponent[] = [];
const projectActionsLength = projectActions.length;
projectActions.forEach((action, actionIndex) => {
if (this.isProjectAction(action)) {
const button = this.createButton(action);
buttons.push({ component: button });
} else {
const groupLength = action.actions.length;
action.actions.forEach((groupAction, index) => {
const button = this.createButton(groupAction);
buttons.push({ component: button, toolbarSeparatorAfter: ((groupLength - 1 === index) && (projectActionsLength - 1 !== actionIndex)) }); // Add toolbar separator at the end of the group, if the group is not the last in the list
});
}
});
return this.modelView!.modelBuilder.toolbarContainer()
.withToolbarItems(
buttons
).component();
}
private isProjectAction(obj: any): obj is IProjectAction {
return obj.id !== undefined;
}
private createButton(projectAction: IProjectAction): azdata.ButtonComponent {
let button = this.modelView!.modelBuilder.button()
.withProperties<azdata.ButtonProperties>({
label: projectAction.id,
iconPath: projectAction.icon,
height: '20px'
}).component();
button.onDidClick(async () => {
await projectAction.run(this._treeItem);
});
return button;
}
private createContainer(title: string, location: string): azdata.FlexContainer {
const dashboardData: IDashboardTable[] = this.projectProvider!.dashboardComponents;
const rootContainer = this.modelView!.modelBuilder.flexContainer().withLayout(
{
flexFlow: 'column',
width: '100%',
height: '100%'
}).component();
const titleLabel = this.modelView!.modelBuilder.text()
.withProperties<azdata.TextComponentProperties>({ value: title, CSSStyles: { 'margin-block-start': '20px', 'margin-block-end': '0px' } })
.component();
rootContainer.addItem(titleLabel, { CSSStyles: { 'padding-left': '34px', 'padding-top': '15px', 'font-size': '36px', 'font-weight': '400' } });
const projectFolderPath = path.dirname(location);
const locationLabel = this.modelView!.modelBuilder.text()
.withProperties<azdata.TextComponentProperties>({ value: projectFolderPath, CSSStyles: { 'margin-block-start': '20px', 'margin-block-end': '0px' } })
.component();
rootContainer.addItem(locationLabel, { CSSStyles: { 'padding-left': '34px', 'padding-top': '15px', 'padding-bottom': '50px', 'font-size': '16px' } });
// Add all the tables to the container
dashboardData.forEach(info => {
const tableNameLabel = this.modelView!.modelBuilder.text()
.withProperties<azdata.TextComponentProperties>({ value: info.name, CSSStyles: { 'margin-block-start': '30px', 'margin-block-end': '0px' } })
.component();
rootContainer.addItem(tableNameLabel, { CSSStyles: { 'padding-left': '25px', 'padding-bottom': '20px', ...constants.cssStyles.title } });
const columns: azdata.DeclarativeTableColumn[] = [];
info.columns.forEach((column: IDashboardColumnInfo) => {
let col = {
displayName: column.displayName,
valueType: column.type === 'icon' ? azdata.DeclarativeDataType.component : azdata.DeclarativeDataType.string,
isReadOnly: true,
width: column.width,
headerCssStyles: {
'border': 'none',
...constants.cssStyles.tableHeader
},
rowCssStyles: {
...constants.cssStyles.tableRow
},
};
columns.push(col);
});
const data: azdata.DeclarativeTableCellValue[][] = [];
info.data.forEach(values => {
const columnValue: azdata.DeclarativeTableCellValue[] = [];
values.forEach(val => {
if (typeof val === 'string') {
columnValue.push({ value: val });
} else {
const iconComponent = this.modelView!.modelBuilder.image().withProperties<azdata.ImageComponentProperties>({
iconPath: val.icon,
width: '15px',
height: '15px',
iconHeight: '15px',
iconWidth: '15px'
}).component();
const stringComponent = this.modelView!.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
value: val.text,
CSSStyles: { 'margin-block-start': 'auto', 'block-size': 'auto', 'margin-block-end': '0px' }
}).component();
const columnData = this.modelView!.modelBuilder.flexContainer().withItems([iconComponent, stringComponent], { flex: '0 0 auto', CSSStyles: { 'margin-right': '10px' } }).withLayout({ flexFlow: 'row' }).component();
columnValue.push({ value: columnData });
}
});
data.push(columnValue);
});
const table = this.modelView!.modelBuilder.declarativeTable()
.withProperties<azdata.DeclarativeTableProperties>({ columns: columns, dataValues: data, ariaLabel: info.name, CSSStyles: { 'margin-left': '30px' } }).component();
rootContainer.addItem(table);
});
return rootContainer;
}
}

View File

@@ -12,6 +12,7 @@ import { NewProjectDialog } from './dialogs/newProjectDialog';
import { OpenExistingDialog } from './dialogs/openExistingDialog';
import { IWorkspaceService } from './common/interfaces';
import { IconPathHelper } from './common/iconHelper';
import { ProjectDashboard } from './dialogs/projectDashboard';
export function activate(context: vscode.ExtensionContext): Promise<IExtension> {
const workspaceService = new WorkspaceService(context);
@@ -51,6 +52,10 @@ export function activate(context: vscode.ExtensionContext): Promise<IExtension>
context.subscriptions.push(vscode.commands.registerCommand('projects.removeProject', async (treeItem: WorkspaceTreeItem) => {
await workspaceService.removeProject(vscode.Uri.file(treeItem.element.project.projectFilePath));
}));
context.subscriptions.push(vscode.commands.registerCommand('projects.manageProject', async (treeItem: WorkspaceTreeItem) => {
const dashboard = new ProjectDashboard(workspaceService, treeItem);
await dashboard.showDashboard();
}));
IconPathHelper.setExtensionContext(context);

View File

@@ -3,11 +3,11 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IDashboardTable, IProjectAction, IProjectProvider, IProjectType } from 'dataworkspace';
import 'mocha';
import * as vscode from 'vscode';
import * as should from 'should';
import * as vscode from 'vscode';
import { ProjectProviderRegistry } from '../common/projectProviderRegistry';
import { IProjectProvider, IProjectType } from 'dataworkspace';
export class MockTreeDataProvider implements vscode.TreeDataProvider<any>{
onDidChangeTreeData?: vscode.Event<any> | undefined;
@@ -19,7 +19,7 @@ export class MockTreeDataProvider implements vscode.TreeDataProvider<any>{
}
}
export function createProjectProvider(projectTypes: IProjectType[]): IProjectProvider {
export function createProjectProvider(projectTypes: IProjectType[], projectActions: IProjectAction[], dashboardComponents: IDashboardTable[]): IProjectProvider {
const treeDataProvider = new MockTreeDataProvider();
const projectProvider: IProjectProvider = {
supportedProjectTypes: projectTypes,
@@ -31,7 +31,9 @@ export function createProjectProvider(projectTypes: IProjectType[]): IProjectPro
},
createProject: (name: string, location: vscode.Uri, projectTypeId: string): Promise<vscode.Uri> => {
return Promise.resolve(location);
}
},
projectActions: projectActions,
dashboardComponents: dashboardComponents
};
return projectProvider;
}
@@ -52,7 +54,25 @@ suite('ProjectProviderRegistry Tests', function (): void {
displayName: 'test project 1',
description: ''
}
]);
],
[{
id: 'testAction1',
run: async (): Promise<any> => { return Promise.resolve(); }
},
{
id: 'testAction2',
run: async (): Promise<any> => { return Promise.resolve(); }
}],
[{
name: 'tableInfo1',
columns: [{ displayName: 'c1', width: 75, type: 'string' }],
data: [['d1']]
},
{
name: 'tableInfo2',
columns: [{ displayName: 'c1', width: 75, type: 'string' }],
data: [['d1']]
}]);
const provider2 = createProjectProvider([
{
id: 'sp1',
@@ -61,7 +81,37 @@ suite('ProjectProviderRegistry Tests', function (): void {
displayName: 'sql project',
description: ''
}
]);
],
[{
id: 'Add',
run: async (): Promise<any> => { return Promise.resolve(); }
},
{
id: 'Schema Compare',
run: async (): Promise<any> => { return Promise.resolve(); }
},
{
id: 'Build',
run: async (): Promise<any> => { return Promise.resolve(); }
},
{
id: 'Publish',
run: async (): Promise<any> => { return Promise.resolve(); }
},
{
id: 'Target Version',
run: async (): Promise<any> => { return Promise.resolve(); }
}],
[{
name: 'Deployments',
columns: [{ displayName: 'c1', width: 75, type: 'string' }],
data: [['d1']]
},
{
name: 'Builds',
columns: [{ displayName: 'c1', width: 75, type: 'string' }],
data: [['d1']]
}]);
should.strictEqual(ProjectProviderRegistry.providers.length, 0, 'there should be no project provider at the beginning of the test');
const disposable1 = ProjectProviderRegistry.registerProvider(provider1, 'test.testProvider');
let providerResult = ProjectProviderRegistry.getProviderByProjectExtension('testproj');
@@ -104,7 +154,16 @@ suite('ProjectProviderRegistry Tests', function (): void {
displayName: 'test project',
description: ''
}
]);
],
[{
id: 'testAction1',
run: async (): Promise<any> => { return Promise.resolve(); }
}],
[{
name: 'tableInfo1',
columns: [{ displayName: 'c1', width: 75, type: 'string' }],
data: [['d1']]
}]);
should.strictEqual(ProjectProviderRegistry.providers.length, 0, 'there should be no project provider at the beginning of the test');
ProjectProviderRegistry.registerProvider(provider, 'test.testProvider');
should.strictEqual(ProjectProviderRegistry.providers.length, 1, 'there should be only one project provider at this time');

View File

@@ -136,6 +136,28 @@ suite('WorkspaceService Tests', function (): void {
icon: '',
displayName: 'test project 1'
}
],
[
{
id: 'testAction1',
run: async (): Promise<any> => { return Promise.resolve(); }
},
{
id: 'testAction2',
run: async (): Promise<any> => { return Promise.resolve(); }
}
],
[
{
name: 'tableInfo1',
columns: [{ displayName: 'c1', width: 75, type: 'string' }],
data: [['d1']]
},
{
name: 'tableInfo2',
columns: [{ displayName: 'c1', width: 75, type: 'string' }],
data: [['d1']]
}
]);
const provider2 = createProjectProvider([
{
@@ -145,6 +167,40 @@ suite('WorkspaceService Tests', function (): void {
icon: '',
displayName: 'sql project'
}
],
[
{
id: 'Add',
run: async (): Promise<any> => { return Promise.resolve(); }
},
{
id: 'Schema Compare',
run: async (): Promise<any> => { return Promise.resolve(); }
},
{
id: 'Build',
run: async (): Promise<any> => { return Promise.resolve(); }
},
{
id: 'Publish',
run: async (): Promise<any> => { return Promise.resolve(); }
},
{
id: 'Target Version',
run: async (): Promise<any> => { return Promise.resolve(); }
}
],
[
{
name: 'Deployments',
columns: [{ displayName: 'c1', width: 75, type: 'string' }],
data: [['d1']]
},
{
name: 'Builds',
columns: [{ displayName: 'c1', width: 75, type: 'string' }],
data: [['d1']]
}
]);
sinon.stub(ProjectProviderRegistry, 'providers').value([provider1, provider2]);
const consoleErrorStub = sinon.stub(console, 'error');
@@ -178,7 +234,37 @@ suite('WorkspaceService Tests', function (): void {
icon: '',
displayName: 'test project'
}
]));
],
[{
id: 'Add',
run: async (): Promise<any> => { return Promise.resolve(); }
},
{
id: 'Schema Compare',
run: async (): Promise<any> => { return Promise.resolve(); }
},
{
id: 'Build',
run: async (): Promise<any> => { return Promise.resolve(); }
},
{
id: 'Publish',
run: async (): Promise<any> => { return Promise.resolve(); }
},
{
id: 'Target Version',
run: async (): Promise<any> => { return Promise.resolve(); }
}],
[{
name: 'Deployments',
columns: [{ displayName: 'c1', width: 75, type: 'string' }],
data: [['d1']]
},
{
name: 'Builds',
columns: [{ displayName: 'c1', width: 75, type: 'string' }],
data: [['d1']]
}]));
let provider = await service.getProjectProvider(vscode.Uri.file('abc.sqlproj'));
should.notStrictEqual(provider, undefined, 'Provider should be returned for sqlproj');
should.strictEqual(provider!.supportedProjectTypes[0].projectFileExtension, 'sqlproj');
@@ -193,7 +279,16 @@ suite('WorkspaceService Tests', function (): void {
projectFileExtension: 'csproj',
icon: '',
displayName: 'test cs project'
}]));
}],
[{
id: 'testAction2',
run: async (): Promise<any> => { return Promise.resolve(); }
}],
[{
name: 'tableInfo2',
columns: [{ displayName: 'c1', width: 75, type: 'string' }],
data: [['d1']]
}]));
provider = await service.getProjectProvider(vscode.Uri.file('abc.csproj'));
should.notStrictEqual(provider, undefined, 'Provider should be returned for csproj');
should.strictEqual(provider!.supportedProjectTypes[0].projectFileExtension, 'csproj');
@@ -324,7 +419,7 @@ suite('WorkspaceService Tests', function (): void {
await vscode.workspace.getConfiguration(constants.projectsConfigurationKey).update(constants.showNotAddedProjectsMessageKey, true, true);
sinon.stub(service, 'getProjectsInWorkspace').returns([vscode.Uri.file('abc.sqlproj'), vscode.Uri.file('folder1/abc1.sqlproj')]);
sinon.stub(vscode.workspace, 'workspaceFolders').value([{uri: vscode.Uri.file('.')}]);
sinon.stub(vscode.workspace, 'workspaceFolders').value([{ uri: vscode.Uri.file('.') }]);
sinon.stub(service, 'getAllProjectTypes').resolves([{
projectFileExtension: 'sqlproj',
id: 'sql project',

View File

@@ -3,14 +3,14 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IProjectProvider, WorkspaceTreeItem } from 'dataworkspace';
import 'mocha';
import * as sinon from 'sinon';
import * as vscode from 'vscode';
import * as should from 'should';
import * as sinon from 'sinon';
import * as TypeMoq from 'typemoq';
import * as vscode from 'vscode';
import { WorkspaceTreeDataProvider } from '../common/workspaceTreeDataProvider';
import { WorkspaceService } from '../services/workspaceService';
import { IProjectProvider, WorkspaceTreeItem } from 'dataworkspace';
import { MockTreeDataProvider } from './projectProviderRegistry.test';
interface ExtensionGlobalMemento extends vscode.Memento {
@@ -90,7 +90,37 @@ suite('workspaceTreeDataProvider Tests', function (): void {
},
createProject: (name: string, location: vscode.Uri): Promise<vscode.Uri> => {
return Promise.resolve(location);
}
},
projectActions: [{
id: 'Add',
run: async (): Promise<any> => { return Promise.resolve(); }
},
{
id: 'Schema Compare',
run: async (): Promise<any> => { return Promise.resolve(); }
},
{
id: 'Build',
run: async (): Promise<any> => { return Promise.resolve(); }
},
{
id: 'Publish',
run: async (): Promise<any> => { return Promise.resolve(); }
},
{
id: 'Target Version',
run: async (): Promise<any> => { return Promise.resolve(); }
}],
dashboardComponents: [{
name: 'Deployments',
columns: [{ displayName: 'c1', width: 75, type: 'string' }],
data: [['d1']]
},
{
name: 'Builds',
columns: [{ displayName: 'c1', width: 75, type: 'string' }],
data: [['d1']]
}]
};
const getProjectProviderStub = sinon.stub(workspaceService, 'getProjectProvider');
getProjectProviderStub.onFirstCall().resolves(undefined);

View File

@@ -0,0 +1,20 @@
<svg width="12" height="13" viewBox="0 0 12 13" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0)">
<g clip-path="url(#clip1)">
<g clip-path="url(#clip2)">
<path d="M12 5.68887V6.48887H6.4V12.0889H5.6V6.48887H0V5.68887H5.6V0.0888672H6.4V5.68887H12Z" fill="#0078D4"/>
</g>
</g>
</g>
<defs>
<clipPath id="clip0">
<rect width="12" height="12" fill="white" transform="translate(0 0.0888672)"/>
</clipPath>
<clipPath id="clip1">
<rect width="50" height="50" fill="white" transform="translate(0 0.0888672)"/>
</clipPath>
<clipPath id="clip2">
<rect width="12" height="12" fill="white" transform="translate(0 0.0888672)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 667 B

View File

@@ -0,0 +1,3 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.60039 8H2.00039V6.4H3.60039V8ZM11.6004 4.8V12H0.400391V4.8H1.20039V8.8H10.8004V4.8H11.6004ZM5.20039 9.6H3.60039V11.2H5.20039V9.6ZM8.40039 9.6H6.80039V11.2H8.40039V9.6ZM5.20039 6.4H6.80039V8H5.20039V6.4ZM10.0004 8H8.40039V6.4H10.0004V8ZM5.60039 5.7625L3.71914 3.88125L4.28164 3.31875L5.20039 4.23125V0H6.00039V4.23125L6.91914 3.31875L7.48164 3.88125L5.60039 5.7625Z" fill="#0078D4"/>
</svg>

After

Width:  |  Height:  |  Size: 498 B

View File

@@ -0,0 +1,11 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0)">
<path d="M6.07422 12.0752C9.38793 12.0752 12.0742 9.3889 12.0742 6.0752C12.0742 2.76149 9.38793 0.0751953 6.07422 0.0751953C2.76051 0.0751953 0.0742188 2.76149 0.0742188 6.0752C0.0742188 9.3889 2.76051 12.0752 6.07422 12.0752Z" fill="#E00B1C"/>
<path d="M9.51952 3.54599L8.45302 2.47949L5.99902 4.93349L3.54502 2.47949L2.47852 3.54599L4.93252 5.99999L2.47852 8.45399L3.54502 9.52049L5.99902 7.06649L8.45302 9.52049L9.51952 8.45399L7.06552 5.99999L9.51952 3.54599Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0">
<rect width="12" height="12" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 709 B

View File

@@ -0,0 +1,7 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 12C9.31371 12 12 9.31371 12 6C12 2.68629 9.31371 0 6 0C2.68629 0 0 2.68629 0 6C0 9.31371 2.68629 12 6 12Z" fill="#015CDA"/>
<path d="M3.75 5.9998C3.75049 5.8396 3.76834 5.67991 3.80325 5.52355L2.34825 5.1748C2.2845 5.44522 2.25154 5.72198 2.25 5.9998C2.25388 6.3091 2.2962 6.61671 2.376 6.91555L3.83175 6.56755C3.7801 6.38263 3.75262 6.19179 3.75 5.9998Z" fill="white"/>
<path d="M4.26841 7.41846L3.09766 8.34471C3.45993 8.79675 3.92202 9.15871 4.44766 9.40221L5.07691 8.04546C4.76297 7.90109 4.48637 7.68658 4.26841 7.41846Z" fill="white"/>
<path d="M4.21958 4.64118C4.4359 4.35737 4.71663 4.12909 5.03858 3.97518L4.40933 2.61768C3.87573 2.87116 3.40957 3.24712 3.04883 3.71493L4.21958 4.64118Z" fill="white"/>
<path d="M6 2.25V3.75C6.59674 3.75 7.16903 3.98705 7.59099 4.40901C8.01295 4.83097 8.25 5.40326 8.25 6C8.25 6.59674 8.01295 7.16903 7.59099 7.59099C7.16903 8.01295 6.59674 8.25 6 8.25V9.75C6.99456 9.75 7.94839 9.35491 8.65165 8.65165C9.35491 7.94839 9.75 6.99456 9.75 6C9.75 5.00544 9.35491 4.05161 8.65165 3.34835C7.94839 2.64509 6.99456 2.25 6 2.25Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,11 @@
<svg width="13" height="13" viewBox="0 0 13 13" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0)">
<path d="M5.99922 1.51387V9.83887H6.74922V1.51387L9.44922 4.28887L10.0492 3.76387L6.37422 0.0888672L2.69922 3.76387L3.29922 4.28887L5.99922 1.51387Z" fill="#0078D4"/>
<path d="M11.25 4.58887V11.3389H1.5V4.58887H0.75V12.0889H1.5H11.25H12V4.58887H11.25Z" fill="#0078D4"/>
</g>
<defs>
<clipPath id="clip0">
<rect width="12" height="12" fill="white" transform="translate(0.75 0.0888672)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 537 B

View File

@@ -0,0 +1,3 @@
<svg width="12" height="13" viewBox="0 0 12 13" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.75 5.33887C10.0586 5.33887 10.3496 5.39941 10.623 5.52051C10.8965 5.6377 11.1348 5.79785 11.3379 6.00098C11.541 6.2041 11.7012 6.44238 11.8184 6.71582C11.9395 6.98926 12 7.28027 12 7.58887C12 7.89746 11.9395 8.18848 11.8184 8.46191C11.7012 8.73535 11.541 8.97363 11.3379 9.17676C11.1348 9.37988 10.8965 9.54199 10.623 9.66309C10.3496 9.78027 10.0586 9.83887 9.75 9.83887C9.51562 9.83887 9.28711 9.80371 9.06445 9.7334C8.8418 9.66309 8.63672 9.55957 8.44922 9.42285C8.38672 9.48535 8.28125 9.59473 8.13281 9.75098C7.98828 9.90332 7.82031 10.0791 7.62891 10.2783C7.4375 10.4736 7.23438 10.6787 7.01953 10.8936C6.80859 11.1045 6.60938 11.2998 6.42188 11.4795C6.23438 11.6553 6.06836 11.8018 5.92383 11.9189C5.7793 12.0322 5.67969 12.0889 5.625 12.0889C5.52344 12.0889 5.43555 12.0518 5.36133 11.9775C5.28711 11.9033 5.25 11.8154 5.25 11.7139C5.25 11.6592 5.30664 11.5596 5.41992 11.415C5.53711 11.2705 5.68359 11.1045 5.85938 10.917C6.03906 10.7295 6.23438 10.5303 6.44531 10.3193C6.66016 10.1045 6.86523 9.90137 7.06055 9.70996C7.25977 9.51855 7.43555 9.35059 7.58789 9.20605C7.74414 9.05762 7.85352 8.95215 7.91602 8.88965C7.7793 8.70215 7.67578 8.49707 7.60547 8.27441C7.53516 8.05176 7.5 7.82324 7.5 7.58887C7.5 7.28027 7.55859 6.98926 7.67578 6.71582C7.79688 6.44238 7.95898 6.2041 8.16211 6.00098C8.36523 5.79785 8.60352 5.6377 8.87695 5.52051C9.15039 5.39941 9.44141 5.33887 9.75 5.33887ZM9.75 9.08887C9.95312 9.08887 10.1465 9.0498 10.3301 8.97168C10.5137 8.88965 10.6719 8.78223 10.8047 8.64941C10.9414 8.5127 11.0488 8.35449 11.127 8.1748C11.209 7.99121 11.25 7.7959 11.25 7.58887C11.25 7.38574 11.209 7.19238 11.127 7.00879C11.0488 6.8252 10.9414 6.66699 10.8047 6.53418C10.6719 6.39746 10.5137 6.29004 10.3301 6.21191C10.1465 6.12988 9.95312 6.08887 9.75 6.08887C9.54297 6.08887 9.34766 6.12988 9.16406 6.21191C8.98438 6.29004 8.82617 6.39746 8.68945 6.53418C8.55664 6.66699 8.44922 6.8252 8.36719 7.00879C8.28906 7.19238 8.25 7.38574 8.25 7.58887C8.25 7.7959 8.28906 7.99121 8.36719 8.1748C8.44922 8.35449 8.55664 8.5127 8.68945 8.64941C8.82617 8.78223 8.98438 8.88965 9.16406 8.97168C9.34766 9.0498 9.54297 9.08887 9.75 9.08887ZM1.5 8.33887V6.83887H0V0.0888672H9V1.58887H10.5V4.69434C10.375 4.66309 10.25 4.6377 10.125 4.61816C10 4.59863 9.875 4.58887 9.75 4.58887V2.33887H2.25V7.58887H6.75C6.75 7.71387 6.75977 7.83887 6.7793 7.96387C6.79883 8.08887 6.82422 8.21387 6.85547 8.33887H1.5ZM1.5 6.08887V1.58887H8.25V0.838867H0.75V6.08887H1.5Z" fill="#0078D4"/>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -0,0 +1,4 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 12C9.31371 12 12 9.31371 12 6C12 2.68629 9.31371 0 6 0C2.68629 0 0 2.68629 0 6C0 9.31371 2.68629 12 6 12Z" fill="#57A300"/>
<path d="M2.6652 6.21818C2.6097 6.15893 2.5812 6.08093 2.5842 5.99993C2.5872 5.91818 2.6217 5.84318 2.6817 5.78693L3.30795 5.20793C3.36495 5.15618 3.4377 5.12768 3.51345 5.12768C3.59745 5.12768 3.67845 5.16293 3.73545 5.22443L5.33745 6.94343L8.1927 3.28718C8.25045 3.21293 8.33745 3.16943 8.43195 3.16943C8.49945 3.16943 8.5632 3.19118 8.6172 3.23243L9.2967 3.75668C9.42645 3.85268 9.4542 4.03943 9.3567 4.17218L5.7057 8.84693C5.56395 9.02843 5.2932 9.04043 5.1357 8.87168L2.6652 6.21818Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 744 B

View File

@@ -0,0 +1,10 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0)">
<path d="M12.7002 7.14809L15.0552 9.50009L12.7002 11.8521L12.0002 11.1521L13.1412 10.0001H7.00024V9.00009H13.1412L12.0002 7.85209L12.7002 7.14809ZM10.0002 11.0001H11.0002V12.5001C11.004 12.7175 10.9531 12.9324 10.8522 13.1251C10.7458 13.3164 10.611 13.4904 10.4522 13.6411C10.1565 13.9236 9.80964 14.1473 9.43024 14.3001C9.00477 14.4762 8.56565 14.6173 8.11724 14.7221C7.66241 14.8298 7.2001 14.903 6.73424 14.9411C6.27624 14.9751 5.86491 14.9948 5.50024 15.0001C5.13557 15.0054 4.72424 14.9848 4.26624 14.9381C3.80358 14.8955 3.34424 14.8223 2.89124 14.7191C2.4427 14.6183 2.00348 14.4798 1.57824 14.3051C1.19415 14.1539 0.843761 13.9282 0.547239 13.6411C0.391829 13.4926 0.259978 13.3213 0.15624 13.1331C0.0539872 12.9377 0.000467488 12.7206 0.000239503 12.5001V3.50009C-0.00395315 3.2826 0.0469607 3.0676 0.14824 2.87509C0.254632 2.6838 0.389509 2.50981 0.548239 2.35909C0.844004 2.07656 1.19084 1.85292 1.57024 1.70009C1.99571 1.52397 2.43483 1.38284 2.88324 1.27809C3.33807 1.17038 3.80038 1.09717 4.26624 1.05909C4.72424 1.02509 5.13557 1.00542 5.50024 1.00009C5.86491 0.994755 6.27624 1.01542 6.73424 1.06209C7.1969 1.1047 7.65624 1.17786 8.10924 1.28109C8.55793 1.38351 8.99716 1.52368 9.42224 1.70009C9.80633 1.85127 10.1567 2.07694 10.4532 2.36409C10.6087 2.51255 10.7405 2.68385 10.8442 2.87209C10.9458 3.06589 10.9993 3.28128 11.0002 3.50009V8.00009H10.0002V4.98409C9.68514 5.18601 9.34665 5.34887 8.99224 5.46909C8.6121 5.60047 8.22313 5.70473 7.82824 5.78109C7.43171 5.85867 7.03105 5.91342 6.62824 5.94509C6.22557 5.97709 5.84957 5.99542 5.50024 6.00009C5.15091 6.00475 4.77591 5.98909 4.37524 5.95309C3.97195 5.92166 3.57112 5.8642 3.17524 5.78109C2.78145 5.70002 2.39278 5.59584 2.01124 5.46909C1.65408 5.35334 1.31405 5.19022 1.00024 4.98409V12.5001C1.00321 12.6172 1.05078 12.7288 1.13324 12.8121C1.22768 12.9168 1.33523 13.0089 1.45324 13.0861C1.57584 13.1676 1.70379 13.2408 1.83624 13.3051C1.96624 13.3671 2.07024 13.4171 2.14824 13.4531C2.40457 13.5592 2.66868 13.6455 2.93824 13.7111C3.22163 13.7802 3.50832 13.8349 3.79724 13.8751C4.09457 13.9171 4.38357 13.9484 4.66424 13.9691C4.94491 13.9898 5.22357 14.0001 5.50024 14.0001C5.76691 14.0001 6.04291 13.9924 6.32824 13.9771C6.62131 13.9606 6.91335 13.9292 7.20324 13.8831C7.50057 13.8364 7.78457 13.7791 8.05524 13.7111C8.32737 13.6431 8.59464 13.5569 8.85524 13.4531C8.93324 13.4224 9.03724 13.3754 9.16724 13.3121C9.30122 13.248 9.42937 13.1724 9.55024 13.0861C9.66572 13.0056 9.77292 12.9138 9.87024 12.8121C9.9114 12.7712 9.94408 12.7226 9.96639 12.6691C9.9887 12.6155 10.0002 12.5581 10.0002 12.5001V11.0001ZM1.00024 3.50009C1.00321 3.61723 1.05078 3.72882 1.13324 3.81209C1.22768 3.91681 1.33523 4.0089 1.45324 4.08609C1.57584 4.16762 1.70379 4.24079 1.83624 4.30509L2.14824 4.45309C2.40403 4.56068 2.66826 4.64697 2.93824 4.71109C3.21924 4.77909 3.50524 4.83309 3.79724 4.87509C4.08924 4.91709 4.37824 4.94809 4.66424 4.96909C4.95024 4.99009 5.22924 5.00009 5.50024 5.00009C5.77124 5.00009 6.04224 4.99209 6.32824 4.97709C6.6203 4.96047 6.91134 4.92909 7.20024 4.88309C7.49757 4.83642 7.78391 4.77909 8.05924 4.71109C8.3288 4.64547 8.59291 4.55921 8.84924 4.45309C8.92724 4.42175 9.03124 4.37475 9.16124 4.31209C9.29502 4.24763 9.42314 4.17203 9.54424 4.08609C9.65972 4.00558 9.76692 3.9138 9.86424 3.81209C9.90649 3.77172 9.94028 3.72334 9.96363 3.66977C9.98698 3.6162 9.99942 3.55852 10.0002 3.50009C9.99726 3.38294 9.94969 3.27135 9.86724 3.18809C9.7728 3.08337 9.66525 2.99128 9.54724 2.91409C9.4245 2.83427 9.29655 2.76277 9.16424 2.70009L8.85224 2.55209C8.59541 2.44945 8.33136 2.36589 8.06224 2.30209C7.78091 2.23409 7.49357 2.17509 7.20024 2.12509C6.91306 2.07878 6.62367 2.0474 6.33324 2.03109C6.04924 2.01609 5.77124 2.00509 5.50024 2.00009C5.22924 1.99509 4.95824 2.00809 4.67224 2.02309C4.37917 2.03961 4.08713 2.07098 3.79724 2.11709C3.49991 2.16442 3.21357 2.22175 2.93824 2.28909C2.66868 2.35471 2.40457 2.44096 2.14824 2.54709C2.07024 2.57775 1.96624 2.62475 1.83624 2.68809C1.70246 2.75255 1.57434 2.82815 1.45324 2.91409C1.33776 2.99459 1.23056 3.08638 1.13324 3.18809C1.09153 3.22871 1.0583 3.2772 1.03547 3.33076C1.01264 3.38431 1.00066 3.44187 1.00024 3.50009V3.50009Z" fill="#0078D4"/>
</g>
<defs>
<clipPath id="clip0">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

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');
});
});