diff --git a/build/gulpfile.vscode.js b/build/gulpfile.vscode.js index 130a7004ed..f4495c984d 100644 --- a/build/gulpfile.vscode.js +++ b/build/gulpfile.vscode.js @@ -77,9 +77,10 @@ const sqlBuiltInExtensions = [ 'agent', 'import', 'profiler', - 'admin-pack' + 'admin-pack', + 'big-data-cluster' ]; -var azureExtensions = [ 'azurecore', 'mssql']; +var azureExtensions = ['azurecore', 'mssql']; const vscodeEntryPoints = _.flatten([ buildfile.entrypoint('vs/workbench/workbench.main'), @@ -213,7 +214,7 @@ function getElectron(arch) { }); return gulp.src('package.json') - .pipe(json({ name: product.nameShort })) + .pipe(json({ name: product.nameShort })) .pipe(electron(electronOpts)) .pipe(filter(['**', '!**/app/package.json'])) .pipe(vfs.dest('.build/electron')); @@ -262,21 +263,21 @@ function computeChecksum(filename) { function packageBuiltInExtensions() { const sqlBuiltInLocalExtensionDescriptions = glob.sync('extensions/*/package.json') - .map(manifestPath => { - const extensionPath = path.dirname(path.join(root, manifestPath)); - const extensionName = path.basename(extensionPath); - return { name: extensionName, path: extensionPath }; - }) - .filter(({ name }) => excludedExtensions.indexOf(name) === -1) - .filter(({ name }) => builtInExtensions.every(b => b.name !== name)) - .filter(({ name }) => sqlBuiltInExtensions.indexOf(name) >= 0); + .map(manifestPath => { + const extensionPath = path.dirname(path.join(root, manifestPath)); + const extensionName = path.basename(extensionPath); + return { name: extensionName, path: extensionPath }; + }) + .filter(({ name }) => excludedExtensions.indexOf(name) === -1) + .filter(({ name }) => builtInExtensions.every(b => b.name !== name)) + .filter(({ name }) => sqlBuiltInExtensions.indexOf(name) >= 0); sqlBuiltInLocalExtensionDescriptions.forEach(element => { const packagePath = path.join(path.dirname(root), element.name + '.vsix'); console.info('Creating vsix for ' + element.path + ' result:' + packagePath); vsce.createVSIX({ - cwd: element.path, - packagePath: packagePath, - useYarn: true + cwd: element.path, + packagePath: packagePath, + useYarn: true }); }); } @@ -398,7 +399,7 @@ function packageTask(platform, arch, opts) { // TODO the API should be copied to `out` during compile, not here const api = gulp.src('src/vs/vscode.d.ts').pipe(rename('out/vs/vscode.d.ts')); - // {{SQL CARBON EDIT}} + // {{SQL CARBON EDIT}} const dataApi = gulp.src('src/vs/data.d.ts').pipe(rename('out/sql/data.d.ts')); const depsSrc = [ @@ -516,7 +517,7 @@ gulp.task('clean-vscode-linux-x64', util.rimraf(path.join(buildRoot, 'azuredatas gulp.task('clean-vscode-linux-arm', util.rimraf(path.join(buildRoot, 'azuredatastudio-linux-arm'))); gulp.task('vscode-win32-ia32', ['optimize-vscode', 'clean-vscode-win32-ia32'], packageTask('win32', 'ia32')); -gulp.task('vscode-win32-x64', ['vscode-win32-x64-azurecore', 'vscode-win32-x64-mssql', 'optimize-vscode', 'clean-vscode-win32-x64'], packageTask('win32', 'x64')); +gulp.task('vscode-win32-x64', ['vscode-win32-x64-azurecore', 'vscode-win32-x64-mssql', 'optimize-vscode', 'clean-vscode-win32-x64'], packageTask('win32', 'x64')); gulp.task('vscode-darwin', ['vscode-darwin-azurecore', 'vscode-darwin-mssql', 'optimize-vscode', 'clean-vscode-darwin'], packageTask('darwin')); gulp.task('vscode-linux-ia32', ['optimize-vscode', 'clean-vscode-linux-ia32'], packageTask('linux', 'ia32')); gulp.task('vscode-linux-x64', ['vscode-linux-x64-azurecore', 'vscode-linux-x64-mssql', 'optimize-vscode', 'clean-vscode-linux-x64'], packageTask('linux', 'x64')); @@ -572,9 +573,9 @@ gulp.task('vscode-translations-push-test', ['optimize-vscode'], function () { gulp.src(pathToMetadata).pipe(i18n.createXlfFilesForCoreBundle()), gulp.src(pathToSetup).pipe(i18n.createXlfFilesForIsl()), gulp.src(pathToExtensions).pipe(i18n.createXlfFilesForExtensions()) - // {{SQL CARBON EDIT}} - // disable since function makes calls to VS Code Transifex API - // ).pipe(i18n.findObsoleteResources(apiHostname, apiName, apiToken) + // {{SQL CARBON EDIT}} + // disable since function makes calls to VS Code Transifex API + // ).pipe(i18n.findObsoleteResources(apiHostname, apiName, apiToken) ).pipe(vfs.dest('../vscode-transifex-input')); }); @@ -656,7 +657,7 @@ function getSettingsSearchBuildId(packageJson) { const branch = process.env.BUILD_SOURCEBRANCH; const branchId = branch.indexOf('/release/') >= 0 ? 0 : /\/master$/.test(branch) ? 1 : - 2; // Some unexpected branch + 2; // Some unexpected branch const out = cp.execSync(`git rev-list HEAD --count`); const count = parseInt(out.toString()); @@ -729,6 +730,6 @@ function installService() { } gulp.task('install-sqltoolsservice', () => { - return installService(); + return installService(); }); diff --git a/extensions/big-data-cluster/README.md b/extensions/big-data-cluster/README.md new file mode 100644 index 0000000000..856d62333d --- /dev/null +++ b/extensions/big-data-cluster/README.md @@ -0,0 +1,17 @@ +# Microsoft SQL Server big data cluster Extension for Azure Data Studio + +Welcome to Microsoft SQL Server big data cluster Extension for Azure Data Studio! + +## Code of Conduct + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. + +## Privacy Statement + +The [Microsoft Enterprise and Developer Privacy Statement](https://privacy.microsoft.com/en-us/privacystatement) describes the privacy statement of this software. + +## License + +Copyright (c) Microsoft Corporation. All rights reserved. + +Licensed under the [Source EULA](https://raw.githubusercontent.com/Microsoft/azuredatastudio/master/LICENSE.txt). diff --git a/extensions/big-data-cluster/images/cluster.svg b/extensions/big-data-cluster/images/cluster.svg new file mode 100644 index 0000000000..e0e8e68f41 --- /dev/null +++ b/extensions/big-data-cluster/images/cluster.svg @@ -0,0 +1 @@ +cluster \ No newline at end of file diff --git a/extensions/big-data-cluster/images/cluster_inverse.svg b/extensions/big-data-cluster/images/cluster_inverse.svg new file mode 100644 index 0000000000..b35c0c5d36 --- /dev/null +++ b/extensions/big-data-cluster/images/cluster_inverse.svg @@ -0,0 +1 @@ +cluster_inverse \ No newline at end of file diff --git a/extensions/big-data-cluster/images/sqlserver.png b/extensions/big-data-cluster/images/sqlserver.png new file mode 100644 index 0000000000..d884faa14a Binary files /dev/null and b/extensions/big-data-cluster/images/sqlserver.png differ diff --git a/extensions/big-data-cluster/package.json b/extensions/big-data-cluster/package.json new file mode 100644 index 0000000000..b02eea39f4 --- /dev/null +++ b/extensions/big-data-cluster/package.json @@ -0,0 +1,41 @@ +{ + "name": "big-data-cluster", + "displayName": "SQL Server big data cluster", + "description": "SQL Server big data cluster", + "version": "0.0.1", + "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": "0.10.x" + }, + "activationEvents": [ + "*" + ], + "main": "./out/main", + "repository": { + "type": "git", + "url": "https://github.com/Microsoft/azuredatastudio.git" + }, + "extensionDependencies": [ + "Microsoft.mssql" + ], + "contributes": { + "commands": [ + { + "command": "mssql.cluster.create", + "title": "Create SQL Server big data cluster", + "category": "SQL Server" + } + ] + }, + "dependencies": { + "vscode-nls": "^3.2.1" + }, + "devDependencies": { + "mocha-junit-reporter": "^1.17.0", + "mocha-multi-reporters": "^1.1.7" + } +} diff --git a/extensions/big-data-cluster/src/data/kubeConfigParser.ts b/extensions/big-data-cluster/src/data/kubeConfigParser.ts new file mode 100644 index 0000000000..022b1d981a --- /dev/null +++ b/extensions/big-data-cluster/src/data/kubeConfigParser.ts @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { ClusterInfo } from '../interfaces'; + +export interface IKubeConfigParser { + parse(configPath: string): ClusterInfo[]; +} + +export class TestKubeConfigParser implements IKubeConfigParser { + parse(configPath: string): ClusterInfo[] { + let clusters = []; + for (let i = 0; i < 18; i++) { + let name; + if (i % 2 === 0) { + name = `kubernetes cluster ${i}`; + } + else { + name = 'cluster dev ' + i; + } + clusters.push( + { + displayName: name, + name: `kub-dev-xxxx-cluster-${i}`, + user: 'root' + } + ); + } + return clusters; + } +} + diff --git a/extensions/big-data-cluster/src/interfaces.ts b/extensions/big-data-cluster/src/interfaces.ts new file mode 100644 index 0000000000..ce021226e1 --- /dev/null +++ b/extensions/big-data-cluster/src/interfaces.ts @@ -0,0 +1,17 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +export interface ClusterInfo { + name: string; + displayName: string; + user: string; +} + +export enum TargetClusterType { + ExistingKubernetesCluster, + NewLocalCluster, + NewAksCluster +} diff --git a/extensions/big-data-cluster/src/main.ts b/extensions/big-data-cluster/src/main.ts new file mode 100644 index 0000000000..c4105a8e60 --- /dev/null +++ b/extensions/big-data-cluster/src/main.ts @@ -0,0 +1,21 @@ +/*--------------------------------------------------------------------------------------------- + * 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 vscode = require('vscode'); +import { MainController } from './mainController'; +export let controller: MainController; + +export function activate(context: vscode.ExtensionContext) { + controller = new MainController(context); + controller.activate(); +} + +// this method is called when your extension is deactivated +export function deactivate(): void { + if (controller) { + controller.deactivate(); + } +} diff --git a/extensions/big-data-cluster/src/mainController.ts b/extensions/big-data-cluster/src/mainController.ts new file mode 100644 index 0000000000..00ae5964ce --- /dev/null +++ b/extensions/big-data-cluster/src/mainController.ts @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * 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 vscode from 'vscode'; +import { CreateClusterWizard } from './wizards/create-cluster/createClusterWizard'; + +/** + * The main controller class that initializes the extension + */ +export class MainController { + protected _context: vscode.ExtensionContext; + + public constructor(context: vscode.ExtensionContext) { + this._context = context; + } + + /** + * Activates the extension + */ + public activate(): void { + vscode.commands.registerCommand('mssql.cluster.create', () => { + let wizard = new CreateClusterWizard(this._context); + wizard.open(); + }); + } + + /** + * Deactivates the extension + */ + public deactivate(): void { } +} diff --git a/extensions/big-data-cluster/src/typings/ref.d.ts b/extensions/big-data-cluster/src/typings/ref.d.ts new file mode 100644 index 0000000000..41e273db7f --- /dev/null +++ b/extensions/big-data-cluster/src/typings/ref.d.ts @@ -0,0 +1,9 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/// +/// +/// +/// \ No newline at end of file diff --git a/extensions/big-data-cluster/src/wizards/create-cluster/createClusterModel.ts b/extensions/big-data-cluster/src/wizards/create-cluster/createClusterModel.ts new file mode 100644 index 0000000000..58d56546a7 --- /dev/null +++ b/extensions/big-data-cluster/src/wizards/create-cluster/createClusterModel.ts @@ -0,0 +1,26 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { IKubeConfigParser } from '../../data/kubeConfigParser'; +import { ClusterInfo, TargetClusterType } from '../../interfaces'; + +export class CreateClusterModel { + + constructor(private _kubeConfigParser: IKubeConfigParser) { + } + + public loadClusters(configPath: string): ClusterInfo[] { + return this._kubeConfigParser.parse(configPath); + } + + public targetClusterType: TargetClusterType; + + public selectedCluster: ClusterInfo; + + public adminUserName: string; + + public adminPassword: string; +} diff --git a/extensions/big-data-cluster/src/wizards/create-cluster/createClusterWizard.ts b/extensions/big-data-cluster/src/wizards/create-cluster/createClusterWizard.ts new file mode 100644 index 0000000000..a2ab1d87a3 --- /dev/null +++ b/extensions/big-data-cluster/src/wizards/create-cluster/createClusterWizard.ts @@ -0,0 +1,45 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { CreateClusterModel } from './createClusterModel'; +import { SelectTargetClusterPage } from './pages/targetClusterPage'; +import { SummaryPage } from './pages/summaryPage'; +import { SettingsPage } from './pages/settingsPage'; +import { ClusterProfilePage } from './pages/clusterProfilePage'; +import { TestKubeConfigParser } from '../../data/kubeConfigParser'; +import { ExtensionContext } from 'vscode'; +import { WizardBase } from '../wizardBase'; +import * as nls from 'vscode-nls'; + +const localize = nls.loadMessageBundle(); + +export class CreateClusterWizard extends WizardBase { + constructor(context: ExtensionContext) { + let configParser = new TestKubeConfigParser(); + let model = new CreateClusterModel(configParser); + super(model, context, localize('bdc-create.wizardTitle', 'Create a big data cluster')); + } + + protected initialize(): void { + let settingsPage = new SettingsPage(this.model, this); + let clusterProfilePage = new ClusterProfilePage(this.model, this); + let selectTargetClusterPage = new SelectTargetClusterPage(this.model, this); + let summaryPage = new SummaryPage(this.model, this); + + this.wizard.pages = [ + settingsPage.page, + clusterProfilePage.page, + selectTargetClusterPage.page, + summaryPage.page + ]; + + this.wizard.generateScriptButton.label = localize('bdc-create.generateScriptsButtonText', 'Generate Scripts'); + this.wizard.doneButton.label = localize('bdc-create.createClusterButtonText', 'Create'); + + this.wizard.generateScriptButton.onClick(() => { }); + this.wizard.doneButton.onClick(() => { }); + } +} diff --git a/extensions/big-data-cluster/src/wizards/create-cluster/pages/clusterProfilePage.ts b/extensions/big-data-cluster/src/wizards/create-cluster/pages/clusterProfilePage.ts new file mode 100644 index 0000000000..deebb8eea3 --- /dev/null +++ b/extensions/big-data-cluster/src/wizards/create-cluster/pages/clusterProfilePage.ts @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * 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 sqlops from 'sqlops'; +import { WizardPageBase } from '../../wizardPageBase'; +import { CreateClusterModel } from '../createClusterModel'; +import { WizardBase } from '../../wizardBase'; +import * as nls from 'vscode-nls'; + +const localize = nls.loadMessageBundle(); + +export class ClusterProfilePage extends WizardPageBase { + constructor(model: CreateClusterModel, wizard: WizardBase) { + super(localize('bdc-create.clusterProfilePageTitle', 'Select a cluster profile'), + localize('bdc-create.clusterProfilePageDescription', 'Select your requirement and we will provide you a pre-defined default scaling. You can later go to cluster configuration and customize it.'), + model, wizard); + } + + protected initialize(view: sqlops.ModelView): Thenable { + let formBuilder = view.modelBuilder.formContainer(); + let form = formBuilder.component(); + return view.initializeModel(form); + } +} diff --git a/extensions/big-data-cluster/src/wizards/create-cluster/pages/settingsPage.ts b/extensions/big-data-cluster/src/wizards/create-cluster/pages/settingsPage.ts new file mode 100644 index 0000000000..3995e5930b --- /dev/null +++ b/extensions/big-data-cluster/src/wizards/create-cluster/pages/settingsPage.ts @@ -0,0 +1,76 @@ +/*--------------------------------------------------------------------------------------------- + * 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 sqlops from 'sqlops'; +import { WizardPageBase } from '../../wizardPageBase'; +import { CreateClusterModel } from '../createClusterModel'; +import { WizardBase } from '../../wizardBase'; +import * as nls from 'vscode-nls'; + +const localize = nls.loadMessageBundle(); + +export class SettingsPage extends WizardPageBase { + constructor(model: CreateClusterModel, wizard: WizardBase) { + super(localize('bdc-create.settingsPageTitle', 'Settings'), + localize('bdc-create.settingsPageDescription', 'Configure the settings required for deploying SQL Server big data cluster'), + model, wizard); + } + + protected initialize(view: sqlops.ModelView): Thenable { + let formBuilder = view.modelBuilder.formContainer(); + let adminUserNameInput = this.createInputWithLabel(view, localize('bdc-create.AdminUsernameText', 'Admin username'), true, (inputBox: sqlops.InputBoxComponent) => { + this.model.adminUserName = inputBox.value; + }); + let adminPasswordInput = this.createInputWithLabel(view, localize('bdc-create.AdminUserPasswordText', 'Password'), true, (inputBox: sqlops.InputBoxComponent) => { + this.model.adminPassword = inputBox.value; + }, 'password'); + + let basicSettingsGroup = view.modelBuilder.groupContainer().withItems([adminUserNameInput, adminPasswordInput]).withLayout({ header: localize('bdc-create.BasicSettingsText', 'Basic Settings'), collapsible: true }).component(); + let dockerSettingsGroup = view.modelBuilder.groupContainer().withItems([]).withLayout({ header: localize('bdc-create.DockerSettingsText', 'Docker Settings'), collapsible: true }).component(); + + let acceptEulaCheckbox = view.modelBuilder.checkBox().component(); + acceptEulaCheckbox.label = localize('bdc-create.AcceptEulaText', 'I accept the SQL Server EULA'); + acceptEulaCheckbox.checked = false; + + let eulaHyperlink = view.modelBuilder.hyperlink().withProperties({ + label: localize('bdc-create.ViewEulaText', 'View Eula'), + url: 'https://docs.microsoft.com/en-us/sql/getting-started/about-the-sql-server-license-terms?view=sql-server-2014' + }).component(); + + let eulaContainer = this.createRow(view, [acceptEulaCheckbox, eulaHyperlink]); + + let form = formBuilder.withFormItems([ + { + title: '', + component: basicSettingsGroup + }, { + title: '', + component: dockerSettingsGroup + }, { + title: '', + component: eulaContainer + }]).component(); + return view.initializeModel(form); + } + + private createInputWithLabel(view: sqlops.ModelView, label: string, isRequiredField: boolean, textChangedHandler: (inputBox: sqlops.InputBoxComponent) => void, inputType: string = 'text'): sqlops.FlexContainer { + let input = view.modelBuilder.inputBox().withProperties({ + required: isRequiredField, + inputType: inputType + }).component(); + let text = view.modelBuilder.text().withProperties({ value: label }).component(); + input.width = '300px'; + text.width = '150px'; + input.onTextChanged(() => { + textChangedHandler(input); + }); + return this.createRow(view, [text, input]); + } + + private createRow(view: sqlops.ModelView, items: sqlops.Component[]): sqlops.FlexContainer { + return view.modelBuilder.flexContainer().withItems(items, { CSSStyles: { 'margin-right': '10px' } }).withLayout({ flexFlow: 'row', alignItems: 'baseline' }).component(); + } +} diff --git a/extensions/big-data-cluster/src/wizards/create-cluster/pages/summaryPage.ts b/extensions/big-data-cluster/src/wizards/create-cluster/pages/summaryPage.ts new file mode 100644 index 0000000000..e22a4330a0 --- /dev/null +++ b/extensions/big-data-cluster/src/wizards/create-cluster/pages/summaryPage.ts @@ -0,0 +1,25 @@ +/*--------------------------------------------------------------------------------------------- + * 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 sqlops from 'sqlops'; +import { WizardPageBase } from '../../wizardPageBase'; +import { CreateClusterModel } from '../createClusterModel'; +import { WizardBase } from '../../wizardBase'; +import * as nls from 'vscode-nls'; + +const localize = nls.loadMessageBundle(); + +export class SummaryPage extends WizardPageBase { + constructor(model: CreateClusterModel, wizard: WizardBase) { + super(localize('bdc-create.summaryPageTitle', 'Summary'), '', model, wizard); + } + + protected initialize(view: sqlops.ModelView): Thenable { + let formBuilder = view.modelBuilder.formContainer(); + let form = formBuilder.component(); + return view.initializeModel(form); + } +} diff --git a/extensions/big-data-cluster/src/wizards/create-cluster/pages/targetClusterPage.ts b/extensions/big-data-cluster/src/wizards/create-cluster/pages/targetClusterPage.ts new file mode 100644 index 0000000000..4e2fe5bd05 --- /dev/null +++ b/extensions/big-data-cluster/src/wizards/create-cluster/pages/targetClusterPage.ts @@ -0,0 +1,176 @@ +/*--------------------------------------------------------------------------------------------- + * 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 sqlops from 'sqlops'; +import * as vscode from 'vscode'; +import * as os from 'os'; +import { WizardPageBase } from '../../wizardPageBase'; +import { CreateClusterModel } from '../createClusterModel'; +import { WizardBase } from '../../wizardBase'; +import { TargetClusterType } from '../../../interfaces'; +import * as nls from 'vscode-nls'; + +const localize = nls.loadMessageBundle(); +const ClusterTypeRadioButtonGroupName = 'SelectClusterType'; + +export class SelectTargetClusterPage extends WizardPageBase { + constructor(model: CreateClusterModel, wizard: WizardBase) { + super(localize('bdc-create.selectTargetClusterPageTitle', 'Where do you want to deploy this SQL Server big data cluster?'), + localize('bdc-create.selectTargetClusterPageDescription', 'Select an existing Kubernetes cluster or choose a cluster type you want to deploy'), + model, wizard); + } + + private existingClusterOption: sqlops.RadioButtonComponent; + private createLocalClusterOption: sqlops.RadioButtonComponent; + private createAksClusterOption: sqlops.RadioButtonComponent; + private pageContainer: sqlops.DivContainer; + private existingClusterControl: sqlops.FlexContainer; + private createLocalClusterControl: sqlops.FlexContainer; + private createAksClusterControl: sqlops.FlexContainer; + private clusterContextsLabel: sqlops.TextComponent; + private errorLoadingClustersLabel: sqlops.TextComponent; + private clusterContextContainer: sqlops.DivContainer; + + private cards: sqlops.CardComponent[]; + + protected initialize(view: sqlops.ModelView): Thenable { + let self = this; + this.model.targetClusterType = TargetClusterType.ExistingKubernetesCluster; + this.existingClusterOption = this.createTargetTypeRadioButton(view, localize('bdc-create.existingK8sCluster', 'Existing Kubernetes cluster'), true); + this.createLocalClusterOption = this.createTargetTypeRadioButton(view, localize('bdc-create.createLocalCluster', 'Create new local cluster')); + this.createAksClusterOption = this.createTargetTypeRadioButton(view, localize('bdc-create.createAksCluster', 'Create new Azure Kubernetes Service cluster')); + + this.existingClusterOption.onDidClick(() => { + self.pageContainer.clearItems(); + self.pageContainer.addItem(self.existingClusterControl); + self.model.targetClusterType = TargetClusterType.ExistingKubernetesCluster; + }); + + this.createLocalClusterOption.onDidClick(() => { + self.pageContainer.clearItems(); + self.pageContainer.addItem(self.createLocalClusterControl); + self.model.targetClusterType = TargetClusterType.NewLocalCluster; + }); + + this.createAksClusterOption.onDidClick(() => { + self.pageContainer.clearItems(); + self.pageContainer.addItem(self.createAksClusterControl); + self.model.targetClusterType = TargetClusterType.NewAksCluster; + }); + + let optionGroup = view.modelBuilder.divContainer().withItems([this.existingClusterOption, this.createLocalClusterOption, this.createAksClusterOption], + { CSSStyles: { 'margin-right': '30px' } }).withLayout({ width: 'auto' }).component(); + this.initExistingClusterControl(view); + this.initLocalClusterControl(view); + this.initAksClusterControl(view); + this.pageContainer = view.modelBuilder.divContainer().withItems([this.existingClusterControl]).withLayout({ width: '100%' }).component(); + let container = view.modelBuilder.flexContainer().withItems([optionGroup, this.pageContainer], { flex: '0 0 auto' }).withLayout({ flexFlow: 'row', alignItems: 'left' }).component(); + + let formBuilder = view.modelBuilder.formContainer().withFormItems( + [ + { + component: container, + title: '' + } + ], + { + horizontal: true + } + ).withLayout({ width: '100%', height: '100%' }); + + let form = formBuilder.component(); + return view.initializeModel(form); + } + + private createTargetTypeRadioButton(view: sqlops.ModelView, label: string, checked: boolean = false): sqlops.RadioButtonComponent { + return view.modelBuilder.radioButton().withProperties({ label: label, name: ClusterTypeRadioButtonGroupName, checked: checked }).component(); + } + + private initExistingClusterControl(view: sqlops.ModelView): void { + let self = this; + let sectionDescription = view.modelBuilder.text().withProperties({ value: localize('bdc-create.existingClusterSectionDescription', 'Select the cluster context you want to install the SQL Server big data cluster') }).component(); + let configFileLabel = view.modelBuilder.text().withProperties({ value: localize('bdc-create.kubeConfigFileLabelText', 'KubeConfig File') }).component(); + let configFileInput = view.modelBuilder.inputBox().withProperties({ width: '300px' }).component(); + let browseFileButton = view.modelBuilder.button().withProperties({ label: localize('bdc-browseText', 'Browse'), width: '100px' }).component(); + let configFileContainer = view.modelBuilder.flexContainer() + .withLayout({ flexFlow: 'row', alignItems: 'baseline' }) + .withItems([configFileLabel, configFileInput, browseFileButton], { CSSStyles: { 'margin-right': '10px' } }).component(); + this.clusterContextsLabel = view.modelBuilder.text().withProperties({ value: localize('bdc-clusterContextsLabelText', 'Cluster Contexts') }).component(); + this.errorLoadingClustersLabel = view.modelBuilder.text().withProperties({ value: localize('bdc-errorLoadingClustersText', 'No cluster information is found in the config file or an error ocurred while loading the config file') }).component(); + this.clusterContextContainer = view.modelBuilder.divContainer().component(); + this.existingClusterControl = view.modelBuilder.divContainer().withItems([sectionDescription, configFileContainer, this.clusterContextContainer], { CSSStyles: { 'margin-top': '0px' } }).component(); + + browseFileButton.onDidClick(async () => { + let fileUris = await vscode.window.showOpenDialog( + { + canSelectFiles: true, + canSelectFolders: false, + canSelectMany: false, + defaultUri: vscode.Uri.file(os.homedir()), + openLabel: localize('bdc-selectKubeConfigFileText', 'Select'), + filters: { + 'KubeConfig Files': ['kubeconfig'], + } + } + ); + + if (!fileUris || fileUris.length === 0) { + return; + } + self.clusterContextContainer.clearItems(); + + let fileUri = fileUris[0]; + + configFileInput.value = fileUri.fsPath; + + let clusters = self.model.loadClusters(fileUri.fsPath); + + self.cards = []; + if (clusters.length !== 0) { + self.model.selectedCluster = clusters[0]; + for (let i = 0; i < clusters.length; i++) { + let cluster = clusters[i]; + let card = view.modelBuilder.card().withProperties({ + selected: i === 0, + label: cluster.name, + descriptions: [cluster.displayName, cluster.user], + cardType: sqlops.CardType.ListItem, + iconPath: { + dark: self.wizard.context.asAbsolutePath('images/cluster_inverse.svg'), + light: self.wizard.context.asAbsolutePath('images/cluster.svg') + }, + }).component(); + card.onCardSelectedChanged(() => { + if (card.selected) { + self.cards.forEach(c => { + if (c !== card) { + c.selected = false; + } + }); + self.model.selectedCluster = cluster; + } + }); + self.cards.push(card); + } + + self.clusterContextContainer.addItem(self.clusterContextsLabel); + self.clusterContextContainer.addItems(self.cards); + } else { + self.clusterContextContainer.addItem(this.errorLoadingClustersLabel); + } + }); + } + + private initLocalClusterControl(view: sqlops.ModelView): void { + let placeholder = view.modelBuilder.text().withProperties({ value: 'create local cluster place holder' }).component(); + this.createLocalClusterControl = view.modelBuilder.divContainer().withItems([placeholder]).component(); + } + + private initAksClusterControl(view: sqlops.ModelView): void { + let placeholder = view.modelBuilder.text().withProperties({ value: 'AKS cluster place holder' }).component(); + this.createAksClusterControl = view.modelBuilder.divContainer().withItems([placeholder]).component(); + } +} diff --git a/extensions/big-data-cluster/src/wizards/wizardBase.ts b/extensions/big-data-cluster/src/wizards/wizardBase.ts new file mode 100644 index 0000000000..31b5b5983d --- /dev/null +++ b/extensions/big-data-cluster/src/wizards/wizardBase.ts @@ -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. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import * as sqlops from 'sqlops'; +import { ExtensionContext } from 'vscode'; + +export abstract class WizardBase { + + protected wizard: sqlops.window.modelviewdialog.Wizard; + + constructor(public model: T, public context: ExtensionContext, private title: string) { + } + + public open(): Thenable { + this.wizard = sqlops.window.modelviewdialog.createWizard(this.title); + this.initialize(); + return this.wizard.open(); + } + + protected abstract initialize(): void; +} diff --git a/extensions/big-data-cluster/src/wizards/wizardPageBase.ts b/extensions/big-data-cluster/src/wizards/wizardPageBase.ts new file mode 100644 index 0000000000..3415365f24 --- /dev/null +++ b/extensions/big-data-cluster/src/wizards/wizardPageBase.ts @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * 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 sqlops from 'sqlops'; +import { WizardBase } from './wizardBase'; + +export abstract class WizardPageBase { + private _page: sqlops.window.modelviewdialog.WizardPage; + + public get page(): sqlops.window.modelviewdialog.WizardPage { + return this._page; + } + + public get wizard(): WizardBase { + return this._wizard; + } + + constructor(title: string, description: string, protected model: T, private _wizard: WizardBase) { + this._page = sqlops.window.modelviewdialog.createWizardPage(title); + this._page.description = description; + this._page.registerContent((view: sqlops.ModelView) => { + return this.initialize(view); + }); + } + + protected abstract initialize(view: sqlops.ModelView): Thenable; +} diff --git a/extensions/big-data-cluster/tsconfig.json b/extensions/big-data-cluster/tsconfig.json new file mode 100644 index 0000000000..e6baa6d40d --- /dev/null +++ b/extensions/big-data-cluster/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compileOnSave": true, + "compilerOptions": { + "module": "commonjs", + "target": "es6", + "outDir": "./out", + "lib": [ + "es6", "es2015.promise" + ], + "sourceMap": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "moduleResolution": "node", + "declaration": true + }, + "exclude": [ + "node_modules" + ] +} diff --git a/extensions/big-data-cluster/yarn.lock b/extensions/big-data-cluster/yarn.lock new file mode 100644 index 0000000000..596deea0d6 --- /dev/null +++ b/extensions/big-data-cluster/yarn.lock @@ -0,0 +1,109 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +ansi-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" + integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= + +charenc@~0.0.1: + version "0.0.2" + resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" + integrity sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc= + +crypt@~0.0.1: + version "0.0.2" + resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b" + integrity sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs= + +debug@^2.2.0: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@^3.1.0: + version "3.2.6" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" + integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== + dependencies: + ms "^2.1.1" + +is-buffer@~1.1.1: + version "1.1.6" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" + integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== + +lodash@^4.16.4: + version "4.17.11" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" + integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg== + +md5@^2.1.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/md5/-/md5-2.2.1.tgz#53ab38d5fe3c8891ba465329ea23fac0540126f9" + integrity sha1-U6s41f48iJG6RlMp6iP6wFQBJvk= + dependencies: + charenc "~0.0.1" + crypt "~0.0.1" + is-buffer "~1.1.1" + +minimist@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" + integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= + +mkdirp@~0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" + integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= + dependencies: + minimist "0.0.8" + +mocha-junit-reporter@^1.17.0: + version "1.18.0" + resolved "https://registry.yarnpkg.com/mocha-junit-reporter/-/mocha-junit-reporter-1.18.0.tgz#9209a3fba30025ae3ae5e6bfe7f9c5bc3c2e8ee2" + integrity sha512-y3XuqKa2+HRYtg0wYyhW/XsLm2Ps+pqf9HaTAt7+MVUAKFJaNAHOrNseTZo9KCxjfIbxUWwckP5qCDDPUmjSWA== + dependencies: + debug "^2.2.0" + md5 "^2.1.0" + mkdirp "~0.5.1" + strip-ansi "^4.0.0" + xml "^1.0.0" + +mocha-multi-reporters@^1.1.7: + version "1.1.7" + resolved "https://registry.yarnpkg.com/mocha-multi-reporters/-/mocha-multi-reporters-1.1.7.tgz#cc7f3f4d32f478520941d852abb64d9988587d82" + integrity sha1-zH8/TTL0eFIJQdhSq7ZNmYhYfYI= + dependencies: + debug "^3.1.0" + lodash "^4.16.4" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= + +ms@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" + integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== + +strip-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" + integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8= + dependencies: + ansi-regex "^3.0.0" + +vscode-nls@^3.2.1: + version "3.2.5" + resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-3.2.5.tgz#25520c1955108036dec607c85e00a522f247f1a4" + integrity sha512-ITtoh3V4AkWXMmp3TB97vsMaHRgHhsSFPsUdzlueSL+dRZbSNTZeOmdQv60kjCV306ghPxhDeoNUEm3+EZMuyw== + +xml@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/xml/-/xml-1.0.1.tgz#78ba72020029c5bc87b8a81a3cfcd74b4a2fc1e5" + integrity sha1-eLpyAgApxbyHuKgaPPzXS0ovweU= diff --git a/src/sql/media/icons/collapsed.svg b/src/sql/media/icons/collapsed.svg new file mode 100644 index 0000000000..3a63808c35 --- /dev/null +++ b/src/sql/media/icons/collapsed.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/sql/media/icons/collapsed_inverse.svg b/src/sql/media/icons/collapsed_inverse.svg new file mode 100644 index 0000000000..cf5c3641aa --- /dev/null +++ b/src/sql/media/icons/collapsed_inverse.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/sql/media/icons/expanded.svg b/src/sql/media/icons/expanded.svg new file mode 100644 index 0000000000..75f73adbb0 --- /dev/null +++ b/src/sql/media/icons/expanded.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/sql/media/icons/expanded_inverse.svg b/src/sql/media/icons/expanded_inverse.svg new file mode 100644 index 0000000000..73d41e6399 --- /dev/null +++ b/src/sql/media/icons/expanded_inverse.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/sql/parts/modelComponents/card.component.html b/src/sql/parts/modelComponents/card.component.html index c04190297d..350f88a426 100644 --- a/src/sql/parts/modelComponents/card.component.html +++ b/src/sql/parts/modelComponents/card.component.html @@ -1,33 +1,49 @@ - - - - - - - - - - - + + + + + + + + + + + + + + {{label}} - {{label}} - - + - - - {{label}} - {{value}} - - - - {{action.label}} - - {{action.actionTitle}} - - - - + + + {{label}} + {{value}} + + + + {{action.label}} + + {{action.actionTitle}} + + + + + + + + + + + {{label}} + + {{desc}} + + + + + \ No newline at end of file diff --git a/src/sql/parts/modelComponents/card.component.ts b/src/sql/parts/modelComponents/card.component.ts index 8d701ef1ce..d27b9d30f3 100644 --- a/src/sql/parts/modelComponents/card.component.ts +++ b/src/sql/parts/modelComponents/card.component.ts @@ -70,8 +70,9 @@ export default class CardComponent extends ComponentWithIconBase implements ICom } public getClass(): string { - return (this.selectable && this.selected || this._hasFocus) ? 'model-card selected' : - 'model-card unselected'; + let cardClass = this.isListItemCard ? 'model-card-list-item' : 'model-card'; + return (this.selectable && this.selected || this._hasFocus) ? `${cardClass} selected` : + `${cardClass} unselected`; } public onCardHoverChanged(event: any) { @@ -93,11 +94,16 @@ export default class CardComponent extends ComponentWithIconBase implements ICom } public get iconClass(): string { - return this._iconClass + ' icon' + ' cardIcon'; + if (this.isListItemCard) { + return this._iconClass + ' icon' + ' list-item-icon'; + } + else { + return this._iconClass + ' icon' + ' cardIcon'; + } } private get selectable(): boolean { - return this.cardType === 'VerticalButton'; + return this.cardType === 'VerticalButton' || this.cardType === 'ListItem'; } // CSS-bound properties @@ -126,11 +132,15 @@ export default class CardComponent extends ComponentWithIconBase implements ICom return !this.cardType || this.cardType === 'Details'; } + public get isListItemCard(): boolean { + return !this.cardType || this.cardType === 'ListItem'; + } + public get isVerticalButton(): boolean { return this.cardType === 'VerticalButton'; } - public get showRadioButton():boolean{ + public get showRadioButton(): boolean { return this.selectable && (this.selected || this._hasFocus); } @@ -138,6 +148,9 @@ export default class CardComponent extends ComponentWithIconBase implements ICom return this.selectable && this.selected; } + public get descriptions(): string[] { + return this.getPropertyOrDefault((props) => props.descriptions, []); + } public get actions(): ActionDescriptor[] { return this.getPropertyOrDefault((props) => props.actions, []); diff --git a/src/sql/parts/modelComponents/card.css b/src/sql/parts/modelComponents/card.css index 52d1f1a179..c69b9e705b 100644 --- a/src/sql/parts/modelComponents/card.css +++ b/src/sql/parts/modelComponents/card.css @@ -10,11 +10,13 @@ vertical-align: top; } +.model-card-list-item.selected, .model-card.selected { border-color: rgb(0, 120, 215); box-shadow: rgba(0, 120, 215, 0.75) 0px 0px 6px; } +.model-card-list-item.unselected, .model-card.unselected { border-color: rgb(214, 214, 214); box-shadow: none; @@ -96,6 +98,7 @@ text-align: center; } +.model-card-list-item .selection-indicator-container, .model-card .selection-indicator-container { position: absolute; top: 5px; @@ -110,6 +113,30 @@ border-style: solid; } +.model-card-list-item .selection-indicator-container, +.model-card .selection-indicator-container { + position: absolute; + overflow: hidden; + width: 16px; + height: 16px; + border-radius: 50%; + background-color: white; + border-width: 1px; + border-color: rgb(0, 120, 215); + border-style: solid; +} + +.model-card-list-item .selection-indicator-container { + top: 10px; + right: 10px; +} + +.model-card .selection-indicator-container { + top: 5px; + right: 5px; +} + +.model-card-list-item .selection-indicator, .model-card .selection-indicator { margin: 4px; width: 8px; @@ -135,4 +162,34 @@ .model-table a { cursor: pointer; text-decoration: underline +} + +.model-card-list-item { + display: inline-block; + height: 100%; + width: 100%; + margin: 5px 0px 5px 0px; + border-width: 1px; + border-style: solid; + text-align: left; + vertical-align: top; +} + +.model-card-list-item .list-item-content { + height: auto; + padding: 5px 26px 5px 5px; + min-height: 30px; + min-width: 300px; +} + +.model-card-list-item .list-item-icon { + background-position: 2px 2px; + padding-left:22px; + font-size: 15px; + background-repeat: no-repeat; + background-size: 16px 16px; +} + +.model-card-list-item .list-item-description { + padding-left:22px; } \ No newline at end of file diff --git a/src/sql/parts/modelComponents/components.contribution.ts b/src/sql/parts/modelComponents/components.contribution.ts index 2c537c2b5e..bdc298cab3 100644 --- a/src/sql/parts/modelComponents/components.contribution.ts +++ b/src/sql/parts/modelComponents/components.contribution.ts @@ -25,6 +25,7 @@ import EditorComponent from './editor.component'; import DomComponent from './dom.component'; import { registerComponentType } from 'sql/platform/dashboard/common/modelComponentRegistry'; import { ModelComponentTypes } from 'sql/workbench/api/common/sqlExtHostTypes'; +import HyperlinkComponent from 'sql/parts/modelComponents/hyperlink.component'; export const DIV_CONTAINER = 'div-container'; registerComponentType(DIV_CONTAINER, ModelComponentTypes.DivContainer, DivContainer); @@ -89,3 +90,6 @@ registerComponentType(EDITOR_COMPONENT, ModelComponentTypes.Editor, EditorCompon export const DOM_COMPONENT = 'dom-component'; registerComponentType(DOM_COMPONENT, ModelComponentTypes.Dom, DomComponent); + +export const HYPERLINK_COMPONENT = 'hyperlink-component'; +registerComponentType(HYPERLINK_COMPONENT, ModelComponentTypes.Hyperlink, HyperlinkComponent); \ No newline at end of file diff --git a/src/sql/parts/modelComponents/groupContainer.component.ts b/src/sql/parts/modelComponents/groupContainer.component.ts index 9fb30179b3..8bdaf2d3e3 100644 --- a/src/sql/parts/modelComponents/groupContainer.component.ts +++ b/src/sql/parts/modelComponents/groupContainer.component.ts @@ -20,10 +20,10 @@ import { CommonServiceInterface } from 'sql/services/common/commonServiceInterfa @Component({ selector: 'modelview-groupContainer', template: ` - + {{_containerLayout.header}} - + @@ -40,6 +40,7 @@ export default class GroupContainer extends ContainerBase implement @Input() modelStore: IModelStore; private _containerLayout: GroupLayout; + private _collapsed: boolean; @ViewChild('container', { read: ElementRef }) private _container: ElementRef; @@ -48,6 +49,7 @@ export default class GroupContainer extends ContainerBase implement @Inject(forwardRef(() => ChangeDetectorRef)) changeRef: ChangeDetectorRef, @Inject(forwardRef(() => ElementRef)) el: ElementRef) { super(changeRef, el); + this._collapsed = false; } ngOnInit(): void { @@ -65,6 +67,7 @@ export default class GroupContainer extends ContainerBase implement public setLayout(layout: GroupLayout): void { this._containerLayout = layout; + this._collapsed = !!layout.collapsed; this.layout(); } @@ -72,6 +75,10 @@ export default class GroupContainer extends ContainerBase implement return this._containerLayout && this._containerLayout && this._containerLayout.header !== undefined; } + private isCollapsible(): boolean { + return this.hasHeader() && this._containerLayout.collapsible === true; + } + private getContainerWidth(): string { if (this._containerLayout && this._containerLayout.width) { let width: string = this._containerLayout.width.toString(); @@ -83,4 +90,24 @@ export default class GroupContainer extends ContainerBase implement return '100%'; } } + + private getContainerDisplayStyle(): string { + return !this.isCollapsible() || !this._collapsed ? 'block' : 'none'; + } + + private getHeaderClass(): string { + if (this.isCollapsible()) { + let modifier = this._collapsed ? 'collapsed' : 'expanded'; + return `modelview-group-header-collapsible ${modifier}`; + } else { + return 'modelview-group-header'; + } + } + + private changeState(): void { + if (this.isCollapsible()) { + this._collapsed = !this._collapsed; + this._changeRef.detectChanges(); + } + } } diff --git a/src/sql/parts/modelComponents/groupLayout.css b/src/sql/parts/modelComponents/groupLayout.css index bd9374365c..7ee9dedbf4 100644 --- a/src/sql/parts/modelComponents/groupLayout.css +++ b/src/sql/parts/modelComponents/groupLayout.css @@ -7,14 +7,40 @@ .modelview-group-row { display: table-row; - } +.modelview-group-header-collapsible, .modelview-group-header { padding-bottom: 5px; font-size: 14px; } +.modelview-group-header-collapsible { + padding-left: 20px; + background-position: 2px 2px; + background-size: 16px 16px; + background-repeat: no-repeat; + cursor: pointer; +} + +.vs .modelview-group-header-collapsible.expanded { + background-image: url("../../media/icons/expanded.svg"); +} + +.vs-dark .modelview-group-header-collapsible.expanded, +.hc-black .modelview-group-header-collapsible.expanded { + background-image: url("../../media/icons/expanded_inverse.svg"); +} + +.vs .modelview-group-header-collapsible.collapsed { + background-image: url("../../media/icons/collapsed.svg"); +} + +.vs-dark .modelview-group-header-collapsible.collapsed, +.hc-black .modelview-group-header-collapsible.collapsed { + background-image: url("../../media/icons/collapsed_inverse.svg"); +} + .modelview-group-cell { padding-bottom: 5px; display: table-cell; diff --git a/src/sql/parts/modelComponents/hyperlink.component.ts b/src/sql/parts/modelComponents/hyperlink.component.ts new file mode 100644 index 0000000000..1a58a06118 --- /dev/null +++ b/src/sql/parts/modelComponents/hyperlink.component.ts @@ -0,0 +1,70 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { + Component, Input, Inject, ChangeDetectorRef, forwardRef, + OnDestroy, AfterViewInit, ElementRef +} from '@angular/core'; + +import * as sqlops from 'sqlops'; + +import { ComponentBase } from 'sql/parts/modelComponents/componentBase'; +import { IComponent, IComponentDescriptor, IModelStore } from 'sql/parts/modelComponents/interfaces'; +import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service'; + +@Component({ + selector: 'modelview-hyperlink', + template: `{{getLabel()}}` +}) +export default class HyperlinkComponent extends ComponentBase implements IComponent, OnDestroy, AfterViewInit { + @Input() descriptor: IComponentDescriptor; + @Input() modelStore: IModelStore; + + constructor( + @Inject(forwardRef(() => CommonServiceInterface)) private _commonService: CommonServiceInterface, + @Inject(forwardRef(() => ChangeDetectorRef)) changeRef: ChangeDetectorRef, + @Inject(forwardRef(() => ElementRef)) el: ElementRef) { + super(changeRef, el); + } + + ngOnInit(): void { + this.baseInit(); + } + + ngAfterViewInit(): void { + } + + ngOnDestroy(): void { + this.baseDestroy(); + } + + public setLayout(layout: any): void { + this.layout(); + } + + public set label(newValue: string) { + this.setPropertyFromUI((properties, value) => { properties.label = value; }, newValue); + } + + public get label(): string { + return this.getPropertyOrDefault((props) => props.label, ''); + } + + public getLabel(): string { + return this.label; + } + + public set url(newValue: string) { + this.setPropertyFromUI((properties, value) => { properties.url = value; }, newValue); + } + + public get url(): string { + return this.getPropertyOrDefault((props) => props.url, ''); + } + + public getUrl(): string { + return this.url; + } +} diff --git a/src/sql/parts/modelComponents/text.component.ts b/src/sql/parts/modelComponents/text.component.ts index 987fa6e110..34b4d0e557 100644 --- a/src/sql/parts/modelComponents/text.component.ts +++ b/src/sql/parts/modelComponents/text.component.ts @@ -18,7 +18,7 @@ import { CommonServiceInterface } from 'sql/services/common/commonServiceInterfa @Component({ selector: 'modelview-text', template: ` - {{getValue()}}` + {{getValue()}}` }) export default class TextComponent extends ComponentBase implements IComponent, OnDestroy, AfterViewInit { @Input() descriptor: IComponentDescriptor; diff --git a/src/sql/sqlops.proposed.d.ts b/src/sql/sqlops.proposed.d.ts index ae980991e3..f789fbdbe4 100644 --- a/src/sql/sqlops.proposed.d.ts +++ b/src/sql/sqlops.proposed.d.ts @@ -40,6 +40,7 @@ declare module 'sqlops' { toolbarContainer(): ToolbarBuilder; loadingComponent(): LoadingComponentBuilder; fileBrowserTree(): ComponentBuilder; + hyperlink(): ComponentBuilder; } export interface TreeComponentDataProvider extends vscode.TreeDataProvider { @@ -348,6 +349,8 @@ declare module 'sqlops' { export interface GroupLayout { width?: number | string; header?: string; + collapsible?: boolean; + collapsed?: boolean; } export interface GroupItemLayout { @@ -433,7 +436,8 @@ declare module 'sqlops' { export enum CardType { VerticalButton = 'VerticalButton', - Details = 'Details' + Details = 'Details', + ListItem = 'ListItem' } /** @@ -444,6 +448,7 @@ declare module 'sqlops' { label: string; value?: string; actions?: ActionDescriptor[]; + descriptions?: string[]; status?: StatusIndicator; /** @@ -540,6 +545,11 @@ declare module 'sqlops' { value?: string; } + export interface HyperlinkComponentProperties extends ComponentProperties { + label: string; + url: string; + } + export interface DropDownProperties extends ComponentProperties { value?: string | CategoryValue; values?: string[] | CategoryValue[]; @@ -638,10 +648,15 @@ declare module 'sqlops' { } - export interface TextComponent extends Component { + export interface TextComponent extends Component, ComponentProperties { value: string; } + export interface HyperlinkComponent extends Component, HyperlinkComponentProperties { + label: string; + url: string; + } + export interface InputBoxComponent extends Component, InputBoxProperties { onTextChanged: vscode.Event; } diff --git a/src/sql/workbench/api/common/sqlExtHostTypes.ts b/src/sql/workbench/api/common/sqlExtHostTypes.ts index c7597cbba4..374418c0e2 100644 --- a/src/sql/workbench/api/common/sqlExtHostTypes.ts +++ b/src/sql/workbench/api/common/sqlExtHostTypes.ts @@ -164,7 +164,8 @@ export enum ModelComponentTypes { TreeComponent, FileBrowserTree, Editor, - Dom + Dom, + Hyperlink } export interface IComponentShape { @@ -262,6 +263,7 @@ export interface CardProperties { label: string; value?: string; actions?: ActionDescriptor[]; + descriptions?: string[]; status?: StatusIndicator; selected?: boolean; cardType: CardType; @@ -300,7 +302,8 @@ export enum DeclarativeDataType { export enum CardType { VerticalButton = 'VerticalButton', - Details = 'Details' + Details = 'Details', + ListItem = 'ListItem' } export enum Orientation { @@ -492,7 +495,7 @@ export class CellRange { } constructor(start: number, end: number) { - if (typeof(start) !== 'number' || typeof(start) !== 'number' || start < 0 || end < 0) { + if (typeof (start) !== 'number' || typeof (start) !== 'number' || start < 0 || end < 0) { throw new Error('Invalid arguments'); } diff --git a/src/sql/workbench/api/node/extHostModelView.ts b/src/sql/workbench/api/node/extHostModelView.ts index 314110c367..8c7711f6f9 100644 --- a/src/sql/workbench/api/node/extHostModelView.ts +++ b/src/sql/workbench/api/node/extHostModelView.ts @@ -198,6 +198,13 @@ class ModelBuilderImpl implements sqlops.ModelBuilder { return builder; } + hyperlink(): sqlops.ComponentBuilder { + let id = this.getNextComponentId(); + let builder: ComponentBuilderImpl = this.getComponentBuilder(new HyperlinkComponentWrapper(this._proxy, this._handle, id), id); + this._componentBuilders.set(id, builder); + return builder; + } + getComponentBuilder(component: ComponentWrapper, id: string): ComponentBuilderImpl { let componentBuilder: ComponentBuilderImpl = new ComponentBuilderImpl(component); this._componentBuilders.set(id, componentBuilder); @@ -1248,6 +1255,28 @@ class TreeComponentWrapper extends ComponentWrapper implements sqlops.TreeCom } } +class HyperlinkComponentWrapper extends ComponentWrapper implements sqlops.HyperlinkComponentProperties { + + constructor(proxy: MainThreadModelViewShape, handle: number, id: string) { + super(proxy, handle, ModelComponentTypes.Hyperlink, id); + this.properties = {}; + } + + public get label(): string { + return this.properties['label']; + } + public set label(v: string) { + this.setProperty('label', v); + } + + public get url(): string { + return this.properties['url']; + } + public set url(v: string) { + this.setProperty('url', v); + } +} + class ModelViewImpl implements sqlops.ModelView { public onClosedEmitter = new Emitter();
{{value}}
{{getValue()}}