/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import type * as azdataType 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 { IconPathHelper } from '../common/iconHelper'; import { IWorkspaceService } from '../common/interfaces'; import { fileExist, getAzdataApi } from '../common/utils'; export class ProjectDashboard { private dashboard: azdataType.window.ModelViewDashboard | undefined; private modelView: azdataType.ModelView | undefined; private projectProvider: IProjectProvider | undefined; private overviewTab: azdataType.DashboardTab | undefined; private rootContainer: azdataType.FlexContainer | undefined; private tableContainer: azdataType.Component | undefined; constructor(private _workspaceService: IWorkspaceService, private _treeItem: WorkspaceTreeItem) { } public async showDashboard(): Promise { 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, projectFilePath: string): Promise { this.dashboard = getAzdataApi()!.window.createModelViewDashboard(title, 'ProjectDashboard', { alwaysShowTabs: false }); this.dashboard.registerTabs(async (modelView: azdataType.ModelView) => { this.modelView = modelView; this.overviewTab = { title: '', id: 'overview-tab', content: this.createContainer(title, projectFilePath), toolbar: this.createToolbarContainer(projectFilePath) }; return [ this.overviewTab ]; }); } private createToolbarContainer(projectFilePath: string): azdataType.ToolbarContainer { const projectActions: (IProjectAction | IProjectActionGroup)[] = this.projectProvider!.projectToolbarActions; // Add actions as buttons const buttons: azdataType.ToolbarComponent[] = []; const projectActionsLength = projectActions.length; projectActions.forEach((action, actionIndex) => { if (this.isProjectAction(action)) { const button = this.createButton(action); buttons.push({ component: button, toolbarSeparatorAfter: (projectActionsLength - 1 === actionIndex) }); } 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 }); } }); const refreshButton = this.modelView!.modelBuilder.button() .withProps({ label: constants.Refresh, iconPath: IconPathHelper.refresh, height: '20px' }).component(); refreshButton.onDidClick(() => { this.rootContainer?.removeItem(this.tableContainer!); this.tableContainer = this.createTables(projectFilePath); this.rootContainer?.addItem(this.tableContainer); }); buttons.push({ component: refreshButton }); return this.modelView!.modelBuilder.toolbarContainer() .withToolbarItems( buttons ).component(); } private isProjectAction(obj: any): obj is IProjectAction { return obj.id !== undefined; } private createButton(projectAction: IProjectAction): azdataType.ButtonComponent { let button = this.modelView!.modelBuilder.button() .withProps({ label: projectAction.id, iconPath: projectAction.icon, height: '20px' }).component(); button.onDidClick(() => { projectAction.run(this._treeItem); }); return button; } private createContainer(title: string, projectFilePath: string): azdataType.FlexContainer { this.rootContainer = this.modelView!.modelBuilder.flexContainer().withLayout( { flexFlow: 'column', width: '100%', height: '100%' }).component(); const headerContainer = this.createHeader(title, projectFilePath); this.tableContainer = this.createTables(projectFilePath); this.rootContainer.addItem(headerContainer); this.rootContainer.addItem(this.tableContainer); return this.rootContainer; } /** * Create header with title, location and background */ private createHeader(title: string, location: string): azdataType.Component { const headerContainer = this.modelView!.modelBuilder.flexContainer().withLayout( { flexFlow: 'column', width: '100%', height: '30%' }).component(); const header = this.modelView!.modelBuilder.flexContainer().withLayout( { flexFlow: 'column', width: '100%', height: '30%' }).component(); const titleLabel = this.modelView!.modelBuilder.text() .withProps({ value: title, CSSStyles: { 'margin-block-start': '20px', 'margin-block-end': '0px' } }) .component(); header.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() .withProps({ value: projectFolderPath, CSSStyles: { 'margin-block-start': '20px', 'margin-block-end': '0px' } }) .component(); header.addItem(locationLabel, { CSSStyles: { 'padding-left': '34px', 'padding-top': '15px', 'padding-bottom': '50px', 'font-size': '16px' } }); const backgroundImage = this.projectProvider!.image; // background image added at the bottom right of the header // Files need to have the vscode-file scheme to be loaded by ADS const backgroundUri = vscode.Uri.file(backgroundImage!.light.toString()).with({ scheme: 'vscode-file' }); headerContainer.addItem(header, { CSSStyles: { 'background-image': `url(${backgroundUri})`, 'background-repeat': 'no-repeat', 'background-position': '85% bottom', 'background-size': '10%', 'border-bottom': 'solid 1px #ccc', 'width': '100%', 'height': '100%' } }); return headerContainer; } /** * Adds all the tables to the container */ private createTables(projectFile: string): azdataType.Component { const dashboardData: IDashboardTable[] = this.projectProvider!.getDashboardComponents(projectFile); const tableContainer = this.modelView!.modelBuilder.flexContainer().withLayout( { flexFlow: 'column', width: '100%', height: 'auto' }).component(); dashboardData.forEach(info => { const tableNameLabel = this.modelView!.modelBuilder.text() .withProps({ value: info.name, CSSStyles: { 'margin-block-start': '30px', 'margin-block-end': '0px' } }) .component(); tableContainer.addItem(tableNameLabel, { CSSStyles: { 'padding-left': '25px', 'padding-bottom': '20px', ...constants.cssStyles.title } }); if (info.data.length === 0) { const noDataText = constants.noPreviousData(info.name.toLocaleLowerCase()); const noDataLabel = this.modelView!.modelBuilder.text() .withProps({ value: noDataText }) .component(); tableContainer.addItem(noDataLabel, { CSSStyles: { 'padding-left': '25px', 'padding-bottom': '20px' } }); } else { const columns: azdataType.DeclarativeTableColumn[] = []; info.columns.forEach((column: IDashboardColumnInfo) => { let col = { displayName: column.displayName, valueType: column.type === 'icon' ? getAzdataApi()!.DeclarativeDataType.component : getAzdataApi()!.DeclarativeDataType.string, isReadOnly: true, width: column.width, headerCssStyles: { 'border': 'none', ...constants.cssStyles.tableHeader }, rowCssStyles: { ...constants.cssStyles.tableRow }, }; columns.push(col); }); const data: azdataType.DeclarativeTableCellValue[][] = []; info.data.forEach(values => { const columnValue: azdataType.DeclarativeTableCellValue[] = []; values.forEach(val => { if (typeof val === 'string') { columnValue.push({ value: val }); } else { const iconComponent = this.modelView!.modelBuilder.image().withProps({ iconPath: val.icon, width: '15px', height: '15px', iconHeight: '15px', iconWidth: '15px' }).component(); const stringComponent = this.modelView!.modelBuilder.text().withProps({ 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() .withProps({ columns: columns, dataValues: data, ariaLabel: info.name, CSSStyles: { 'margin-left': '30px' } }).component(); tableContainer.addItem(table); } }); return tableContainer; } }