Feat/model backed ui (#1145)

This is an initial PR for a new model-driven UI where extensions can provide definitions of the components & how they're laid out using Containers.
#1140, #1141, #1142, #1143 and #1144 are all tracking additional work needed to improve the initial implementation and fix some issues with the implementation.

Features:
- Supports defining a FlexContainer that maps to a flexbox-based layout.
- Supports creating a card component, which is a key-value pair based control that will lay out simple information to a user. Eventually this will have an optional set of actions associated with it.
- Has a sample project which shows how to use the API and was used for verification
This commit is contained in:
Kevin Cunnane
2018-04-13 15:59:18 -07:00
committed by GitHub
parent e022f4a0d1
commit b2c70e9301
63 changed files with 13238 additions and 84 deletions

3
samples/sqlservices/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
node_modules
*.vsix
typings/sqlops.proposed.d.ts

57
samples/sqlservices/.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,57 @@
// A launch configuration that launches the extension inside a new window
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
// To debug the extension:
// 1. please install the "SQL Operations Studio Debug" extension into VSCode
// 2. Ensure sqlops is added to your path:
// - open SQL Operations Studio
// - run the command "Install 'sqlops' command in PATH"
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug in SqlOps install",
"type": "sqlopsExtensionHost",
"request": "launch",
"runtimeExecutable": "sqlops",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}"
]
},
{
"type": "node",
"request": "attach",
"name": "Attach to Ops Studio",
"protocol": "inspector",
"port": 5870,
"restart": true,
"sourceMaps": true,
"outFiles": [
"${workspaceRoot}/out/**/*.js"
],
"preLaunchTask": "",
"timeout": 25000
},
{
"name": "Debug in enlistment",
"type": "sqlopsExtensionHost",
"request": "launch",
"windows": {
"runtimeExecutable": "${workspaceFolder}/../../scripts/sql.bat"
},
"osx": {
"runtimeExecutable": "${workspaceFolder}/../../scripts/sql.sh"
},
"linux": {
"runtimeExecutable": "${workspaceFolder}/../../scripts/sql.sh"
},
"args": [
"--extensionDevelopmentPath=${workspaceFolder}"
],
"timeout": 20000
}
]
}

14
samples/sqlservices/.vscode/tasks.json vendored Normal file
View File

@@ -0,0 +1,14 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"type": "gulp",
"task": "build",
"problemMatcher": [
"$gulp-tsc"
]
}
]
}

View File

@@ -0,0 +1,4 @@
.vscode/**
.vscode-test/**
.gitignore
vsc-extension-quickstart.md

View File

@@ -0,0 +1 @@
This is a sample extension that will show some basic model-backed UI scenarios. The long-term goal is to use SQL Service querying (e.g. see if Agent and other services are running) and visualize in interesting ways. Additional suggestions for improving this sample are welcome.

View File

@@ -0,0 +1,14 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
"use strict";
// NOTE: These are es6 gulpfiles
// Basic build tasks
require('./tasks/buildtasks');
// VSIX generation tasks
require('./tasks/packagetasks');

7118
samples/sqlservices/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,81 @@
{
"name": "sqlservices",
"displayName": "sqlservices",
"description": "Lists SQL Server service status in the management dashboard for a server",
"version": "0.0.1",
"publisher": "demo",
"engines": {
"vscode": "^1.21.0",
"sqlops": "*"
},
"categories": [
"Other"
],
"activationEvents": [
"*"
],
"main": "./out/src/extension",
"contributes": {
"dashboard.tabs": [
{
"id": "sqlservices.tab",
"title": "sqlservices",
"icon": {
"light": "./out/src/media/insights.svg",
"dark": "./out/src/media/insights_inverse.svg"
},
"description": "Shows available services running in the SQL Server instance",
"container": {
"nav-section": [
{
"id": "sqlservices",
"title": "Services",
"gridItemConfig": {
"sizex": 2,
"sizey": 1
},
"container": {
"modelview-container": null
}
},
{
"id": "splitPanel",
"title": "SplitPanel",
"gridItemConfig": {
"sizex": 2,
"sizey": 1
},
"container": {
"modelview-container": null
}
}
]
}
}
]
},
"scripts": {
"build": "gulp build",
"compile": "gulp compile",
"watch": "gulp watch",
"typings": "gulp copytypings",
"postinstall": "node ./node_modules/vscode/bin/install && node ./node_modules/sqlops/bin/install"
},
"dependencies": {
"vscode-nls": "^3.2.2"
},
"devDependencies": {
"@types/node": "^7.0.43",
"child-process-promise": "^2.2.1",
"del": "^3.0.0",
"gulp": "^4.0.0",
"gulp-color": "0.0.1",
"gulp-sourcemaps": "^2.6.4",
"gulp-tslint": "^6.0.2",
"gulp-typescript": "^3.2.4",
"sqlops": "github:anthonydresser/sqlops-extension-sqlops",
"tslint": "^3.14.0",
"typescript": "^2.6.1",
"vscode": "^1.1.14"
}
}

View File

@@ -0,0 +1,9 @@
/*
* This file should contain SQL code that returns a result set
* To get started, build your own queries in SQL Operations Studio and click the
* "View as Chart" button to get the correct chart format. Then choose "Create Insight"
* and update the package.json with the JSON contents, and this file with the query
* used to generate the chart.
*/
select 'My Label' as [Label], 1 as [Value]

View File

@@ -0,0 +1,10 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
// CONFIG VALUES ///////////////////////////////////////////////////////////
export const extensionConfigSectionName = 'sqlservices';
export const configLogDebugInfo = 'logDebugInfo';

View File

@@ -0,0 +1,104 @@
/*---------------------------------------------------------------------------------------------
* 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 Utils from '../utils';
import * as vscode from 'vscode';
import SplitPropertiesPanel from './splitPropertiesPanel';
/**
* The main controller class that initializes the extension
*/
export default class MainController implements vscode.Disposable {
constructor(protected context: vscode.ExtensionContext) {
}
// PUBLIC METHODS //////////////////////////////////////////////////////
public dispose(): void {
this.deactivate();
}
/**
* Deactivates the extension
*/
public deactivate(): void {
Utils.logDebug('Main controller deactivated');
}
public activate(): Promise<boolean> {
this.registerSqlServicesModelView();
this.registerSplitPanelModelView();
sqlops.tasks.registerTask('sqlservices.clickTask', (profile) => {
vscode.window.showInformationMessage(`Clicked from profile ${profile.serverName}.${profile.databaseName}`);
});
return Promise.resolve(true);
}
private registerSqlServicesModelView(): void {
sqlops.dashboard.registerModelViewProvider('sqlservices', async (view) => {
let flexModel = view.modelBuilder.flexContainer()
.withLayout({
flexFlow: 'row'
}).withItems([
// 1st child panel with N cards
view.modelBuilder.flexContainer()
.withLayout({ flexFlow: 'column' })
.withItems([
view.modelBuilder.card()
.withProperties<sqlops.CardProperties>({
label: 'label1',
value: 'value1',
actions: [{ label: 'action', taskId: 'sqlservices.clickTask' }]
})
.component()
]).component(),
// 2nd child panel with N cards
view.modelBuilder.flexContainer()
.withLayout({ flexFlow: 'column' })
.withItems([
view.modelBuilder.card()
.withProperties<sqlops.CardProperties>({
label: 'label2',
value: 'value2',
actions: [{ label: 'action', taskId: 'sqlservices.clickTask' }]
})
.component()
]).component()
], { flex: '0 1 50%' })
.component();
await view.initializeModel(flexModel);
});
}
private registerSplitPanelModelView(): void {
sqlops.dashboard.registerModelViewProvider('splitPanel', async (view) => {
let numPanels = 3;
let splitPanel = new SplitPropertiesPanel(view, numPanels);
await view.initializeModel(splitPanel.modelBase);
// Add a bunch of cards after an initial timeout
setTimeout(async () => {
for (let i = 0; i < 10; i++) {
let panel = i % numPanels;
let card = view.modelBuilder.card().component();
card.label = `label${i.toString()}`;
splitPanel.addItem(card, panel);
}
}, 0);
});
}
}

View File

@@ -0,0 +1,43 @@
/*---------------------------------------------------------------------------------------------
* 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';
/**
* The main controller class that initializes the extension
*/
export default class SplitPropertiesPanel {
private panels: sqlops.FlexContainer[];
private _modelBase: sqlops.FlexContainer;
constructor(view: sqlops.ModelView, numPanels: number) {
this.panels = [];
let ratio = Math.round(100 / numPanels);
for (let i = 0; i < numPanels; i++) {
this.panels.push(view.modelBuilder.flexContainer()
.withLayout({ flexFlow: 'column' }).component());
}
this._modelBase = view.modelBuilder.flexContainer()
.withLayout({
flexFlow: 'row'
}).withItems(this.panels, {
flex: `0 1 ${ratio}%`
})
.component();
}
public get modelBase(): sqlops.Component {
return this._modelBase;
}
public addItem(item: sqlops.Component, panel: number): void {
if (panel >= this.panels.length) {
throw new Error(`Cannot add to panel ${panel} as only ${this.panels.length - 1} panels defined`);
}
this.panels[panel].addItem(item, undefined);
}
}

View File

@@ -0,0 +1,37 @@
/*---------------------------------------------------------------------------------------------
* 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 MainController from './controllers/mainController';
let mainController: MainController;
export function activate(context: vscode.ExtensionContext): Promise<boolean> {
let activations: Promise<boolean>[] = [];
// Start the main controller
mainController = new MainController(context);
context.subscriptions.push(mainController);
activations.push(mainController.activate());
return Promise.all(activations)
.then((results: boolean[]) => {
for (let result of results) {
if (!result) {
return false;
}
}
return true;
});
}
export function deactivate(): void {
if (mainController) {
mainController.deactivate();
}
}

View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{fill:#212121;}</style></defs><title>insights</title><path class="cls-1" d="M15,4V8H14V5.71L9.49,10.2l-2-2L2,13.71V14H15v1H1V1H2V12.29L7.49,6.8l2,2L13.28,5H11V4Z"/></svg>

After

Width:  |  Height:  |  Size: 282 B

View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{fill:#fff;}</style></defs><title>insights_inverse</title><path class="cls-1" d="M15,4V8H14V5.71L9.49,10.2l-2-2L2,13.71V14H15v1H1V1H2V12.29L7.49,6.8l2,2L13.28,5H11V4Z"/></svg>

After

Width:  |  Height:  |  Size: 287 B

View File

@@ -0,0 +1,52 @@
/*---------------------------------------------------------------------------------------------
* 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 fs from 'fs-extra';
import * as handlebars from 'handlebars';
import * as path from 'path';
import * as vscode from 'vscode';
import * as Constants from './constants';
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
/**
* Helper to log messages to the developer console if enabled
* @param msg Message to log to the console
*/
export function logDebug(msg: any): void {
let config = vscode.workspace.getConfiguration(Constants.extensionConfigSectionName);
let logDebugInfo = config[Constants.configLogDebugInfo];
if (logDebugInfo === true) {
let currentTime = new Date().toLocaleTimeString();
let outputMsg = '[' + currentTime + ']: ' + msg ? msg.toString() : '';
console.log(outputMsg);
}
}
export function renderTemplateHtml(extensionPath: string, templateName: string, templateValues: object): Thenable<string> {
let templatePath = path.join(extensionPath, 'resources', templateName);
// 1) Read the template from the disk
// 2) Compile it as a handlebars template and render the HTML
// 3) On failure, return a simple string as an error
return fs.readFile(templatePath, 'utf-8')
.then(templateText => {
let template = handlebars.compile(templateText);
return template(templateValues);
})
.then(
undefined,
error => {
logDebug(error);
return localize('errorLoadingTab', 'An error occurred while loading the tab');
}
);
}

View File

@@ -0,0 +1,124 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
"use strict";
let del = require('del');
let gulp = require('gulp');
let srcmap = require('gulp-sourcemaps');
let tslint = require('gulp-tslint');
let ts = require('gulp-typescript');
let cproc = require('child_process');
let os = require('os');
let config = require('./config');
let tsProject = ts.createProject('tsconfig.json');
// GULP TASKS //////////////////////////////////////////////////////////////
gulp.task('clean', function(done) {
return del('out', done);
});
gulp.task('lint', () => {
return gulp.src([
config.paths.project.root + '/src/**/*.ts',
config.paths.project.root + '/test/**/*.ts'
])
.pipe((tslint({
formatter: "verbose"
})))
.pipe(tslint.report());
});
gulp.task('compile:src', function(done) {
gulp.src([
config.paths.project.root + '/src/**/*.sql',
config.paths.project.root + '/src/**/*.svg',
config.paths.project.root + '/src/**/*.html'
]).pipe(gulp.dest('out/src/'));
let srcFiles = [
config.paths.project.root + '/src/**/*.ts',
config.paths.project.root + '/src/**/*.js',
config.paths.project.root + '/typings/**/*.ts'
];
return gulp.src(srcFiles)
.pipe(srcmap.init())
.pipe(tsProject())
.on('error', function() {
if (process.env.BUILDMACHINE) {
done('Extension Tests failed to build. See Above.');
process.exit(1);
}
})
.pipe(srcmap.write('.', {
sourceRoot: function(file) {
return file.cwd + '/src';
}
}))
.pipe(gulp.dest('out/src/'));
});
gulp.task('compile:test', function(done) {
let srcFiles = [
config.paths.project.root + '/test/**/*.ts',
config.paths.project.root + '/typings/**/*.ts'
];
return gulp.src(srcFiles)
.pipe(srcmap.init())
.pipe(tsProject())
.on('error', function() {
if(process.env.BUILDMACHINE) {
done('Failed to compile test source, see above.');
process.exit(1);
}
})
.pipe(srcmap.write('.', {sourceRoot: function(file) { return file.cwd + '/test'; }}))
.pipe(gulp.dest('out/test/'));
});
// COMPOSED GULP TASKS /////////////////////////////////////////////////////
gulp.task("compile", gulp.series("compile:src", "compile:test"));
gulp.task("build", gulp.series("clean", "lint", "compile"));
gulp.task("watch", function() {
gulp.watch([config.paths.project.root + '/src/**/*',
config.paths.project.root + '/test/**/*.ts'],
gulp.series('build'));
});
gulp.task('test', (done) => {
let workspace = process.env['WORKSPACE'];
if (!workspace) {
workspace = process.cwd();
}
process.env.JUNIT_REPORT_PATH = workspace + '/test-reports/ext_xunit.xml';
let sqlopsPath = 'sqlops';
if (process.env['SQLOPS_DEV']) {
let suffix = os.platform === 'win32' ? 'bat' : 'sh';
sqlopsPath = `${process.env['SQLOPS_DEV']}/scripts/sql-cli.${suffix}`;
}
console.log(`Using SQLOPS Path of ${sqlopsPath}`);
cproc.exec(`${sqlopsPath} --extensionDevelopmentPath="${workspace}" --extensionTestsPath="${workspace}/out/test" --verbose`, (error, stdout, stderr) => {
if (error) {
console.error(`exec error: ${error}`);
process.exit(1);
}
console.log(`stdout: ${stdout}`);
console.log(`stderr: ${stderr}`);
done();
});
});
gulp.task('copytypings', function() {
return gulp.src(config.paths.project.root + '/../../src/sql/sqlops.proposed.d.ts')
.pipe(gulp.dest('typings/'));
});

View File

@@ -0,0 +1,24 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
var path = require('path');
var projectRoot = path.resolve(path.dirname(__dirname));
var srcRoot = path.resolve(projectRoot, 'src');
var localization = path.resolve(projectRoot, 'localization');
var config = {
paths: {
project: {
root: projectRoot,
localization: localization
},
extension: {
root: srcRoot
}
}
};
module.exports = config;

View File

@@ -0,0 +1,36 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
let gulp = require('gulp');
let exec = require('child-process-promise').exec;
let color = require('gulp-color');
// CONSTANTS ///////////////////////////////////////////////////////////////
let packageVals = require('../package');
// HELPER FUNCTIONS ////////////////////////////////////////////////////////
let buildPackage = function(packageName) {
// Make sure there are
if (!packageVals.repository) {
return Promise.reject("Repository field is not defined in package.json");
}
// Initialize the package command with program and command
let vsceArgs = [];
vsceArgs.push('./node_modules/vsce/out/vsce');
vsceArgs.push('package'); // package command
// Add the package name
vsceArgs.push('-o');
vsceArgs.push(packageName);
// Put it all together and execute the command
let command = vsceArgs.join(' ');
console.log(command);
return exec(command);
};

View File

@@ -0,0 +1,16 @@
{
"compileOnSave": true,
"compilerOptions": {
"module": "commonjs",
"target": "ES6",
"lib": [ "es6" ],
"sourceMap": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"rootDir": "."
},
"exclude": [
"node_modules",
".vscode-test"
]
}

View File

@@ -0,0 +1,123 @@
{
"rules": {
"align": [
true,
"parameters",
"statements"
],
"ban": false,
"class-name": true,
"comment-format": [
true,
"check-space"
],
"curly": true,
"eofline": true,
"forin": true,
"indent": [
true,
"tabs"
],
"interface-name": true,
"jsdoc-format": true,
"label-position": true,
"label-undefined": true,
"max-line-length": [
true,
160
],
"member-access": false,
"member-ordering": false,
"no-any": false,
"no-arg": true,
"no-bitwise": true,
"no-conditional-assignment": true,
"no-consecutive-blank-lines": false,
"no-console": [
true,
"debug",
"info",
"time",
"timeEnd",
"trace"
],
"no-construct": true,
"no-constructor-vars": false,
"no-debugger": true,
"no-duplicate-key": true,
"no-duplicate-variable": true,
"no-empty": true,
"no-eval": true,
"no-inferrable-types": false,
"no-internal-module": true,
"no-null-keyword": true,
"no-require-imports": false,
"no-shadowed-variable": true,
"no-string-literal": false,
"no-switch-case-fall-through": false,
"no-trailing-whitespace": true,
"no-unreachable": true,
"no-unused-expression": false,
"no-unused-variable": true,
"no-use-before-declare": true,
"no-var-keyword": true,
"no-var-requires": false,
"object-literal-sort-keys": false,
"one-line": [
true,
"check-open-brace",
"check-catch",
"check-else",
"check-finally",
"check-whitespace"
],
"quotemark": [
true,
"single",
"avoid-escape"
],
"radix": true,
"semicolon": true,
"switch-default": true,
"trailing-comma": [
true,
{
"multiline": "never",
"singleline": "never"
}
],
"triple-equals": [
true,
"allow-null-check"
],
"typedef": [
true,
"call-signature",
"property-declaration"
],
"typedef-whitespace": [
true,
{
"call-signature": "nospace",
"index-signature": "nospace",
"parameter": "nospace",
"property-declaration": "nospace",
"variable-declaration": "nospace"
}
],
"use-strict": false,
"variable-name": [
true,
"allow-leading-underscore",
"ban-keywords"
],
"whitespace": [
true,
"check-branch",
"check-decl",
"check-operator",
"check-separator",
"check-type"
]
}
}

File diff suppressed because it is too large Load Diff