From b3a16fd0ced5571608c05eb5b5ec3b1ad493f56b Mon Sep 17 00:00:00 2001 From: Alan Ren Date: Tue, 12 Feb 2019 22:13:30 -0800 Subject: [PATCH] Feature/bdc create (#4012) * initial checkin * rename * wizard pages * target cluster radio button group * resource strings * existing cluster picker * revert changes to unwanted file * revert unwanted changes-2 * update cluster icon * settings page * fix group container * hyperlink component * address review comments * comments part 2 --- build/gulpfile.vscode.js | 43 ++--- extensions/big-data-cluster/README.md | 17 ++ .../big-data-cluster/images/cluster.svg | 1 + .../images/cluster_inverse.svg | 1 + .../big-data-cluster/images/sqlserver.png | Bin 0 -> 37585 bytes extensions/big-data-cluster/package.json | 41 ++++ .../src/data/kubeConfigParser.ts | 35 ++++ extensions/big-data-cluster/src/interfaces.ts | 17 ++ extensions/big-data-cluster/src/main.ts | 21 +++ .../big-data-cluster/src/mainController.ts | 34 ++++ .../big-data-cluster/src/typings/ref.d.ts | 9 + .../create-cluster/createClusterModel.ts | 26 +++ .../create-cluster/createClusterWizard.ts | 45 +++++ .../pages/clusterProfilePage.ts | 27 +++ .../create-cluster/pages/settingsPage.ts | 76 ++++++++ .../create-cluster/pages/summaryPage.ts | 25 +++ .../create-cluster/pages/targetClusterPage.ts | 176 ++++++++++++++++++ .../src/wizards/wizardBase.ts | 24 +++ .../src/wizards/wizardPageBase.ts | 30 +++ extensions/big-data-cluster/tsconfig.json | 19 ++ extensions/big-data-cluster/yarn.lock | 109 +++++++++++ src/sql/media/icons/collapsed.svg | 1 + src/sql/media/icons/collapsed_inverse.svg | 1 + src/sql/media/icons/expanded.svg | 1 + src/sql/media/icons/expanded_inverse.svg | 1 + .../parts/modelComponents/card.component.html | 72 ++++--- .../parts/modelComponents/card.component.ts | 23 ++- src/sql/parts/modelComponents/card.css | 57 ++++++ .../components.contribution.ts | 4 + .../groupContainer.component.ts | 31 ++- src/sql/parts/modelComponents/groupLayout.css | 28 ++- .../modelComponents/hyperlink.component.ts | 70 +++++++ .../parts/modelComponents/text.component.ts | 2 +- src/sql/sqlops.proposed.d.ts | 19 +- .../workbench/api/common/sqlExtHostTypes.ts | 9 +- .../workbench/api/node/extHostModelView.ts | 29 +++ 36 files changed, 1061 insertions(+), 63 deletions(-) create mode 100644 extensions/big-data-cluster/README.md create mode 100644 extensions/big-data-cluster/images/cluster.svg create mode 100644 extensions/big-data-cluster/images/cluster_inverse.svg create mode 100644 extensions/big-data-cluster/images/sqlserver.png create mode 100644 extensions/big-data-cluster/package.json create mode 100644 extensions/big-data-cluster/src/data/kubeConfigParser.ts create mode 100644 extensions/big-data-cluster/src/interfaces.ts create mode 100644 extensions/big-data-cluster/src/main.ts create mode 100644 extensions/big-data-cluster/src/mainController.ts create mode 100644 extensions/big-data-cluster/src/typings/ref.d.ts create mode 100644 extensions/big-data-cluster/src/wizards/create-cluster/createClusterModel.ts create mode 100644 extensions/big-data-cluster/src/wizards/create-cluster/createClusterWizard.ts create mode 100644 extensions/big-data-cluster/src/wizards/create-cluster/pages/clusterProfilePage.ts create mode 100644 extensions/big-data-cluster/src/wizards/create-cluster/pages/settingsPage.ts create mode 100644 extensions/big-data-cluster/src/wizards/create-cluster/pages/summaryPage.ts create mode 100644 extensions/big-data-cluster/src/wizards/create-cluster/pages/targetClusterPage.ts create mode 100644 extensions/big-data-cluster/src/wizards/wizardBase.ts create mode 100644 extensions/big-data-cluster/src/wizards/wizardPageBase.ts create mode 100644 extensions/big-data-cluster/tsconfig.json create mode 100644 extensions/big-data-cluster/yarn.lock create mode 100644 src/sql/media/icons/collapsed.svg create mode 100644 src/sql/media/icons/collapsed_inverse.svg create mode 100644 src/sql/media/icons/expanded.svg create mode 100644 src/sql/media/icons/expanded_inverse.svg create mode 100644 src/sql/parts/modelComponents/hyperlink.component.ts 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 0000000000000000000000000000000000000000..d884faa14a804f63aa2a9d365fc75c09c12eeeb5 GIT binary patch literal 37585 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 RTp9-alNMJHs}M2r`#&r#oSOgu literal 0 HcmV?d00001 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();