DISPATCH-1049 Add console tests
Project: http://git-wip-us.apache.org/repos/asf/qpid-dispatch/repo Commit: http://git-wip-us.apache.org/repos/asf/qpid-dispatch/commit/b5deb035 Tree: http://git-wip-us.apache.org/repos/asf/qpid-dispatch/tree/b5deb035 Diff: http://git-wip-us.apache.org/repos/asf/qpid-dispatch/diff/b5deb035 Branch: refs/heads/master Commit: b5deb03579a7dedd81a56f32baa3e5f4b5b57136 Parents: af99754 Author: Ernest Allen <eal...@redhat.com> Authored: Mon Jun 25 17:25:50 2018 -0400 Committer: Ernest Allen <eal...@redhat.com> Committed: Mon Jun 25 17:25:50 2018 -0400 ---------------------------------------------------------------------- .gitignore | 2 +- console/CMakeLists.txt | 207 +- console/stand-alone/.babelrc | 21 + console/stand-alone/gulpfile.js | 101 +- console/stand-alone/index.html | 11 +- console/stand-alone/main.js | 254 ++ console/stand-alone/modules/connection.js | 347 +++ console/stand-alone/modules/correlator.js | 50 + console/stand-alone/modules/management.js | 63 + console/stand-alone/modules/topology.js | 403 +++ console/stand-alone/modules/utilities.js | 115 + console/stand-alone/package-lock.json | 2519 ++++++++++++++---- console/stand-alone/package.json | 24 +- console/stand-alone/plugin/css/dispatch.css | 4 +- console/stand-alone/plugin/html/qdrList.html | 53 +- .../plugin/html/tmplChartConfig.html | 12 +- console/stand-alone/plugin/js/chord/data.js | 234 +- console/stand-alone/plugin/js/chord/filters.js | 52 +- .../plugin/js/chord/layout/layout.js | 6 +- console/stand-alone/plugin/js/chord/matrix.js | 3 +- console/stand-alone/plugin/js/chord/qdrChord.js | 24 +- .../plugin/js/chord/ribbon/ribbon.js | 4 +- console/stand-alone/plugin/js/dispatchPlugin.js | 264 -- .../stand-alone/plugin/js/dlgChartController.js | 31 +- console/stand-alone/plugin/js/navbar.js | 140 +- .../stand-alone/plugin/js/posintDirective.js | 75 + .../stand-alone/plugin/js/qdrChartService.js | 1607 ++++++----- console/stand-alone/plugin/js/qdrCharts.js | 33 +- console/stand-alone/plugin/js/qdrGlobals.js | 58 +- console/stand-alone/plugin/js/qdrList.js | 1732 ++++++------ console/stand-alone/plugin/js/qdrListChart.js | 146 - console/stand-alone/plugin/js/qdrOverview.js | 85 +- .../plugin/js/qdrOverviewChartsController.js | 18 +- .../plugin/js/qdrOverviewLogsController.js | 25 +- console/stand-alone/plugin/js/qdrSchema.js | 22 +- console/stand-alone/plugin/js/qdrService.js | 317 +-- console/stand-alone/plugin/js/qdrSettings.js | 89 +- .../plugin/js/qdrTopAddressesController.js | 16 +- console/stand-alone/plugin/js/topology/links.js | 216 ++ console/stand-alone/plugin/js/topology/nodes.js | 162 ++ .../plugin/js/topology/qdrTopology.js | 2512 +++++++---------- .../stand-alone/plugin/js/topology/topoUtils.js | 225 ++ .../stand-alone/plugin/js/topology/traffic.js | 445 ++++ .../stand-alone/plugin/js/topology/traffic.ts | 443 --- console/stand-alone/test/filter.js | 73 + console/stand-alone/test/links.js | 82 + console/stand-alone/test/matrix.js | 51 + console/stand-alone/test/nodes.json | 1 + console/stand-alone/test/utilities.js | 192 ++ console/stand-alone/tsconfig.json | 23 +- console/stand-alone/vendor-js.txt | 12 +- 51 files changed, 8155 insertions(+), 5449 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/b5deb035/.gitignore ---------------------------------------------------------------------- diff --git a/.gitignore b/.gitignore index 49a9c44..d7c32e7 100644 --- a/.gitignore +++ b/.gitignore @@ -12,7 +12,7 @@ tests/policy-1/policy-*.json .metadata .settings console/test/topolgies/config-* -.history +.history/ .tox .vscode console/stand-alone/node_modules/ http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/b5deb035/console/CMakeLists.txt ---------------------------------------------------------------------- diff --git a/console/CMakeLists.txt b/console/CMakeLists.txt index bfd9cd8..a55b572 100644 --- a/console/CMakeLists.txt +++ b/console/CMakeLists.txt @@ -20,101 +20,122 @@ ## ## Add cmake option to choose whether to install stand-alone console ## -option(CONSOLE_INSTALL "Build and install console (requires npm)" ON) +option(CONSOLE_INSTALL "Build and install console (requires npm 5.2+)" ON) if(CONSOLE_INSTALL) - find_program(NPX_EXE npx DOC "Location of the npx task runner") - if (NPX_EXE) - - set(CONSOLE_SOURCE_DIR "${CMAKE_SOURCE_DIR}/console/stand-alone") - set(CONSOLE_BUILD_DIR "${CMAKE_BINARY_DIR}/console") - - ## Files needed to create the ${CONSOLE_ARTIFACTS} - file (GLOB_RECURSE CONSOLE_JS_SOURCES ${CONSOLE_SOURCE_DIR}/plugin/js/*.js) - file (GLOB_RECURSE CONSOLE_TS_SOURCES ${CONSOLE_SOURCE_DIR}/plugin/js/*.ts) - set(CONSOLE_CSS_SOURCE ${CONSOLE_SOURCE_DIR}/plugin/css/dispatch.css) - set(ALL_CONSOLE_SOURCES ${CONSOLE_JS_SOURCES} ${CONSOLE_TS_SOURCES} ${CONSOLE_CSS_SOURCE}) - - ## Files created during the console build - set(CONSOLE_ARTIFACTS - ${CONSOLE_BUILD_DIR}/dist/js/dispatch.min.js - ${CONSOLE_BUILD_DIR}/dist/js/vendor.min.js - ${CONSOLE_BUILD_DIR}/dist/css/dispatch.min.css - ${CONSOLE_BUILD_DIR}/dist/css/vendor.min.css - ) - - ## copy the build config files - configure_file( ${CONSOLE_SOURCE_DIR}/package.json ${CONSOLE_BUILD_DIR}/ COPYONLY) - configure_file( ${CONSOLE_SOURCE_DIR}/package-lock.json ${CONSOLE_BUILD_DIR}/ COPYONLY) - configure_file( ${CONSOLE_SOURCE_DIR}/tslint.json ${CONSOLE_BUILD_DIR}/ COPYONLY) - configure_file( ${CONSOLE_SOURCE_DIR}/gulpfile.js ${CONSOLE_BUILD_DIR}/ COPYONLY) - configure_file( ${CONSOLE_SOURCE_DIR}/vendor-js.txt ${CONSOLE_BUILD_DIR}/ COPYONLY) - configure_file( ${CONSOLE_SOURCE_DIR}/vendor-css.txt ${CONSOLE_BUILD_DIR}/ COPYONLY) - - ## Tell cmake how and when to build ${CONSOLE_ARTIFACTS} - add_custom_command ( - OUTPUT ${CONSOLE_ARTIFACTS} - COMMENT "Running console build" - COMMAND npm install --loglevel=error - COMMAND ${NPX_EXE} gulp --src ${CONSOLE_SOURCE_DIR} - DEPENDS ${ALL_CONSOLE_SOURCES} - WORKING_DIRECTORY ${CONSOLE_BUILD_DIR}/ - ) - - ## Ensure ${CONSOLE_ARTIFACTS} is built on a make when needed - add_custom_target(console ALL - DEPENDS ${CONSOLE_ARTIFACTS} - ) - - ## - ## Install the static and built console files - ## - - ## Files copied to the root of the console's install dir - set(BASE_FILES - ${CONSOLE_SOURCE_DIR}/index.html - ${CONSOLE_SOURCE_DIR}/favicon-32x32.png - ) - ## Files copied to the css/ dir - set(CSS_FONTS - ${CONSOLE_SOURCE_DIR}/plugin/css/brokers.ttf - ${CONSOLE_BUILD_DIR}/node_modules/angular-ui-grid/ui-grid.woff - ${CONSOLE_BUILD_DIR}/node_modules/angular-ui-grid/ui-grid.ttf - ) - ## Files copied to the fonts/ dir - set(VENDOR_FONTS - ${CONSOLE_BUILD_DIR}/node_modules/patternfly/dist/fonts/OpenSans-Regular-webfont.woff2 - ${CONSOLE_BUILD_DIR}/node_modules/patternfly/dist/fonts/OpenSans-Bold-webfont.woff2 - ${CONSOLE_BUILD_DIR}/node_modules/patternfly/dist/fonts/OpenSans-Light-webfont.woff2 - ${CONSOLE_BUILD_DIR}/node_modules/patternfly/dist/fonts/OpenSans-Semibold-webfont.woff2 - ${CONSOLE_BUILD_DIR}/node_modules/patternfly/dist/fonts/OpenSans-BoldItalic-webfont.woff2 - ${CONSOLE_BUILD_DIR}/node_modules/patternfly/dist/fonts/OpenSans-Italic-webfont.woff2 - ${CONSOLE_BUILD_DIR}/node_modules/patternfly/dist/fonts/fontawesome-webfont.woff2 - ${CONSOLE_BUILD_DIR}/node_modules/patternfly/dist/fonts/fontawesome-webfont.eot - ${CONSOLE_BUILD_DIR}/node_modules/patternfly/dist/fonts/PatternFlyIcons-webfont.ttf - ${CONSOLE_BUILD_DIR}/node_modules/bootstrap/dist/fonts/glyphicons-halflings-regular.woff2 - ) - - install(DIRECTORY ${CONSOLE_BUILD_DIR}/dist/ - DESTINATION ${CONSOLE_STAND_ALONE_INSTALL_DIR} - PATTERN "*.map" EXCLUDE - ) - install(DIRECTORY ${CONSOLE_SOURCE_DIR}/plugin/html/ - DESTINATION ${CONSOLE_STAND_ALONE_INSTALL_DIR}/html - FILES_MATCHING PATTERN "*.html" - ) - install(FILES ${BASE_FILES} - DESTINATION ${CONSOLE_STAND_ALONE_INSTALL_DIR} - ) - install(FILES ${CSS_FONTS} - DESTINATION ${CONSOLE_STAND_ALONE_INSTALL_DIR}/css/ - ) - install(FILES ${VENDOR_FONTS} - DESTINATION ${CONSOLE_STAND_ALONE_INSTALL_DIR}/fonts/ - ) - else(NPX_EXE) - message(STATUS "Cannot build console, npm not found") - endif(NPX_EXE) + find_program (NPM_EXECUTABLE npm DOC "Location of npm package manager") + + if (NPM_EXECUTABLE) + execute_process(COMMAND ${NPM_EXECUTABLE} --version + OUTPUT_VARIABLE NPM_VERSION) + if(${NPM_VERSION} VERSION_EQUAL "5.2.0" OR ${NPM_VERSION} VERSION_GREATER "5.2.0") + + find_program(NPX_EXE npx DOC "Location of the npx task runner") + if (NPX_EXE) + + set(CONSOLE_SOURCE_DIR "${CMAKE_SOURCE_DIR}/console/stand-alone") + set(CONSOLE_BUILD_DIR "${CMAKE_BINARY_DIR}/console") + + ## Files needed to create the ${CONSOLE_ARTIFACTS} + file (GLOB_RECURSE CONSOLE_JS_SOURCES ${CONSOLE_SOURCE_DIR}/plugin/js/*.js) + file (GLOB_RECURSE CONSOLE_TS_SOURCES ${CONSOLE_SOURCE_DIR}/plugin/js/*.ts) + file (GLOB_RECURSE CONSOLE_MODULE_SOURCES ${CONSOLE_SOURCE_DIR}/modules/*.js) + set(CONSOLE_CSS_SOURCE ${CONSOLE_SOURCE_DIR}/plugin/css/dispatch.css) + set(CONSOLE_MAIN ${CONSOLE_SOURCE_DIR}/main.js) + set(ALL_CONSOLE_SOURCES ${CONSOLE_MAIN} ${CONSOLE_MODULE_SOURCES} ${CONSOLE_JS_SOURCES} ${CONSOLE_TS_SOURCES} ${CONSOLE_CSS_SOURCE}) + + ## Files created during the console build + set(CONSOLE_ARTIFACTS + ${CONSOLE_BUILD_DIR}/dist/js/main.min.js + ${CONSOLE_BUILD_DIR}/dist/js/vendor.min.js + ${CONSOLE_BUILD_DIR}/dist/css/dispatch.min.css + ${CONSOLE_BUILD_DIR}/dist/css/vendor.min.css + ) + + ## copy the build config files + configure_file( ${CONSOLE_SOURCE_DIR}/package.json ${CONSOLE_BUILD_DIR}/ COPYONLY) + configure_file( ${CONSOLE_SOURCE_DIR}/package-lock.json ${CONSOLE_BUILD_DIR}/ COPYONLY) + configure_file( ${CONSOLE_SOURCE_DIR}/tslint.json ${CONSOLE_BUILD_DIR}/ COPYONLY) + configure_file( ${CONSOLE_SOURCE_DIR}/gulpfile.js ${CONSOLE_BUILD_DIR}/ COPYONLY) + configure_file( ${CONSOLE_SOURCE_DIR}/vendor-js.txt ${CONSOLE_BUILD_DIR}/ COPYONLY) + configure_file( ${CONSOLE_SOURCE_DIR}/vendor-css.txt ${CONSOLE_BUILD_DIR}/ COPYONLY) + + ## Tell cmake how and when to build ${CONSOLE_ARTIFACTS} + add_custom_command ( + OUTPUT ${CONSOLE_ARTIFACTS} + COMMENT "Running console build" + COMMAND npm install --loglevel=error + COMMAND ${NPX_EXE} gulp --src ${CONSOLE_SOURCE_DIR} --build "production" + DEPENDS ${ALL_CONSOLE_SOURCES} + WORKING_DIRECTORY ${CONSOLE_BUILD_DIR}/ + ) + + ## Ensure ${CONSOLE_ARTIFACTS} is built on a make when needed + add_custom_target(console ALL + DEPENDS ${CONSOLE_ARTIFACTS} + ) + + ## + ## Install the static and built console files + ## + + ## Files copied to the root of the console's install dir + set(BASE_FILES + ${CONSOLE_SOURCE_DIR}/index.html + ${CONSOLE_SOURCE_DIR}/favicon-32x32.png + ) + ## Files copied to the css/ dir + set(CSS_FONTS + ${CONSOLE_SOURCE_DIR}/plugin/css/brokers.ttf + ) + ## Files copied to the css/fonts/ dir + set(CSSFONTS_FONTS + ${CONSOLE_BUILD_DIR}/node_modules/angular-ui-grid/fonts/ui-grid.woff + ${CONSOLE_BUILD_DIR}/node_modules/angular-ui-grid/fonts/ui-grid.ttf + ) + ## Files copied to the fonts/ dir + set(VENDOR_FONTS + ${CONSOLE_BUILD_DIR}/node_modules/patternfly/dist/fonts/OpenSans-Regular-webfont.woff2 + ${CONSOLE_BUILD_DIR}/node_modules/patternfly/dist/fonts/OpenSans-Bold-webfont.woff2 + ${CONSOLE_BUILD_DIR}/node_modules/patternfly/dist/fonts/OpenSans-Light-webfont.woff2 + ${CONSOLE_BUILD_DIR}/node_modules/patternfly/dist/fonts/OpenSans-Semibold-webfont.woff2 + ${CONSOLE_BUILD_DIR}/node_modules/patternfly/dist/fonts/OpenSans-SemiboldItalic-webfont.woff2 + ${CONSOLE_BUILD_DIR}/node_modules/patternfly/dist/fonts/OpenSans-BoldItalic-webfont.woff2 + ${CONSOLE_BUILD_DIR}/node_modules/patternfly/dist/fonts/OpenSans-Italic-webfont.woff2 + ${CONSOLE_BUILD_DIR}/node_modules/patternfly/dist/fonts/fontawesome-webfont.woff2 + ${CONSOLE_BUILD_DIR}/node_modules/patternfly/dist/fonts/fontawesome-webfont.eot + ${CONSOLE_BUILD_DIR}/node_modules/patternfly/dist/fonts/PatternFlyIcons-webfont.ttf + ${CONSOLE_BUILD_DIR}/node_modules/bootstrap/dist/fonts/glyphicons-halflings-regular.woff2 + ) + + install(DIRECTORY ${CONSOLE_BUILD_DIR}/dist/ + DESTINATION ${CONSOLE_STAND_ALONE_INSTALL_DIR} + PATTERN "*.map" EXCLUDE + ) + install(DIRECTORY ${CONSOLE_SOURCE_DIR}/plugin/html/ + DESTINATION ${CONSOLE_STAND_ALONE_INSTALL_DIR}/html + FILES_MATCHING PATTERN "*.html" + ) + install(FILES ${BASE_FILES} + DESTINATION ${CONSOLE_STAND_ALONE_INSTALL_DIR} + ) + install(FILES ${CSS_FONTS} + DESTINATION ${CONSOLE_STAND_ALONE_INSTALL_DIR}/css/ + ) + install(FILES ${CSSFONTS_FONTS} + DESTINATION ${CONSOLE_STAND_ALONE_INSTALL_DIR}/css/fonts/ + ) + install(FILES ${VENDOR_FONTS} + DESTINATION ${CONSOLE_STAND_ALONE_INSTALL_DIR}/fonts/ + ) + else(NPX_EXE) + message(STATUS "Cannot build console, npx not found.") + endif(NPX_EXE) + else(${NPM_VERSION} VERSION_EQUAL "5.2.0" OR ${NPM_VERSION} VERSION_GREATER "5.2.0") + message(STATUS "Cannot build console. npm version 5.2 or greater is required.") + endif(${NPM_VERSION} VERSION_EQUAL "5.2.0" OR ${NPM_VERSION} VERSION_GREATER "5.2.0") + endif(NPM_EXECUTABLE) + endif(CONSOLE_INSTALL) ## http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/b5deb035/console/stand-alone/.babelrc ---------------------------------------------------------------------- diff --git a/console/stand-alone/.babelrc b/console/stand-alone/.babelrc new file mode 100644 index 0000000..d67a300 --- /dev/null +++ b/console/stand-alone/.babelrc @@ -0,0 +1,21 @@ +/* +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +*/ +{ + "presets": ["es2015"] +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/b5deb035/console/stand-alone/gulpfile.js ---------------------------------------------------------------------- diff --git a/console/stand-alone/gulpfile.js b/console/stand-alone/gulpfile.js index 46531c3..0ddaf10 100644 --- a/console/stand-alone/gulpfile.js +++ b/console/stand-alone/gulpfile.js @@ -19,20 +19,26 @@ under the License. `; const gulp = require('gulp'), - babel = require('gulp-babel'), + mocha = require('gulp-mocha'), + gulpif = require('gulp-if'), + rollup = require('rollup-stream'), + source = require('vinyl-source-stream'), + buffer = require('vinyl-buffer'), concat = require('gulp-concat'), uglify = require('gulp-uglify'), + terser = require('gulp-terser'), + babel = require('gulp-babel'), ngAnnotate = require('gulp-ng-annotate'), - rename = require('gulp-rename'), cleanCSS = require('gulp-clean-css'), del = require('del'), eslint = require('gulp-eslint'), maps = require('gulp-sourcemaps'), insert = require('gulp-insert'), + rename = require('gulp-rename'), fs = require('fs'), tsc = require('gulp-typescript'), - tslint = require('gulp-tslint'); - //tsProject = tsc.createProject('tsconfig.json'); + tslint = require('gulp-tslint'), + through = require('through2'); // temp directory for converted typescript files const built_ts = 'built_ts'; @@ -59,6 +65,7 @@ const arg = (argList => { })(process.argv); var src = arg.src ? arg.src + '/' : ''; +var production = (arg.build === 'production'); const paths = { typescript: { @@ -70,10 +77,14 @@ const paths = { dest: 'dist/css/' }, scripts: { - src: [src + 'plugin/js/**/*.js', built_ts + '/**/*.js'], + src: [src + 'plugin/js/**/*.js'], dest: 'dist/js/' } }; +var touch = through.obj(function(file, enc, done) { + var now = new Date; + fs.utimes(file.path, now, now, done); +}); function clean() { return del(['dist',built_ts ]); @@ -110,20 +121,6 @@ function vendor_styles() { .pipe(gulp.dest(paths.styles.dest)); } -function scripts() { - return gulp.src(paths.scripts.src, { sourcemaps: true }) - .pipe(babel({ - presets: [require.resolve('babel-preset-env')] - })) - .pipe(ngAnnotate()) - .pipe(maps.init()) - .pipe(uglify()) - .pipe(concat('dispatch.min.js')) - .pipe(insert.prepend(license)) - .pipe(maps.write('./')) - .pipe(gulp.dest(paths.scripts.dest)); -} - function vendor_scripts() { var vendor_lines = fs.readFileSync('vendor-js.txt').toString().split('\n'); var vendor_files = vendor_lines.filter( function (line) { @@ -134,7 +131,8 @@ function vendor_scripts() { .pipe(uglify()) .pipe(concat('vendor.min.js')) .pipe(maps.write('./')) - .pipe(gulp.dest(paths.scripts.dest)); + .pipe(gulp.dest(paths.scripts.dest)) + .pipe(touch); } function watch() { gulp.watch(paths.scripts.src, scripts); @@ -167,10 +165,68 @@ function ts_lint() { .pipe(tslint.report()); } +function scripts() { + return rollup({ + input: src + './main.js', + sourcemap: true, + format: 'es' + }).on('error', e => { + console.error(`${e.stack}`); + }) + + // point to the entry file and gives the name of the output file. + .pipe(source('main.min.js', src)) + + // buffer the output. most gulp plugins, including gulp-sourcemaps, don't support streams. + .pipe(buffer()) + + // tell gulp-sourcemaps to load the inline sourcemap produced by rollup-stream. + .pipe(maps.init({loadMaps: true})) + // transform the code further here. + /* + .pipe(babel( + {presets: [ + ['env', { + targets: { + 'browsers': [ + 'Chrome >= 52', + 'FireFox >= 44', + 'Safari >= 7', + 'Explorer 11', + 'last 4 Edge versions' + ] + }, + useBuiltIns: true, + //debug: true + }], + 'es2015' + ], + 'ignore': [ + 'node_modules' + ] + } + )) + */ + .pipe(ngAnnotate()) + //.pipe(gulpif(production, uglify())) + .pipe(gulpif(production, terser())) + .pipe(gulpif(production, insert.prepend(license))) + // write the sourcemap alongside the output file. + .pipe(maps.write('.')) + + // and output to ./dist/main.js as normal. + .pipe(gulp.dest(paths.scripts.dest)); +} + +function test () { + return gulp.src(['test/**/*.js'], {read: false}) + .pipe(mocha({require: ['babel-core/register'], exit: true})) + .on('error', console.error); +} + var build = gulp.series( clean, // removes the dist/ dir - gulp.parallel(lint, ts_lint), // lints the .js, .ts files - typescript, // converts .ts to .js + lint, // lints the .js gulp.parallel(vendor_styles, vendor_scripts, styles, scripts), // uglify and concat cleanup // remove .js that were converted from .ts ); @@ -186,5 +242,6 @@ exports.tsc = typescript; exports.scripts = scripts; exports.styles = styles; exports.vendor = vendor; +exports.test = test; gulp.task('default', build); http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/b5deb035/console/stand-alone/index.html ---------------------------------------------------------------------- diff --git a/console/stand-alone/index.html b/console/stand-alone/index.html index 68070a3..1f08594 100644 --- a/console/stand-alone/index.html +++ b/console/stand-alone/index.html @@ -20,7 +20,6 @@ under the License. <html xmlns:ng="https://angularjs.org"> <head> - <meta charset="utf-8"/> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"/> @@ -76,9 +75,15 @@ under the License. </div> </div> +<!-- <script type="module" src="js/main.min.js"></script> --> <script type="text/javascript" src="js/vendor.min.js"></script> -<script type="text/javascript" src="js/dispatch.min.js"></script> - +<script type="text/javascript" src="js/main.min.js"></script> +<script defer nomodule> + var installError = document.getElementById('installError'); + if (installError) { + installError.innerHTML = 'This browser is not supported because it does not support es-2015 modules. <a href="https://www.ecma-international.org/ecma-262/6.0/">https://www.ecma-international.org/ecma-262/6.0/</a><br/>Please use a different browser.'; + } +</script> <script> // If angular hasn't loaded a page after 1 second, display the error message http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/b5deb035/console/stand-alone/main.js ---------------------------------------------------------------------- diff --git a/console/stand-alone/main.js b/console/stand-alone/main.js new file mode 100644 index 0000000..ca69709 --- /dev/null +++ b/console/stand-alone/main.js @@ -0,0 +1,254 @@ +/* +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +*/ +/* global angular d3 */ + +/** + * @module QDR + * @main QDR + * + * The main entry point for the QDR module + * + */ + +//import angular from 'angular'; +import { QDRLogger, QDRTemplatePath, QDR_LAST_LOCATION } from './plugin/js/qdrGlobals.js'; +import { QDRService } from './plugin/js/qdrService.js'; +import { QDRChartService } from './plugin/js/qdrChartService.js'; +import { NavBarController } from './plugin/js/navbar.js'; +import { OverviewController } from './plugin/js/qdrOverview.js'; +import { OverviewChartsController } from './plugin/js/qdrOverviewChartsController.js'; +import { OverviewLogsController } from './plugin/js/qdrOverviewLogsController.js'; +import { TopologyController } from './plugin/js/topology/qdrTopology.js'; +import { ChordController } from './plugin/js/chord/qdrChord.js'; +import { ListController } from './plugin/js/qdrList.js'; +import { TopAddressesController } from './plugin/js/qdrTopAddressesController.js'; +import { ChartDialogController } from './plugin/js/dlgChartController.js'; +import { SettingsController } from './plugin/js/qdrSettings.js'; +import { SchemaController } from './plugin/js/qdrSchema.js'; +import { ChartsController } from './plugin/js/qdrCharts.js'; +import { posint } from './plugin/js/posintDirective.js'; + +(function(QDR) { + + /** + * This plugin's angularjs module instance + */ + QDR.module = angular.module('QDR', ['ngRoute', 'ngSanitize', 'ngResource', 'ui.bootstrap', + 'ui.grid', 'ui.grid.selection', 'ui.grid.autoResize', 'ui.grid.resizeColumns', 'ui.grid.saveState', + 'ui.slider', 'ui.checkbox']); + + // set up the routing for this plugin + QDR.module.config(function($routeProvider) { + $routeProvider + .when('/', { + templateUrl: QDRTemplatePath + 'qdrOverview.html' + }) + .when('/overview', { + templateUrl: QDRTemplatePath + 'qdrOverview.html' + }) + .when('/topology', { + templateUrl: QDRTemplatePath + 'qdrTopology.html' + }) + .when('/list', { + templateUrl: QDRTemplatePath + 'qdrList.html' + }) + .when('/schema', { + templateUrl: QDRTemplatePath + 'qdrSchema.html' + }) + .when('/charts', { + templateUrl: QDRTemplatePath + 'qdrCharts.html' + }) + .when('/chord', { + templateUrl: QDRTemplatePath + 'qdrChord.html' + }) + .when('/connect', { + templateUrl: QDRTemplatePath + 'qdrConnect.html' + }); + }); + + QDR.module.config(function ($compileProvider) { + $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|ftp|mailto|chrome-extension|file|blob):/); + $compileProvider.imgSrcSanitizationWhitelist(/^\s*(https?|ftp|mailto|chrome-extension):/); + }); + + QDR.module.filter('to_trusted', ['$sce', function($sce){ + return function(text) { + return $sce.trustAsHtml(text); + }; + }]); + + QDR.module.filter('humanify', ['QDRService', function (QDRService) { + return function (input) { + return QDRService.utilities.humanify(input); + }; + }]); + + QDR.module.filter('Pascalcase', function () { + return function (str) { + if (!str) + return ''; + return str.replace(/(\w)(\w*)/g, + function(g0,g1,g2){return g1.toUpperCase() + g2.toLowerCase();}); + }; + }); + + QDR.module.filter('safePlural', function () { + return function (str) { + var es = ['x', 'ch', 'ss', 'sh']; + for (var i=0; i<es.length; ++i) { + if (str.endsWith(es[i])) + return str + 'es'; + } + if (str.endsWith('y')) + return str.substr(0, str.length-2) + 'ies'; + if (str.endsWith('s')) + return str; + return str + 's'; + }; + }); + + QDR.module.filter('pretty', function () { + return function (str) { + var formatComma = d3.format(','); + if (!isNaN(parseFloat(str)) && isFinite(str)) + return formatComma(str); + return str; + }; + }); + + // one-time initialization happens in the run function + // of our module + QDR.module.run( ['$rootScope', '$route', '$timeout', '$location', '$log', 'QDRService', 'QDRChartService', function ($rootScope, $route, $timeout, $location, $log, QDRService, QDRChartService) { + let QDRLog = new QDRLogger($log, 'main'); + QDRLog.info('************* creating Dispatch Console ************'); + + var curPath = $location.path(); + var org = curPath.substr(1); + if (org && org.length > 0 && org !== 'connect') { + $location.search('org', org); + } else { + $location.search('org', null); + } + QDR.queue = d3.queue; + + if (!QDRService.management.connection.is_connected()) { + // attempt to connect to the host:port that served this page + var host = $location.host(); + var port = $location.port(); + var search = $location.search(); + if (search.org) { + if (search.org === 'connect') + $location.search('org', 'overview'); + } + var connectOptions = {address: host, port: port}; + QDRLog.info('Attempting AMQP over websockets connection using address:port of browser ('+host+':'+port+')'); + QDRService.management.connection.testConnect(connectOptions) + .then( function () { + // We didn't connect with reconnect: true flag. + // The reason being that if we used reconnect:true and the connection failed, rhea would keep trying. There + // doesn't appear to be a way to tell it to stop trying to reconnect. + QDRService.disconnect(); + QDRLog.info('Connect succeeded. Using address:port of browser'); + connectOptions.reconnect = true; + // complete the connection (create the sender/receiver) + QDRService.connect(connectOptions) + .then( function () { + // register a callback for when the node list is available (needed for loading saved charts) + QDRService.management.topology.addUpdatedAction('initChartService', function() { + QDRService.management.topology.delUpdatedAction('initChartService'); + QDRChartService.init(); // initialize charting service after we are connected + }); + // get the list of nodes + QDRService.management.topology.startUpdating(false); + }); + }, function () { + QDRLog.info('failed to auto-connect to ' + host + ':' + port); + QDRLog.info('redirecting to connect page'); + $timeout(function () { + $location.path('/connect'); + $location.search('org', org); + $location.replace(); + }); + }); + } + + $rootScope.$on('$routeChangeSuccess', function() { + var path = $location.path(); + if (path !== '/connect') { + localStorage[QDR_LAST_LOCATION] = path; + } + }); + }]); + + QDR.module.controller ('QDR.MainController', ['$scope', '$log', '$location', function ($scope, $log, $location) { + let QDRLog = new QDRLogger($log, 'MainController'); + QDRLog.debug('started QDR.MainController with location.url: ' + $location.url()); + QDRLog.debug('started QDR.MainController with window.location.pathname : ' + window.location.pathname); + $scope.topLevelTabs = []; + $scope.topLevelTabs.push({ + id: 'qdr', + content: 'Qpid Dispatch Router Console', + title: 'Dispatch Router Console', + isValid: function() { return true; }, + href: function() { return '#connect'; }, + isActive: function() { return true; } + }); + }]); + + QDR.module.controller ('QDR.Core', function ($scope, $rootScope) { + $scope.alerts = []; + $scope.breadcrumb = {}; + $scope.closeAlert = function(index) { + $scope.alerts.splice(index, 1); + }; + $scope.$on('setCrumb', function(event, data) { + $scope.breadcrumb = data; + }); + $scope.$on('newAlert', function(event, data) { + $scope.alerts.push(data); + $scope.$apply(); + }); + $scope.$on('clearAlerts', function () { + $scope.alerts = []; + $scope.$apply(); + }); + $scope.pageMenuClicked = function () { + $rootScope.$broadcast('pageMenuClicked'); + }; + }); + + QDR.module.controller('QDR.NavBarController', NavBarController); + QDR.module.controller('QDR.OverviewController', OverviewController); + QDR.module.controller('QDR.OverviewChartsController', OverviewChartsController); + QDR.module.controller('QDR.OverviewLogsController', OverviewLogsController); + QDR.module.controller('QDR.TopAddressesController', TopAddressesController); + QDR.module.controller('QDR.ChartDialogController', ChartDialogController); + QDR.module.controller('QDR.SettingsController', SettingsController); + QDR.module.controller('QDR.TopologyController', TopologyController); + QDR.module.controller('QDR.ChordController', ChordController); + QDR.module.controller('QDR.ListController', ListController); + QDR.module.controller('QDR.SchemaController', SchemaController); + QDR.module.controller('QDR.ChartsController', ChartsController); + + QDR.module.service('QDRService', QDRService); + QDR.module.service('QDRChartService', QDRChartService); + QDR.module.directive('posint', posint); + // .directive('exampleDirective', () => new ExampleDirective); +}({})); + http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/b5deb035/console/stand-alone/modules/connection.js ---------------------------------------------------------------------- diff --git a/console/stand-alone/modules/connection.js b/console/stand-alone/modules/connection.js new file mode 100644 index 0000000..db21e01 --- /dev/null +++ b/console/stand-alone/modules/connection.js @@ -0,0 +1,347 @@ +/* + * Copyright 2017 Red Hat Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* global Promise */ + +const rhea = require('rhea'); +//import { on, websocket_connect, removeListener, once, connect } from 'rhea'; +import Correlator from './correlator.js'; + +class ConnectionManager { + constructor(protocol) { + this.sender = undefined; + this.receiver = undefined; + this.connection = undefined; + this.version = undefined; + this.errorText = undefined; + this.protocol = protocol; + this.schema = undefined; + this.connectActions = []; + this.disconnectActions = []; + this.correlator = new Correlator(); + this.on_message = (function (context) { + this.correlator.resolve(context); + }).bind(this); + this.on_disconnected = (function () { + this.errorText = 'Disconnected'; + this.executeDisconnectActions(this.errorText); + }).bind(this); + this.on_connection_open = (function () { + this.executeConnectActions(); + }).bind(this); + } + versionCheck(minVer) { + var verparts = this.version.split('.'); + var minparts = minVer.split('.'); + try { + for (var i = 0; i < minparts.length; ++i) { + if (parseInt(minVer[i] > parseInt(verparts[i]))) + return false; + } + } + catch (e) { + return false; + } + return true; + } + addConnectAction(action) { + if (typeof action === 'function') { + this.delConnectAction(action); + this.connectActions.push(action); + } + } + addDisconnectAction(action) { + if (typeof action === 'function') { + this.delDisconnectAction(action); + this.disconnectActions.push(action); + } + } + delConnectAction(action) { + if (typeof action === 'function') { + var index = this.connectActions.indexOf(action); + if (index >= 0) + this.connectActions.splice(index, 1); + } + } + delDisconnectAction(action) { + if (typeof action === 'function') { + var index = this.disconnectActions.indexOf(action); + if (index >= 0) + this.disconnectActions.splice(index, 1); + } + } + executeConnectActions() { + this.connectActions.forEach(function (action) { + try { + action(); + } + catch (e) { + // in case the page that registered the handler has been unloaded + } + }); + this.connectActions = []; + } + executeDisconnectActions(message) { + this.disconnectActions.forEach(function (action) { + try { + action(message); + } + catch (e) { + // in case the page that registered the handler has been unloaded + } + }); + this.disconnectActions = []; + } + on(eventType, fn) { + if (eventType === 'connected') { + this.addConnectAction(fn); + } + else if (eventType === 'disconnected') { + this.addDisconnectAction(fn); + } + else { + console.log('unknown event type ' + eventType); + } + } + setSchema(schema) { + this.schema = schema; + } + is_connected() { + return this.connection && + this.sender && + this.receiver && + this.receiver.remote && + this.receiver.remote.attach && + this.receiver.remote.attach.source && + this.receiver.remote.attach.source.address && + this.connection.is_open(); + } + disconnect() { + if (this.sender) + this.sender.close(); + if (this.receiver) + this.receiver.close(); + if (this.connection) + this.connection.close(); + } + createSenderReceiver(options) { + return new Promise((function (resolve, reject) { + var timeout = options.timeout || 10000; + // set a timer in case the setup takes too long + var giveUp = (function () { + this.connection.removeListener('receiver_open', receiver_open); + this.connection.removeListener('sendable', sendable); + this.errorText = 'timed out creating senders and receivers'; + reject(Error(this.errorText)); + }).bind(this); + var timer = setTimeout(giveUp, timeout); + // register an event hander for when the setup is complete + var sendable = (function (context) { + clearTimeout(timer); + this.version = this.connection.properties ? this.connection.properties.version : '0.1.0'; + // in case this connection dies + rhea.on('disconnected', this.on_disconnected); + // in case this connection dies and is then reconnected automatically + rhea.on('connection_open', this.on_connection_open); + // receive messages here + this.connection.on('message', this.on_message); + resolve(context); + }).bind(this); + this.connection.once('sendable', sendable); + // Now actually createt the sender and receiver. + // register an event handler for when the receiver opens + var receiver_open = (function () { + // once the receiver is open, create the sender + if (options.sender_address) + this.sender = this.connection.open_sender(options.sender_address); + else + this.sender = this.connection.open_sender(); + }).bind(this); + this.connection.once('receiver_open', receiver_open); + // create a dynamic receiver + this.receiver = this.connection.open_receiver({ source: { dynamic: true } }); + }).bind(this)); + } + connect(options) { + return new Promise((function (resolve, reject) { + var finishConnecting = function () { + this.createSenderReceiver(options) + .then(function (results) { + resolve(results); + }, function (error) { + reject(error); + }); + }; + if (!this.connection) { + options.test = false; // if you didn't want a connection, you should have called testConnect() and not connect() + this.testConnect(options) + .then((function () { + finishConnecting.call(this); + }).bind(this), (function () { + // connect failed or timed out + this.errorText = 'Unable to connect'; + this.executeDisconnectActions(this.errorText); + reject(Error(this.errorText)); + }).bind(this)); + } + else { + finishConnecting.call(this); + } + }).bind(this)); + } + getReceiverAddress() { + return this.receiver.remote.attach.source.address; + } + // Try to connect using the options. + // if options.test === true -> close the connection if it succeeded and resolve the promise + // if the connection attempt fails or times out, reject the promise regardless of options.test + testConnect(options, callback) { + return new Promise((function (resolve, reject) { + var timeout = options.timeout || 10000; + var reconnect = options.reconnect || false; // in case options.reconnect is undefined + var baseAddress = options.address + ':' + options.port; + if (options.linkRouteAddress) { + baseAddress += ('/' + options.linkRouteAddress); + } + var wsprotocol = location.protocol === 'https:' ? 'wss' : 'ws'; + if (this.connection) { + delete this.connection; + this.connection = null; + } + var ws = rhea.websocket_connect(WebSocket); + var c = { + connection_details: new ws(wsprotocol + '://' + baseAddress, ['binary']), + reconnect: reconnect, + properties: options.properties || { console_identifier: 'Dispatch console' } + }; + if (options.hostname) + c.hostname = options.hostname; + if (options.username && options.username !== '') { + c.username = options.username; + } + if (options.password && options.password !== '') { + c.password = options.password; + } + // set a timeout + var disconnected = (function () { + clearTimeout(timer); + rhea.removeListener('disconnected', disconnected); + rhea.removeListener('connection_open', connection_open); + this.connection = null; + var rej = 'failed to connect'; + if (callback) + callback({ error: rej }); + reject(Error(rej)); + }).bind(this); + var timer = setTimeout(disconnected, timeout); + // the event handler for when the connection opens + var connection_open = (function (context) { + clearTimeout(timer); + // prevent future disconnects from calling reject + rhea.removeListener('disconnected', disconnected); + // we were just checking. we don't really want a connection + if (options.test) { + context.connection.close(); + this.connection = null; + } + else + this.on_connection_open(); + var res = { context: context }; + if (callback) + callback(res); + resolve(res); + }).bind(this); + // register an event handler for when the connection opens + rhea.once('connection_open', connection_open); + // register an event handler for if the connection fails to open + rhea.once('disconnected', disconnected); + // attempt the connection + this.connection = rhea.connect(c); + }).bind(this)); + } + sendMgmtQuery(operation) { + return this.send([], '/$management', operation); + } + sendQuery(toAddr, entity, attrs, operation) { + operation = operation || 'QUERY'; + var fullAddr = this._fullAddr(toAddr); + var body = { attributeNames: attrs || [] }; + return this.send(body, fullAddr, operation, this.schema.entityTypes[entity].fullyQualifiedType); + } + send(body, to, operation, entityType) { + var application_properties = { + operation: operation, + type: 'org.amqp.management', + name: 'self' + }; + if (entityType) + application_properties.entityType = entityType; + return this._send(body, to, application_properties); + } + sendMethod(toAddr, entity, attrs, operation, props) { + var fullAddr = this._fullAddr(toAddr); + var application_properties = { + operation: operation, + }; + if (entity) { + application_properties.type = this.schema.entityTypes[entity].fullyQualifiedType; + } + if (attrs.name) + application_properties.name = attrs.name; + else if (attrs.identity) + application_properties.identity = attrs.identity; + if (props) { + for (var attrname in props) { + application_properties[attrname] = props[attrname]; + } + } + return this._send(attrs, fullAddr, application_properties); + } + _send(body, to, application_properties) { + var _correlationId = this.correlator.corr(); + var self = this; + return new Promise(function (resolve, reject) { + self.correlator.register(_correlationId, resolve, reject); + self.sender.send({ + body: body, + to: to, + reply_to: self.receiver.remote.attach.source.address, + correlation_id: _correlationId, + application_properties: application_properties + }); + }); + } + _fullAddr(toAddr) { + var toAddrParts = toAddr.split('/'); + toAddrParts.shift(); + var fullAddr = toAddrParts.join('/'); + return fullAddr; + } + availableQeueuDepth() { + return this.correlator.depth(); + } +} + +class ConnectionException { + constructor(message) { + this.message = message; + this.name = 'ConnectionException'; + } +} + +const _ConnectionManager = ConnectionManager; +export { _ConnectionManager as ConnectionManager }; +const _ConnectionException = ConnectionException; +export { _ConnectionException as ConnectionException }; http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/b5deb035/console/stand-alone/modules/correlator.js ---------------------------------------------------------------------- diff --git a/console/stand-alone/modules/correlator.js b/console/stand-alone/modules/correlator.js new file mode 100644 index 0000000..bf34f93 --- /dev/null +++ b/console/stand-alone/modules/correlator.js @@ -0,0 +1,50 @@ +/* + * Copyright 2017 Red Hat Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { utils } from './utilities.js'; + +class Correlator { + constructor() { + this._objects = {}; + this._correlationID = 0; + this.maxCorrelatorDepth = 10; + } + corr() { + return ++(this._correlationID) + ''; + } + // Associate this correlation id with the promise's resolve and reject methods + register(id, resolve, reject) { + this._objects[id] = { resolver: resolve, rejector: reject }; + } + // Call the promise's resolve method. + // This is called by rhea's receiver.on('message') function + resolve(context) { + var correlationID = context.message.correlation_id; + // call the promise's resolve function with a copy of the rhea response (so we don't keep any references to internal rhea data) + this._objects[correlationID].resolver({ response: utils.copy(context.message.body), context: context }); + delete this._objects[correlationID]; + } + reject(id, error) { + this._objects[id].rejector(error); + delete this._objects[id]; + } + // Return the number of requests that can be sent before we start queuing requests + depth() { + return Math.max(1, this.maxCorrelatorDepth - Object.keys(this._objects).length); + } +} + +export default Correlator; http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/b5deb035/console/stand-alone/modules/management.js ---------------------------------------------------------------------- diff --git a/console/stand-alone/modules/management.js b/console/stand-alone/modules/management.js new file mode 100644 index 0000000..4b3bb32 --- /dev/null +++ b/console/stand-alone/modules/management.js @@ -0,0 +1,63 @@ +/* + * Copyright 2015 Red Hat Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* global Promise */ + +import { ConnectionManager } from './connection.js'; +import Topology from './topology.js'; + +export class Management { + constructor(protocol) { + this.connection = new ConnectionManager(protocol); + this.topology = new Topology(this.connection); + } + getSchema(callback) { + var self = this; + return new Promise(function (resolve, reject) { + self.connection.sendMgmtQuery('GET-SCHEMA') + .then(function (responseAndContext) { + var response = responseAndContext.response; + for (var entityName in response.entityTypes) { + var entity = response.entityTypes[entityName]; + if (entity.deprecated) { + // deprecated entity + delete response.entityTypes[entityName]; + } + else { + for (var attributeName in entity.attributes) { + var attribute = entity.attributes[attributeName]; + if (attribute.deprecated) { + // deprecated attribute + delete response.entityTypes[entityName].attributes[attributeName]; + } + } + } + } + self.connection.setSchema(response); + if (callback) + callback(response); + resolve(response); + }, function (error) { + if (callback) + callback(error); + reject(error); + }); + }); + } + schema() { + return this.connection.schema; + } +} http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/b5deb035/console/stand-alone/modules/topology.js ---------------------------------------------------------------------- diff --git a/console/stand-alone/modules/topology.js b/console/stand-alone/modules/topology.js new file mode 100644 index 0000000..e208a6f --- /dev/null +++ b/console/stand-alone/modules/topology.js @@ -0,0 +1,403 @@ +/* + * Copyright 2015 Red Hat Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* global Promise d3 */ + +import { utils } from './utilities.js'; + +class Topology { + constructor(connectionManager) { + this.connection = connectionManager; + this.updatedActions = {}; + this.entities = []; // which entities to request each topology update + this.entityAttribs = {}; + this._nodeInfo = {}; // info about all known nodes and entities + this.filtering = false; // filter out nodes that don't have connection info + this.timeout = 5000; + this.updateInterval = 5000; + this._getTimer = null; + this.updating = false; + } + addUpdatedAction(key, action) { + if (typeof action === 'function') { + this.updatedActions[key] = action; + } + } + delUpdatedAction(key) { + if (key in this.updatedActions) + delete this.updatedActions[key]; + } + executeUpdatedActions(error) { + for (var action in this.updatedActions) { + this.updatedActions[action].apply(this, [error]); + } + } + setUpdateEntities(entities) { + this.entities = entities; + for (var i = 0; i < entities.length; i++) { + this.entityAttribs[entities[i]] = []; + } + } + addUpdateEntities(entityAttribs) { + if (Object.prototype.toString.call(entityAttribs) !== '[object Array]') { + entityAttribs = [entityAttribs]; + } + for (var i = 0; i < entityAttribs.length; i++) { + var entity = entityAttribs[i].entity; + this.entityAttribs[entity] = entityAttribs[i].attrs || []; + } + } + on(eventName, fn, key) { + if (eventName === 'updated') + this.addUpdatedAction(key, fn); + } + unregister(eventName, key) { + if (eventName === 'updated') + this.delUpdatedAction(key); + } + nodeInfo() { + return this._nodeInfo; + } + get() { + return new Promise((function (resolve, reject) { + this.connection.sendMgmtQuery('GET-MGMT-NODES') + .then((function (response) { + response = response.response; + if (Object.prototype.toString.call(response) === '[object Array]') { + var workInfo = {}; + // if there is only one node, it will not be returned + if (response.length === 0) { + var parts = this.connection.getReceiverAddress().split('/'); + parts[parts.length - 1] = '$management'; + response.push(parts.join('/')); + } + for (var i = 0; i < response.length; ++i) { + workInfo[response[i]] = {}; + } + var gotResponse = function (nodeName, entity, response) { + workInfo[nodeName][entity] = response; + }; + var q = d3.queue(this.connection.availableQeueuDepth()); + for (var id in workInfo) { + for (var entity in this.entityAttribs) { + q.defer((this.q_fetchNodeInfo).bind(this), id, entity, this.entityAttribs[entity], q, gotResponse); + } + } + q.await((function () { + // filter out nodes that have no connection info + if (this.filtering) { + for (var id in workInfo) { + if (!(workInfo[id].connection)) { + this.flux = true; + delete workInfo[id]; + } + } + } + this._nodeInfo = utils.copy(workInfo); + this.onDone(this._nodeInfo); + resolve(this._nodeInfo); + }).bind(this)); + } + }).bind(this), function (error) { + reject(error); + }); + }).bind(this)); + } + onDone(result) { + clearTimeout(this._getTimer); + if (this.updating) + this._getTimer = setTimeout((this.get).bind(this), this.updateInterval); + this.executeUpdatedActions(result); + } + startUpdating(filter) { + this.stopUpdating(); + this.updating = true; + this.filtering = filter; + this.get(); + } + stopUpdating() { + this.updating = false; + if (this._getTimer) { + clearTimeout(this._getTimer); + this._getTimer = null; + } + } + fetchEntity(node, entity, attrs, callback) { + var results = {}; + var gotResponse = function (nodeName, dotentity, response) { + results = response; + }; + var q = d3.queue(this.connection.availableQeueuDepth()); + q.defer((this.q_fetchNodeInfo).bind(this), node, entity, attrs, q, gotResponse); + q.await(function () { + callback(node, entity, results); + }); + } + // called from d3.queue.defer so the last argument (callback) is supplied by d3 + q_fetchNodeInfo(nodeId, entity, attrs, q, heartbeat, callback) { + this.getNodeInfo(nodeId, entity, attrs, q, function (nodeName, dotentity, response) { + heartbeat(nodeName, dotentity, response); + callback(null); + }); + } + // get all the requested entities/attributes for a single router + fetchEntities(node, entityAttribs, doneCallback, resultCallback) { + var q = d3.queue(this.connection.availableQeueuDepth()); + var results = {}; + if (!resultCallback) { + resultCallback = function (nodeName, dotentity, response) { + if (!results[nodeName]) + results[nodeName] = {}; + results[nodeName][dotentity] = response; + }; + } + var gotAResponse = function (nodeName, dotentity, response) { + resultCallback(nodeName, dotentity, response); + }; + if (Object.prototype.toString.call(entityAttribs) !== '[object Array]') { + entityAttribs = [entityAttribs]; + } + for (var i = 0; i < entityAttribs.length; ++i) { + var ea = entityAttribs[i]; + q.defer((this.q_fetchNodeInfo).bind(this), node, ea.entity, ea.attrs || [], q, gotAResponse); + } + q.await(function () { + doneCallback(results); + }); + } + // get all the requested entities for all known routers + fetchAllEntities(entityAttribs, doneCallback, resultCallback) { + var q = d3.queue(this.connection.availableQeueuDepth()); + var results = {}; + if (!resultCallback) { + resultCallback = function (nodeName, dotentity, response) { + if (!results[nodeName]) + results[nodeName] = {}; + results[nodeName][dotentity] = response; + }; + } + var gotAResponse = function (nodeName, dotentity, response) { + resultCallback(nodeName, dotentity, response); + }; + if (Object.prototype.toString.call(entityAttribs) !== '[object Array]') { + entityAttribs = [entityAttribs]; + } + var nodes = Object.keys(this._nodeInfo); + for (var n = 0; n < nodes.length; ++n) { + for (var i = 0; i < entityAttribs.length; ++i) { + var ea = entityAttribs[i]; + q.defer((this.q_fetchNodeInfo).bind(this), nodes[n], ea.entity, ea.attrs || [], q, gotAResponse); + } + } + q.await(function () { + doneCallback(results); + }); + } + // enusre all the topology nones have all these entities + ensureAllEntities(entityAttribs, callback, extra) { + this.ensureEntities(Object.keys(this._nodeInfo), entityAttribs, callback, extra); + } + // ensure these nodes have all these entities. don't fetch unless forced to + ensureEntities(nodes, entityAttribs, callback, extra) { + if (Object.prototype.toString.call(entityAttribs) !== '[object Array]') { + entityAttribs = [entityAttribs]; + } + if (Object.prototype.toString.call(nodes) !== '[object Array]') { + nodes = [nodes]; + } + this.addUpdateEntities(entityAttribs); + var q = d3.queue(this.connection.availableQeueuDepth()); + for (var n = 0; n < nodes.length; ++n) { + for (var i = 0; i < entityAttribs.length; ++i) { + var ea = entityAttribs[i]; + // if we don'e already have the entity or we want to force a refresh + if (!this._nodeInfo[nodes[n]][ea.entity] || ea.force) + q.defer((this.q_ensureNodeInfo).bind(this), nodes[n], ea.entity, ea.attrs || [], q); + } + } + q.await(function () { + callback(extra); + }); + } + addNodeInfo(id, entity, values) { + // save the results in the nodeInfo object + if (id) { + if (!(id in this._nodeInfo)) { + this._nodeInfo[id] = {}; + } + // copy the values to allow garbage collection + this._nodeInfo[id][entity] = values; + } + } + isLargeNetwork() { + return Object.keys(this._nodeInfo).length >= 12; + } + getConnForLink(link) { + // find the connection for this link + var conns = this._nodeInfo[link.nodeId].connection; + var connIndex = conns.attributeNames.indexOf('identity'); + var linkCons = conns.results.filter(function (conn) { + return conn[connIndex] === link.connectionId; + }); + return utils.flatten(conns.attributeNames, linkCons[0]); + } + nodeNameList() { + var nl = []; + for (var id in this._nodeInfo) { + nl.push(utils.nameFromId(id)); + } + return nl.sort(); + } + nodeIdList() { + var nl = []; + for (var id in this._nodeInfo) { + //if (this._nodeInfo['connection']) + nl.push(id); + } + return nl.sort(); + } + nodeList() { + var nl = []; + for (var id in this._nodeInfo) { + nl.push({ + name: utils.nameFromId(id), + id: id + }); + } + return nl; + } + // d3.queue'd function to make a management query for entities/attributes + q_ensureNodeInfo(nodeId, entity, attrs, q, callback) { + this.getNodeInfo(nodeId, entity, attrs, q, (function (nodeName, dotentity, response) { + this.addNodeInfo(nodeName, dotentity, response); + callback(null); + }).bind(this)); + return { + abort: function () { + delete this._nodeInfo[nodeId]; + } + }; + } + getNodeInfo(nodeName, entity, attrs, q, callback) { + var timedOut = function (q) { + q.abort(); + }; + var atimer = setTimeout(timedOut, this.timeout, q); + this.connection.sendQuery(nodeName, entity, attrs) + .then(function (response) { + clearTimeout(atimer); + callback(nodeName, entity, response.response); + }, function () { + q.abort(); + }); + } + getMultipleNodeInfo(nodeNames, entity, attrs, callback, selectedNodeId, aggregate) { + var self = this; + if (typeof aggregate === 'undefined') + aggregate = true; + var responses = {}; + var gotNodesResult = function (nodeName, dotentity, response) { + responses[nodeName] = response; + }; + var q = d3.queue(this.connection.availableQeueuDepth()); + nodeNames.forEach(function (id) { + q.defer((self.q_fetchNodeInfo).bind(self), id, entity, attrs, q, gotNodesResult); + }); + q.await(function () { + if (aggregate) + self.aggregateNodeInfo(nodeNames, entity, selectedNodeId, responses, callback); + else { + callback(nodeNames, entity, responses); + } + }); + } + quiesceLink(nodeId, name) { + var attributes = { + adminStatus: 'disabled', + name: name + }; + return this.connection.sendMethod(nodeId, 'router.link', attributes, 'UPDATE'); + } + aggregateNodeInfo(nodeNames, entity, selectedNodeId, responses, callback) { + // aggregate the responses + var self = this; + var newResponse = {}; + var thisNode = responses[selectedNodeId]; + newResponse.attributeNames = thisNode.attributeNames; + newResponse.results = thisNode.results; + newResponse.aggregates = []; + // initialize the aggregates + for (var i = 0; i < thisNode.results.length; ++i) { + // there is a result for each unique entity found (ie addresses, links, etc.) + var result = thisNode.results[i]; + var vals = []; + // there is a val for each attribute in this entity + result.forEach(function (val) { + vals.push({ + sum: val, + detail: [] + }); + }); + newResponse.aggregates.push(vals); + } + var nameIndex = thisNode.attributeNames.indexOf('name'); + var ent = self.connection.schema.entityTypes[entity]; + var ids = Object.keys(responses); + ids.sort(); + ids.forEach(function (id) { + var response = responses[id]; + var results = response.results; + results.forEach(function (result) { + // find the matching result in the aggregates + var found = newResponse.aggregates.some(function (aggregate) { + if (aggregate[nameIndex].sum === result[nameIndex]) { + // result and aggregate are now the same record, add the graphable values + newResponse.attributeNames.forEach(function (key, i) { + if (ent.attributes[key] && ent.attributes[key].graph) { + if (id != selectedNodeId) + aggregate[i].sum += result[i]; + } + aggregate[i].detail.push({ + node: utils.nameFromId(id) + ':', + val: result[i] + }); + }); + return true; // stop looping + } + return false; // continute looking for the aggregate record + }); + if (!found) { + // this attribute was not found in the aggregates yet + // because it was not in the selectedNodeId's results + var vals = []; + result.forEach(function (val) { + vals.push({ + sum: val, + detail: [{ + node: utils.nameFromId(id), + val: val + }] + }); + }); + newResponse.aggregates.push(vals); + } + }); + }); + callback(nodeNames, entity, newResponse); + } +} + +export default Topology; \ No newline at end of file http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/b5deb035/console/stand-alone/modules/utilities.js ---------------------------------------------------------------------- diff --git a/console/stand-alone/modules/utilities.js b/console/stand-alone/modules/utilities.js new file mode 100644 index 0000000..328da38 --- /dev/null +++ b/console/stand-alone/modules/utilities.js @@ -0,0 +1,115 @@ +/* + * Copyright 2015 Red Hat Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* global d3 */ +var ddd = typeof window === 'undefined' ? require ('d3') : d3; + +var utils = { + isAConsole: function (properties, connectionId, nodeType, key) { + return this.isConsole({ + properties: properties, + connectionId: connectionId, + nodeType: nodeType, + key: key + }); + }, + isConsole: function (d) { + return (d && d.properties && d.properties.console_identifier === 'Dispatch console'); + }, + isArtemis: function (d) { + return (d.nodeType === 'route-container' || d.nodeType === 'on-demand') && (d.properties && d.properties.product === 'apache-activemq-artemis'); + }, + + isQpid: function (d) { + return (d.nodeType === 'route-container' || d.nodeType === 'on-demand') && (d.properties && d.properties.product === 'qpid-cpp'); + }, + flatten: function (attributes, result) { + if (!attributes || !result) + return {}; + var flat = {}; + attributes.forEach(function(attr, i) { + if (result && result.length > i) + flat[attr] = result[i]; + }); + return flat; + }, + copy: function (obj) { + if (obj) + return JSON.parse(JSON.stringify(obj)); + }, + identity_clean: function (identity) { + if (!identity) + return '-'; + var pos = identity.indexOf('/'); + if (pos >= 0) + return identity.substring(pos + 1); + return identity; + }, + addr_text: function (addr) { + if (!addr) + return '-'; + if (addr[0] == 'M') + return addr.substring(2); + else + return addr.substring(1); + }, + addr_class: function (addr) { + if (!addr) return '-'; + if (addr[0] == 'M') return 'mobile'; + if (addr[0] == 'R') return 'router'; + if (addr[0] == 'A') return 'area'; + if (addr[0] == 'L') return 'local'; + if (addr[0] == 'C') return 'link-incoming'; + if (addr[0] == 'E') return 'link-incoming'; + if (addr[0] == 'D') return 'link-outgoing'; + if (addr[0] == 'F') return 'link-outgoing'; + if (addr[0] == 'T') return 'topo'; + return 'unknown: ' + addr[0]; + }, + humanify: function (s) { + if (!s || s.length === 0) + return s; + var t = s.charAt(0).toUpperCase() + s.substr(1).replace(/[A-Z]/g, ' $&'); + return t.replace('.', ' '); + }, + pretty: function (v) { + var formatComma = ddd.format(','); + if (!isNaN(parseFloat(v)) && isFinite(v)) + return formatComma(v); + return v; + }, + isMSIE: function () { + return (document.documentMode || /Edge/.test(navigator.userAgent)); + }, + valFor: function (aAr, vAr, key) { + var idx = aAr.indexOf(key); + if ((idx > -1) && (idx < vAr.length)) { + return vAr[idx]; + } + return null; + }, + // extract the name of the router from the router id + nameFromId: function (id) { + // the router id looks like 'amqp:/topo/0/routerName/$managemrnt' + var parts = id.split('/'); + // handle cases where the router name contains a / + parts.splice(0, 3); // remove amqp, topo, 0 + parts.pop(); // remove $management + return parts.join('/'); + } + +}; +export { utils }; \ No newline at end of file --------------------------------------------------------------------- To unsubscribe, e-mail: commits-unsubscr...@qpid.apache.org For additional commands, e-mail: commits-h...@qpid.apache.org