├── .gitignore ├── .travis.yml ├── README.md ├── config ├── helpers.js ├── karma-test-shim.js ├── karma.conf.js ├── webpack.common.config.js ├── webpack.dev.config.js ├── webpack.prod.config.js └── webpack.test.config.js ├── karma.conf.js ├── package.json ├── src ├── app │ ├── boot.ts │ ├── model │ │ ├── api-definition.ts │ │ ├── api-json-schema.ts │ │ ├── api-operation.ts │ │ ├── api-parameter.ts │ │ ├── api-result.ts │ │ ├── api-utils.ts │ │ └── apidoc.ts │ ├── modules │ │ ├── app │ │ │ ├── app.component.ts │ │ │ ├── app.html │ │ │ ├── app.module.ts │ │ │ ├── app.routing.ts │ │ │ ├── header │ │ │ │ ├── header.html │ │ │ │ └── header.ts │ │ │ ├── home │ │ │ │ ├── home.html │ │ │ │ └── home.ts │ │ │ ├── settings │ │ │ │ ├── settings.html │ │ │ │ └── settings.ts │ │ │ └── themeable.component.ts │ │ ├── main │ │ │ ├── data-type │ │ │ │ ├── data-type-link.html │ │ │ │ └── data-type-link.ts │ │ │ ├── detail │ │ │ │ ├── detail.html │ │ │ │ └── detail.ts │ │ │ ├── left-menu │ │ │ │ ├── left-menu.html │ │ │ │ └── left-menu.ts │ │ │ ├── list │ │ │ │ ├── list.html │ │ │ │ └── list.ts │ │ │ ├── main.module.ts │ │ │ ├── main.routing.ts │ │ │ └── main.ts │ │ ├── materialize │ │ │ ├── directives │ │ │ │ ├── materialize-collapsible.ts │ │ │ │ ├── materialize-collection.ts │ │ │ │ └── materialize-header.ts │ │ │ ├── input-file │ │ │ │ ├── materialize-input-file.html │ │ │ │ └── materialize-input-file.ts │ │ │ ├── materialize.module.ts │ │ │ ├── modals │ │ │ │ ├── body-modal.html │ │ │ │ ├── body-modal.ts │ │ │ │ ├── chart-modal.html │ │ │ │ ├── chart-modal.ts │ │ │ │ ├── materialize-modal.ts │ │ │ │ ├── type-modal.html │ │ │ │ └── type.modal.ts │ │ │ └── select │ │ │ │ ├── materialize-select.html │ │ │ │ ├── materialize-select.ts │ │ │ │ ├── multiple-materialize-select.spec.ts │ │ │ │ ├── multiple-materialize-select.ts │ │ │ │ ├── simple-materialize-select.spec.ts │ │ │ │ └── simple-materialize-select.ts │ │ └── shared.module.ts │ ├── pipes │ │ └── pipes.ts │ ├── services │ │ ├── api_http_mocks.ts │ │ ├── apidoc.service.spec.ts │ │ └── apidoc.service.ts │ ├── utils │ │ └── env.config.ts │ └── vendor.ts ├── assets │ ├── icons │ │ └── favicon.ico │ └── styles │ │ ├── chart.css │ │ └── styles.css ├── custom-typings.d.ts ├── index.html └── robots.txt ├── tsconfig.json ├── tslint.json ├── webpack.config.js └── webpack.config.old.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .idea/ 3 | dist/ 4 | typings/ 5 | reports/ 6 | coverage/ 7 | *.iml 8 | src/**/*.js 9 | src/**/*.js.map 10 | *.log 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: required 3 | node_js: 4 | - "4" 5 | script: npm test 6 | services: 7 | - docker 8 | before_install: 9 | - export DISPLAY=:99.0 10 | - sh -e /etc/init.d/xvfb start 11 | - npm i -g npm@^3 12 | - npm install -g karma 13 | - npm install -g typescript 14 | before_script: 15 | - npm install -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Swagger 2 Angular 2 MaterializeCSS [![Build Status](https://travis-ci.org/RedFroggy/swagger2-angular2-materialize.svg?branch=master)](https://travis-ci.org/RedFroggy/swagger2-angular2-materialize) 2 | ========== 3 | 4 | # Presentation 5 | Swagger 2 UI made with Angular 2 and MaterializeCSS 6 | 7 | # Demo 8 | Feel free to try it here: [Swagger 2 UI Demo](http://public.redfroggy.fr/swagger2) 9 | You can try with your Swagger 2 api by setting the url under the "Settings" menu 10 | 11 | # Features 12 | - Responsive application 13 | - Toolbar menu 14 | - Api list 15 | - Api detail 16 | - Api execution 17 | - Possibility to switch between request types (Accept header): for now only application/json and application/xml are supported 18 | - Possibility to switch between response types (Content-Type header): for now only application/json and application/xml are supported 19 | - Dynamic url construction when specifying parameters 20 | - Responses messages listed in table 21 | - Detailed model and sub models information: fields name, types, etc... 22 | - Charts displaying requests time 23 | - Possibility to compare statistics from one api to another 24 | - Possibility to switch between two css themes (settings page) 25 | 26 | # Screenshots 27 | ![image](http://i.imgur.com/2aXJ3TK.png?1) 28 | ![image](http://i.imgur.com/zUvFBFB.png?1) 29 | ![image](http://i.imgur.com/ddWJJgF.png?1) 30 | ![image](http://i.imgur.com/UWzzSii.png?1) 31 | ![image](http://i.imgur.com/WD53mGp.png?1) 32 | ![image](http://i.imgur.com/oqZ3Xec.png?1) 33 | 34 | # Supported browsers 35 | Tested on the following browsers: 36 | - Chrome 37 | - Firefox 38 | - Internet Explorer 11 39 | 40 | # Stack 41 | - [Angular 2.2.1](https://angular.io) 42 | - [MaterializeCSS](http://materializecss.com) 43 | - [ChartJS](http://www.chartjs.org/) 44 | - Webpack 45 | - Karma 46 | - Jasmine 47 | - PhantomJS 48 | 49 | # Project Structure 50 | 51 | ``` 52 | - src: Sources folder 53 | -- app: Application files (TypeScript) 54 | -- boot.ts: Angular2 entry point 55 | -- app.component.ts: Booststrap component 56 | -- app.html: Bootsrap html 57 | -- modules: Angular 2 modules 58 | -- app: application components (home, header and settings page) 59 | -- main: Api List and api detail page, left menu 60 | -- materialize: MaterializeCSS components (modals, inputs, etc..) 61 | -- services: Angular 2 services 62 | -- model: Swagger api typescript definition 63 | -- pipes: Angular2 @Pipe components 64 | -- utils: Utility classes 65 | -- assets: Assets folder 66 | -- icons 67 | -- styles 68 | - typings: TypeScript interfaces for libraries 69 | - node_modules (not in git repository): NPM dependencies 70 | - karma.conf.js: Karma configuration file for unit tests (not yet) 71 | - webpack.test.config.js: Build configuration file used for unit tests (not yet) 72 | - webpack.config.old.js: Build configuration file 73 | - tsconfig.json: TypeScript configuration file 74 | - tslint.json: TSLint configuration file 75 | - typings.json: Typings configuration file 76 | - package.json: For managing npm dependencies and running scripts 77 | - dist: production folder 78 | ``` 79 | 80 | 81 | # Installation 82 | Install the node dependencies: 83 | ```bash 84 | $ npm install 85 | ``` 86 | 87 | # Execution 88 | To start the server: 89 | ```bash 90 | $ npm run start 91 | ``` 92 | Then go to http://localhost:3000 93 | 94 | To build the project: 95 | ```bash 96 | $ npm run build 97 | ``` 98 | 99 | # Contributors 100 | 101 | * Maximilian Hengelein ([@mhengelein](https://github.com/mhengelein)) 102 | * Francesco Soncina ([@phra](https://github.com/phra)) 103 | * Sahlikhouloud ([@Sahlikhouloud](https://github.com/Sahlikhouloud)) 104 | * Wiem Zine El Abidine ([@wi101](https://github.com/wi101)) -------------------------------------------------------------------------------- /config/helpers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by mdesigaud on 16/11/2016. 3 | */ 4 | 5 | var path = require('path'); 6 | var _root = path.resolve(__dirname, '..'); 7 | function root(args) { 8 | args = Array.prototype.slice.call(arguments, 0); 9 | return path.join.apply(path, [_root].concat(args)); 10 | } 11 | exports.root = root; -------------------------------------------------------------------------------- /config/karma-test-shim.js: -------------------------------------------------------------------------------- 1 | /* 2 | * When testing with webpack and ES6, we have to do some extra 3 | * things get testing to work right. Because we are gonna write test 4 | * in ES6 to, we have to compile those as well. That's handled in 5 | * karma.conf.js with the karma-webpack plugin. This is the entry 6 | * file for webpack test. Just like webpack will create a bundle.js 7 | * file for our client, when we run test, it well compile and bundle them 8 | * all here! Crazy huh. So we need to do some setup 9 | */ 10 | Error.stackTraceLimit = Infinity; 11 | 12 | 13 | // Prefer CoreJS over the polyfills above 14 | require('core-js/es6'); 15 | require('core-js/es7/reflect'); 16 | 17 | require('zone.js/dist/zone.js'); 18 | require('zone.js/dist/long-stack-trace-zone.js'); 19 | require('zone.js/dist/async-test.js'); 20 | require('zone.js/dist/fake-async-test.js'); 21 | require('zone.js/dist/sync-test.js'); 22 | require('zone.js/dist/proxy.js'); 23 | require('zone.js/dist/jasmine-patch.js'); 24 | 25 | // RxJS 26 | require('rxjs/Rx'); 27 | 28 | var testing = require('@angular/core/testing'); 29 | var browser = require('@angular/platform-browser-dynamic/testing'); 30 | 31 | testing.TestBed.initTestEnvironment( 32 | browser.BrowserDynamicTestingModule, 33 | browser.platformBrowserDynamicTesting() 34 | ); 35 | 36 | /* 37 | Ok, this is kinda crazy. We can use the the context method on 38 | require that webpack created in order to tell webpack 39 | what files we actually want to require or import. 40 | Below, context will be an function/object with file names as keys. 41 | using that regex we are saying look in ./src/app and ./test then find 42 | any file that ends with spec.js and get its path. By passing in true 43 | we say do this recursively 44 | */ 45 | var testContext = require.context('../src', true, /\.spec\.ts/); 46 | 47 | // get all the files, for each file, call the context function 48 | // that will require the file and load it up here. Context will 49 | // loop and require those spec files here 50 | function requireAll(requireContext) { 51 | return requireContext.keys().map(requireContext); 52 | } 53 | 54 | var modules = requireAll(testContext); 55 | // requires and returns all modules that match -------------------------------------------------------------------------------- /config/karma.conf.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Karma configuration file 3 | * Created by Michael DESIGAUD on 14/04/2016. 4 | */ 5 | 6 | var path = require('path'); 7 | 8 | module.exports = function(config) { 9 | var testWebpackConfig = require('./webpack.test.config.js'); 10 | config.set({ 11 | 12 | // base path that will be used to resolve all patterns (e.g. files, exclude) 13 | basePath: '', 14 | 15 | // frameworks to use 16 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 17 | frameworks: ['jasmine'], 18 | 19 | // list of files to exclude 20 | exclude: [ ], 21 | 22 | reporters: ['junit','html', 'coverage'], 23 | 24 | junitReporter: { 25 | outputDir: 'reports/junit', 26 | outputFile: 'test-results.xml', 27 | useBrowserName: false 28 | }, 29 | htmlReporter: { 30 | outputDir: 'reports/html', // where to put the reports 31 | templatePath: null, // set if you moved jasmine_template.html 32 | focusOnFailures: true, // reports show failures on start 33 | namedFiles: false, // name files instead of creating sub-directories 34 | pageTitle: null, // page title for reports; browser info by default 35 | urlFriendlyName: false, // simply replaces spaces with _ for files/dirs 36 | reportName: 'html_report', // report summary filename; browser info by default 37 | // experimental 38 | preserveDescribeNesting: false, // folded suites stay folded 39 | foldAll: false // reports start folded (only with preserveDescribeNesting) 40 | }, 41 | 42 | // list of files / patterns to load in the browser 43 | // we are building the test environment in ./karma-test-shim.js 44 | files: [ 45 | { pattern: 'node_modules/jquery/dist/jquery.js', watched: false}, 46 | { pattern: 'node_modules/materialize-css/dist/js/materialize.js', watched: false}, 47 | { pattern: 'config/karma-test-shim.js', watched: false } 48 | ], 49 | 50 | // preprocess matching files before serving them to the browser 51 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 52 | preprocessors: { 53 | 'config/karma-test-shim.js': ['coverage', 'webpack', 'sourcemap'] 54 | }, 55 | 56 | // Webpack Config at ./webpack.test.config.js 57 | webpack: testWebpackConfig, 58 | 59 | coverageReporter: { 60 | dir : 'coverage/', 61 | reporters: [ 62 | { type: 'text-summary', subdir: 'report-text' }, 63 | { type: 'json', subdir: 'report-json' }, 64 | { type: 'html', subdir: 'report-html' }, 65 | { type: 'cobertura', subdir: 'report-cobertura' }, 66 | { type: 'lcovonly', subdir: 'report-lcov', file: 'coverage.lcov' } 67 | ] 68 | }, 69 | 70 | // Webpack please don't spam the console when running in karma! 71 | webpackServer: { noInfo: true }, 72 | 73 | // test results reporter to use 74 | // possible values: 'dots', 'progress' 75 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 76 | //reporters: [ 'mocha', 'coverage' ], 77 | 78 | // web server port 79 | port: 9876, 80 | 81 | // enable / disable colors in the output (reporters and logs) 82 | colors: true, 83 | 84 | // level of logging 85 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 86 | logLevel: config.LOG_DEBUG, 87 | 88 | // enable / disable watching file and executing tests whenever any file changes 89 | autoWatch: true, 90 | 91 | // start these browsers 92 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 93 | browsers: [ 94 | 'PhantomJS' 95 | ], 96 | 97 | // Continuous Integration mode 98 | // if true, Karma captures browsers, runs the tests and exits 99 | singleRun: true 100 | }); 101 | 102 | }; -------------------------------------------------------------------------------- /config/webpack.common.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Webpack common configuration 3 | * Created by mdesigaud on 16/11/2016. 4 | */ 5 | var webpack = require('webpack'); 6 | var HtmlWebpackPlugin = require('html-webpack-plugin'); 7 | var ExtractTextPlugin = require('extract-text-webpack-plugin'); 8 | var helpers = require('./helpers'); 9 | 10 | module.exports = { 11 | entry: { 12 | 'vendor': './src/app/vendor.ts', 13 | 'app': './src/app/boot.ts' 14 | }, 15 | 16 | resolve: { 17 | extensions: ['','.ts', '.js'] 18 | }, 19 | 20 | module: { 21 | preLoaders: [ 22 | // Generate source map for debugging 23 | { 24 | test: /\.js$/, 25 | loader: 'source-map-loader', 26 | exclude: [helpers.root('./node_modules/rxjs'), helpers.root('./node_modules/jquery')] 27 | } 28 | ], 29 | loaders: [ 30 | { 31 | test: /\.ts$/, 32 | loaders: ['awesome-typescript-loader', 'angular2-template-loader'], 33 | exclude: [/\.(spec|e2e)\.ts$/] 34 | }, 35 | { 36 | test: /\.json$/, loader: 'json-loader' 37 | }, 38 | { 39 | test: /\.html$/, 40 | loader: 'html' 41 | }, 42 | { 43 | test: /\.(png|jpe?g|gif|svg|woff|woff2|ttf|eot|ico)(\?v=[0-9]\.[0-9]\.[0-9])?$/, 44 | loader: 'file?name=assets/[name].[hash].[ext]?limit=100000' 45 | }, 46 | { 47 | test: /\.css$/, 48 | exclude: helpers.root('src', 'app'), 49 | loader: ExtractTextPlugin.extract('style', 'css?sourceMap') 50 | }, 51 | { 52 | test: /\.css$/, 53 | include: helpers.root('src', 'app'), 54 | loader: 'raw' 55 | }, 56 | { 57 | test: /materialize-css\/bin\//, 58 | loader: 'imports?jQuery=jquery, $=jquery' 59 | } 60 | ] 61 | }, 62 | 63 | plugins: [ 64 | new webpack.optimize.CommonsChunkPlugin({name: ['app', 'vendor']}), 65 | new HtmlWebpackPlugin({template: 'src/index.html'}), 66 | new webpack.ProvidePlugin({ 67 | "$":'jquery', 68 | "jQuery":'jquery', 69 | "window.jQuery": "jquery", 70 | "root.jQuery": "jquery", 71 | "hljs":'highlight.js/lib/highlight' 72 | }) 73 | ] 74 | }; -------------------------------------------------------------------------------- /config/webpack.dev.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Webpack dev configuration 3 | * Created by mdesigaud on 16/11/2016. 4 | */ 5 | var webpackMerge = require('webpack-merge'); 6 | var ExtractTextPlugin = require('extract-text-webpack-plugin'); 7 | var commonConfig = require('./webpack.common.config.js'); 8 | var helpers = require('./helpers'); 9 | 10 | module.exports = webpackMerge(commonConfig, { 11 | devtool: 'cheap-module-eval-source-map', 12 | 13 | output: { 14 | path: helpers.root('dist'), 15 | publicPath: '', 16 | filename: '[name].js', 17 | chunkFilename: '[id].chunk.js' 18 | }, 19 | 20 | plugins: [ 21 | new ExtractTextPlugin('[name].css') 22 | ], 23 | 24 | devServer: { 25 | historyApiFallback: true, 26 | stats: 'minimal' 27 | } 28 | }); -------------------------------------------------------------------------------- /config/webpack.prod.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Webpack prod configuration 3 | * Created by mdesigaud on 16/11/2016. 4 | */ 5 | var webpack = require('webpack'); 6 | var webpackMerge = require('webpack-merge'); 7 | var ExtractTextPlugin = require('extract-text-webpack-plugin'); 8 | var CleanWebpackPlugin = require('clean-webpack-plugin'); 9 | var commonConfig = require('./webpack.common.config.js'); 10 | var helpers = require('./helpers'); 11 | 12 | const ENV = process.env.NODE_ENV = process.env.ENV = 'production'; 13 | 14 | module.exports = webpackMerge(commonConfig, { 15 | devtool: 'source-map', 16 | debug: false, 17 | output: { 18 | path: helpers.root('dist'), 19 | publicPath: '', 20 | filename: '[name].[hash].js', 21 | chunkFilename: '[id].[hash].chunk.js' 22 | }, 23 | 24 | htmlLoader: { 25 | minimize: false // workaround for ng2 26 | }, 27 | 28 | plugins: [ 29 | // Clean dist directory 30 | new CleanWebpackPlugin([helpers.root('dist')]), 31 | new webpack.NoErrorsPlugin(), 32 | new webpack.optimize.DedupePlugin(), 33 | /*new webpack.optimize.UglifyJsPlugin({ // Does not work for now 34 | mangle: { 35 | keep_fnames: true 36 | } 37 | }),*/ 38 | new ExtractTextPlugin('[name].[hash].css'), 39 | new webpack.DefinePlugin({ 40 | 'process.env': { 41 | 'ENV': JSON.stringify(ENV) 42 | } 43 | }) 44 | ] 45 | }); -------------------------------------------------------------------------------- /config/webpack.test.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Webpack test configuration 3 | * Created by mdesigaud on 17/11/2016. 4 | */ 5 | var helpers = require('./helpers'); 6 | 7 | module.exports = { 8 | devtool: 'inline-source-map', 9 | 10 | resolve: { 11 | extensions: ['', '.ts', '.js'] 12 | }, 13 | 14 | module: { 15 | loaders: [ 16 | { 17 | test: /\.ts$/, 18 | loaders: ['awesome-typescript-loader', 'angular2-template-loader'] 19 | }, 20 | { 21 | test: /\.html$/, 22 | loader: 'html' 23 | 24 | }, 25 | { 26 | test: /\.(png|jpe?g|gif|svg|woff|woff2|ttf|eot|ico)$/, 27 | loader: 'null' 28 | }, 29 | { 30 | test: /\.css$/, 31 | exclude: helpers.root('src', 'app'), 32 | loader: 'null' 33 | }, 34 | { 35 | test: /\.css$/, 36 | include: helpers.root('src', 'app'), 37 | loader: 'raw' 38 | } 39 | ] 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Main karma configuration file 3 | * Created by mdesigaud on 17/11/2016. 4 | */ 5 | module.exports = require('./config/karma.conf.js'); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular2-swagger2", 3 | "version": "1.1.0", 4 | "description": "Swagger 2 api documentation with Angular 2 and MaterializeCSS", 5 | "author": "Michael DESIGAUD ", 6 | "license": "MIT", 7 | "engines": { 8 | "node": ">= 4.2.1", 9 | "npm": ">= 3" 10 | }, 11 | "keywords": [ 12 | "angular2", 13 | "materialize", 14 | "swagger" 15 | ], 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/RedFroggy/swagger2-angular2-materialize.git" 19 | }, 20 | "bugs": { 21 | "url": "https://github.com/RedFroggy/swagger2-angular2-materialize/issues" 22 | }, 23 | "devDependencies": { 24 | "@types/chai": "*", 25 | "@types/core-js": "*", 26 | "@types/highlight.js": "*", 27 | "@types/jasmine": "*", 28 | "@types/jquery": "*", 29 | "@types/lodash": "*", 30 | "@types/materialize-css": "0.97.32", 31 | "@types/node": "*", 32 | "angular2-template-loader": "0.6.0", 33 | "autoprefixer": "^6.5.3", 34 | "awesome-typescript-loader": "2.2.4", 35 | "clean-webpack-plugin": "^0.1.14", 36 | "codelyzer": "^1.0.0-beta.4", 37 | "compression-webpack-plugin": "^0.3.2", 38 | "concurrently": "^3.1.0", 39 | "copy-webpack-plugin": "^4.0.1", 40 | "css-loader": "^0.25.0", 41 | "es6-promise": "^4.0.5", 42 | "es6-promise-loader": "^1.0.2", 43 | "es6-shim": "^0.35.1", 44 | "es7-reflect-metadata": "^1.6.0", 45 | "extract-text-webpack-plugin": "^1.0.1", 46 | "file-loader": "^0.9.0", 47 | "hammerjs": "^2.0.8", 48 | "html-loader": "^0.4.4", 49 | "html-webpack-plugin": "^2.24.1", 50 | "ie-shim": "^0.1.0", 51 | "ignore-styles": "^5.0.1", 52 | "istanbul-instrumenter-loader": "^1.0.0", 53 | "jasmine-core": "^2.5.2", 54 | "json-loader": "^0.5.4", 55 | "karma": "^1.3.0", 56 | "karma-coverage": "^1.1.1", 57 | "karma-html-reporter": "^0.2.7", 58 | "karma-jasmine": "^1.0.2", 59 | "karma-junit-reporter": "^1.1.0", 60 | "karma-phantomjs-launcher": "^1.0.2", 61 | "karma-sourcemap-loader": "^0.3.7", 62 | "karma-webpack": "^1.8.0", 63 | "phantomjs-prebuilt": "^2.1.13", 64 | "postcss-loader": "^1.1.1", 65 | "precss": "^1.4.0", 66 | "raw-loader": "^0.5.1", 67 | "resolve-url": "^0.2.1", 68 | "source-map-loader": "^0.1.5", 69 | "style-loader": "^0.13.1", 70 | "tslint": "3.15.1", 71 | "tslint-loader": "^2.1.3", 72 | "typescript": "2.0.9", 73 | "url-loader": "^0.5.7", 74 | "webpack": "^1.13.3", 75 | "webpack-dev-server": "^1.16.2", 76 | "webpack-merge": "^0.17.0" 77 | }, 78 | "dependencies": { 79 | "@angular/common": "2.2.1", 80 | "@angular/compiler": "2.2.1", 81 | "@angular/core": "2.2.1", 82 | "@angular/forms": "2.2.1", 83 | "@angular/http": "2.2.1", 84 | "@angular/platform-browser": "2.2.1", 85 | "@angular/platform-browser-dynamic": "2.2.1", 86 | "@angular/platform-server": "2.2.1", 87 | "@angular/router": "3.2.1", 88 | "@angular/upgrade": "2.2.1", 89 | "bootstrap": "3.3.7", 90 | "chart.js": "^2.4.0", 91 | "core-js": "^2.4.1", 92 | "font-awesome": "4.7.0", 93 | "highlight.js": "9.8.0", 94 | "jquery": "2.1.4", 95 | "lodash": "^4.17.1", 96 | "materialize-css": "^0.97.8", 97 | "moment": "^2.16.0", 98 | "reflect-metadata": "0.1.8", 99 | "rxjs": "5.0.0-beta.12", 100 | "velocity-animate": "^1.3.1", 101 | "vkbeautify": "0.99.1", 102 | "x2js": "^3.0.1", 103 | "zone.js": "^0.6.26", 104 | "rimraf": "2.5.4" 105 | }, 106 | "bin": { 107 | "webpack": "node_modules/webpack/bin/webpack.js", 108 | "rimraf": "node_modules/rimraf/rimraf.js" 109 | }, 110 | "scripts": { 111 | "start": "npm run server:dev", 112 | "server": "npm run server:dev", 113 | "server:dev": "webpack-dev-server --config webpack.config.js --port=3000 --progress --inline --content-base src/", 114 | "build": "npm run build:dev", 115 | "build:dev": "rimraf dist && webpack --config config/webpack.dev.config.js --progress --profile --bail", 116 | "build:prod": "rimraf dist && webpack --config config/webpack.prod.config.js --progress --profile --bail", 117 | "test": "karma start" 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/app/boot.ts: -------------------------------------------------------------------------------- 1 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 2 | import {enableProdMode} from '@angular/core'; 3 | import {AppModule} from './modules/app/app.module'; 4 | 5 | import 'rxjs/add/operator/map'; 6 | 7 | // Styles css 8 | import '../assets/styles/styles.css'; 9 | 10 | function main() { 11 | if (process.env.ENV === 'production') { 12 | enableProdMode(); 13 | } 14 | platformBrowserDynamic().bootstrapModule(AppModule); 15 | } 16 | 17 | if (document.readyState === 'complete') { 18 | main(); 19 | } else { 20 | 21 | document.addEventListener('DOMContentLoaded', main); 22 | } 23 | -------------------------------------------------------------------------------- /src/app/model/api-definition.ts: -------------------------------------------------------------------------------- 1 | import {InfoObject, PathsObject, DefinitionsObject, ResponsesDefinitionsObject, 2 | SecurityDefinitionsObject, SecurityRequirementObject, TagObject, 3 | ExternalDocumentationObject, ResponseObject} from './apidoc'; 4 | import {ApiModelUtils} from './api-utils'; 5 | import {OperationObject} from './api-operation'; 6 | import {IJsonSchema} from './api-json-schema'; 7 | import {ParametersDefinitionsObject, ParameterObject} from './api-parameter'; 8 | import * as _ from 'lodash'; 9 | 10 | export class ApiDefinition { 11 | swagger: string; 12 | info: InfoObject; 13 | host: string; 14 | basePath: string; 15 | schemes: string[]; 16 | consumes: string[]; 17 | produces: string[]; 18 | paths: Array; 19 | definitions: Array; 20 | parameters: ParametersDefinitionsObject; 21 | responses: ResponsesDefinitionsObject; 22 | securityDefinitions: SecurityDefinitionsObject; 23 | security: SecurityRequirementObject[]; 24 | tags: TagObject[]; 25 | externalDocs: ExternalDocumentationObject; 26 | baseUrl: string; 27 | constructor(_apiDoc?: any) { 28 | this.info = new InfoObject(); 29 | this.paths = []; 30 | this.produces = []; 31 | this.consumes = []; 32 | this.schemes = []; 33 | this.definitions = []; 34 | this.parameters = new ParametersDefinitionsObject(); 35 | this.responses = new ResponsesDefinitionsObject(); 36 | this.securityDefinitions = new SecurityDefinitionsObject(); 37 | this.externalDocs = new ExternalDocumentationObject(); 38 | this.security = []; 39 | this.tags = []; 40 | 41 | if (_apiDoc) { 42 | Object.assign(this, _apiDoc); 43 | 44 | // TODO config 45 | this.baseUrl = 'http://' + this.host; 46 | if (this.basePath) { 47 | this.baseUrl += this.basePath; 48 | } 49 | 50 | if (_apiDoc.info) { 51 | this.info = new InfoObject(_apiDoc.info); 52 | } 53 | if (_apiDoc.paths) { 54 | this.paths = []; 55 | Object.keys(_apiDoc.paths).forEach((key: string) => { 56 | this.paths.push(new PathsObject(key, _apiDoc.paths[key])); 57 | }); 58 | } 59 | if (_apiDoc.definitions) { 60 | this.definitions = []; 61 | Object.keys(_apiDoc.definitions).forEach((name: string) => { 62 | this.definitions.push(new DefinitionsObject(name, _apiDoc.definitions[name])); 63 | }); 64 | 65 | } 66 | if (_apiDoc.tags) { 67 | this.tags = []; 68 | _apiDoc.tags.forEach((tag: any) => { 69 | this.tags.push(new TagObject(tag)); 70 | }); 71 | } 72 | if (_apiDoc.externalDocs) { 73 | this.externalDocs = new ExternalDocumentationObject(_apiDoc.externalDocs); 74 | } 75 | } 76 | } 77 | getDefinitionByEntity(entity: string): DefinitionsObject { 78 | return this.definitions.find((definition: DefinitionsObject) => { 79 | return definition.name === entity; 80 | }); 81 | } 82 | hasDefinition(type: string, toEntityName = false): boolean { 83 | if (toEntityName) { 84 | type = this.getEntityName(type); 85 | } 86 | if (!type) { 87 | return false; 88 | } 89 | let definition: DefinitionsObject = this.getDefinitionByEntity(type); 90 | return definition && ApiModelUtils.isObject(definition.schema.type); 91 | } 92 | getEntityName(name: string): string { 93 | return ApiModelUtils.extractEntityName(name); 94 | } 95 | isDtoType(item: ResponseObject|ParameterObject): boolean { 96 | if (!this.isTypeArray(item)) { 97 | return item && item.schema && ApiModelUtils.hasRef(item.schema) && this.hasDefinition(item.schema.entity); 98 | } 99 | return item && item.schema && item.schema.items 100 | && ApiModelUtils.hasRef(item.schema.items) 101 | && this.hasDefinition(item.schema.items.entity); 102 | } 103 | getDtoType(item: ResponseObject|ParameterObject): string { 104 | if (item && item.schema) { 105 | if (item.schema.entity) { 106 | return item.schema.entity; 107 | } 108 | if (item.schema.items && item.schema.items.entity) { 109 | return item.schema.items.entity; 110 | } 111 | } 112 | if (item && item.items) { 113 | return item.items['type']; 114 | } 115 | } 116 | isTypeArray(item: any): boolean { 117 | return ApiModelUtils.isTypeArray(item); 118 | } 119 | getStatusClass(status: number): string { 120 | if (status >= 200 && status < 300) { 121 | return 'green darken-2'; 122 | } 123 | return ' red darken-2'; 124 | } 125 | getBodyDescription(entityName: string, isXml: boolean): any { 126 | let definition: DefinitionsObject = this.getDefinitionByEntity(entityName); 127 | let body: any = {}; 128 | if (definition) { 129 | Object.keys(definition.schema.properties).forEach((name: string) => { 130 | let property: IJsonSchema = definition.schema.properties[name]; 131 | let bodyValue: any; 132 | if (!ApiModelUtils.isArray(property.type) && !ApiModelUtils.isObject(property.type)) { 133 | if (property.type === 'integer') { 134 | bodyValue = 0; 135 | } else if (property.enum && !_.isEmpty(property.enum)) { 136 | bodyValue = property.enum[0]; 137 | } else if (property.type === 'string') { 138 | if (property.format === 'date-time') { 139 | bodyValue = new Date().toISOString(); 140 | } else { 141 | bodyValue = property.example ? property.example : 'string'; 142 | } 143 | } else if (property.type === 'boolean') { 144 | bodyValue = property.default ? property.default : true; 145 | } else if (property.$ref) { 146 | bodyValue = this.getBodyDescription(this.getEntityName(property.$ref), isXml); 147 | if (isXml) { 148 | name = Object.keys(bodyValue)[0]; 149 | bodyValue = bodyValue[name]; 150 | } 151 | } 152 | } else if (ApiModelUtils.isArray(property.type)) { 153 | if (property.items.type === 'string') { 154 | bodyValue = ['string']; 155 | } else if (property.items.$ref) { 156 | bodyValue = [this.getBodyDescription(this.getEntityName(property.items.$ref), isXml)]; 157 | if (isXml && property.xml.wrapped) { 158 | name = property.xml.name; 159 | } 160 | } 161 | } 162 | body[name] = bodyValue; 163 | }); 164 | if (isXml && definition.schema.xml) { 165 | let xmlBody: any = {}; 166 | xmlBody[definition.schema.xml.name] = body; 167 | return xmlBody; 168 | } 169 | } 170 | return body; 171 | } 172 | getOperationsByProperty(values: Array, property: string): Array { 173 | let operations: Array = []; 174 | if (values) { 175 | this.paths.forEach((path: PathsObject) => { 176 | let pathOperations: Array = path.path.operations.filter((operation: OperationObject) => { 177 | return values.indexOf(operation[property]) !== -1; 178 | }); 179 | if (!_.isEmpty(pathOperations)) { 180 | operations = operations.concat(pathOperations); 181 | } 182 | }); 183 | } 184 | return operations; 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/app/model/api-json-schema.ts: -------------------------------------------------------------------------------- 1 | export interface IJsonSchema { 2 | id?: string; 3 | example?: string; 4 | $schema?: string; 5 | $ref?: string; 6 | title?: string; 7 | description?: string; 8 | multipleOf?: number; 9 | maximum?: number; 10 | exclusiveMaximum?: boolean; 11 | minimum?: number; 12 | exclusiveMinimum?: boolean; 13 | maxLength?: number; 14 | minLength?: number; 15 | pattern?: string; 16 | additionalItems?: boolean | IJsonSchema; 17 | items?: IJsonSchema; 18 | maxItems?: number; 19 | minItems?: number; 20 | uniqueItems?: boolean; 21 | maxProperties?: number; 22 | minProperties?: number; 23 | required?: string[]; 24 | additionalProperties?: boolean | IJsonSchema; 25 | definitions?: { 26 | [name: string]: IJsonSchema 27 | }; 28 | properties?: { 29 | [name: string]: IJsonSchema 30 | }; 31 | patternProperties?: { 32 | [name: string]: IJsonSchema 33 | }; 34 | dependencies?: { 35 | [name: string]: IJsonSchema | string[] 36 | }; 37 | 'enum'?: any[]; 38 | type?: string; 39 | format?: string; 40 | default?: boolean; 41 | allOf?: IJsonSchema[]; 42 | anyOf?: IJsonSchema[]; 43 | oneOf?: IJsonSchema[]; 44 | not?: IJsonSchema; 45 | xml?: {name: string, wrapped: boolean}; 46 | } 47 | -------------------------------------------------------------------------------- /src/app/model/api-operation.ts: -------------------------------------------------------------------------------- 1 | 2 | import {ExternalDocumentationObject, SecurityRequirementObject, ResponsesObject, ResponseObject} from './apidoc'; 3 | import {ApiModelUtils} from './api-utils'; 4 | import {ParameterObject} from './api-parameter'; 5 | 6 | import * as _ from 'lodash'; 7 | 8 | const HTTP_METHOD_PATCH = 'PATCH'; 9 | const HTTP_METHOD_POST = 'POST'; 10 | const HTTP_METHOD_PUT = 'PUT'; 11 | const HTTP_METHOD_GET = 'GET'; 12 | const HTTP_METHOD_DELETE = 'DELETE'; 13 | 14 | const APPLICATION_FORM_URL_ENCODED = 'app/x-www-form-urlencoded'; 15 | const MULTIPART_FORM_DATA = 'multipart/form-data'; 16 | const APPLICATION_JSON = 'application/json'; 17 | const APPLICATION_XML = 'application/xml'; 18 | 19 | const METHOD_CLASS: Object = { 20 | GET: 'grey lighten-1', 21 | POST: 'teal lighten-2', 22 | PUT: 'yellow darken-2', 23 | DELETE: 'red lighten-2', 24 | PATCH: 'light-blue lighten-2', 25 | HEAD: 'pink lighten-2' 26 | }; 27 | 28 | export class OperationObject { 29 | name: string; 30 | path: string; 31 | tags: string[]; 32 | summary: string; 33 | description: string; 34 | externalDocs: ExternalDocumentationObject; 35 | operationId: string; 36 | consumes: string[]; 37 | produces: string[]; 38 | parameters: (ParameterObject)[]; 39 | responses: Array; 40 | schemes: string[]; 41 | deprecated: boolean; 42 | security: SecurityRequirementObject[]; 43 | originalData: any; 44 | dataJson: string; 45 | patchJson: string; 46 | consume: {value?: string, selected: string}; 47 | produce: {value?: string, selected: string}; 48 | slug: string; 49 | chartColor: string; 50 | constructor(path?: string, method?: string, _opObj?: any) { 51 | this.responses = []; 52 | this.parameters = []; 53 | this.produces = []; 54 | this.consumes = []; 55 | this.path = path; 56 | this.produce = {selected: APPLICATION_JSON}; 57 | this.consume = {selected: APPLICATION_JSON}; 58 | if (method) { 59 | this.name = method.toUpperCase(); 60 | } 61 | if (_opObj) { 62 | Object.assign(this, _opObj); 63 | this.slug = btoa(this.name + this.path + this.operationId); 64 | if (_opObj.externalDocs) { 65 | this.externalDocs = new ExternalDocumentationObject(_opObj.externalDocs); 66 | } 67 | if (_opObj.responses) { 68 | this.responses = []; 69 | Object.keys(_opObj.responses).forEach((code: string) => { 70 | this.responses.push(new ResponsesObject(code, _opObj.responses)); 71 | }); 72 | } 73 | if (_opObj.parameters) { 74 | this.parameters = []; 75 | _opObj.parameters.forEach((param: any) => { 76 | this.parameters.push(new ParameterObject(param)); 77 | }); 78 | } 79 | if (_opObj.produces && !_.isEmpty(this.produces)) { 80 | this.produce = {selected: this.produces[0]}; 81 | } 82 | if (_opObj.consumes && !_.isEmpty(this.consumes)) { 83 | this.consume = {selected: this.consumes[0]}; 84 | } 85 | } 86 | } 87 | getMethodClass(): string { 88 | if (this.name) { 89 | return METHOD_CLASS[this.name]; 90 | } 91 | } 92 | getResponseByCode(code: string): ResponseObject { 93 | let respObj: ResponsesObject = this.responses.find((resp: ResponsesObject) => { 94 | return resp.code === code; 95 | }); 96 | 97 | if (respObj) { 98 | return respObj.response; 99 | } 100 | } 101 | getRequestUrl(onlyParameters = false): string { 102 | let url: string = !onlyParameters ? this.path : ''; 103 | 104 | if (this.parameters.length > 0) { 105 | this.parameters.forEach((param: ParameterObject) => { 106 | if (param.value && param.value.selected) { 107 | if (param.isPathParam()) { 108 | url = url.replace(new RegExp('{' + param.name + '}'), param.value.selected); 109 | } else if (param.isQueryParam()) { 110 | url += url.indexOf('?') === -1 ? '?' + param.name + '=' 111 | + param.value.selected : '&' + param.name + '=' + param.value.selected; 112 | } 113 | } 114 | }); 115 | } 116 | return url; 117 | } 118 | isPatchMethod(): boolean { 119 | return this.name === HTTP_METHOD_PATCH; 120 | } 121 | isPostMethod(): boolean { 122 | return this.name === HTTP_METHOD_POST; 123 | } 124 | isPutMethod(): boolean { 125 | return this.name === HTTP_METHOD_PUT; 126 | } 127 | isWriteMethod(): boolean { 128 | return this.isPatchMethod() || this.isPostMethod() || this.isPutMethod(); 129 | } 130 | isGetMethod(): boolean { 131 | return this.name === HTTP_METHOD_GET; 132 | } 133 | isDeleteMethod(): boolean { 134 | return this.name === HTTP_METHOD_DELETE; 135 | } 136 | isProduceJson(): boolean { 137 | return ApiModelUtils.isType(this.produce, APPLICATION_JSON); 138 | } 139 | isProduceXml(): boolean { 140 | return ApiModelUtils.isType(this.produce, APPLICATION_XML); 141 | } 142 | isConsumeJson(): boolean { 143 | return ApiModelUtils.isType(this.consume, APPLICATION_JSON); 144 | } 145 | isConsumeXml(): boolean { 146 | return ApiModelUtils.isType(this.consume, APPLICATION_XML); 147 | } 148 | isConsumeFormUrlEncoded(): boolean { 149 | return ApiModelUtils.isType(this.consume, APPLICATION_FORM_URL_ENCODED); 150 | } 151 | isConsumeMultipartFormData(): boolean { 152 | return ApiModelUtils.isType(this.consume, MULTIPART_FORM_DATA); 153 | } 154 | getMapProduces(): {value: string}[] { 155 | return ApiModelUtils.getSelectMap(this.produces); 156 | } 157 | getMapConsumes(): {value: string}[] { 158 | return ApiModelUtils.getSelectMap(this.consumes); 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/app/model/api-parameter.ts: -------------------------------------------------------------------------------- 1 | import {ItemsObject, ReferenceObject} from './apidoc'; 2 | import {ApiModelUtils} from './api-utils'; 3 | 4 | import * as _ from 'lodash'; 5 | 6 | const TYPE_FILE = 'file'; 7 | const TYPE_DATE = 'date'; 8 | const PATH_PARAM = 'path'; 9 | const QUERY_PARAM = 'query'; 10 | const BODY_PARAM = 'body'; 11 | const FORM_PARAM = 'formData'; 12 | const HEADER_PARAM = 'header'; 13 | 14 | export class ParametersDefinitionsObject { 15 | [index: string]: ParameterObject; 16 | } 17 | 18 | export class ParameterObject { 19 | name: string; 20 | 'in': string; 21 | description: string; 22 | required: boolean; 23 | value: {selected: any}; 24 | schema: ReferenceObject; 25 | collectionFormat: string; 26 | items: ItemsObject; 27 | type: string; 28 | constructor(_paramObj?: any) { 29 | this.items = new ItemsObject(); 30 | this.value = {selected: ''}; 31 | if (_paramObj) { 32 | Object.assign(this, _paramObj); 33 | if (_paramObj.schema) { 34 | this.schema = new ReferenceObject(_paramObj.schema); 35 | } 36 | if (_paramObj.items) { 37 | this.items = new ItemsObject(_paramObj.items); 38 | } 39 | } 40 | } 41 | isHeaderParam(): boolean { 42 | return this.in === HEADER_PARAM; 43 | } 44 | isPathParam(): boolean { 45 | return this.in === PATH_PARAM; 46 | } 47 | isQueryParam(): boolean { 48 | return this.in === QUERY_PARAM; 49 | } 50 | isBodyParam(): boolean { 51 | return this.in === BODY_PARAM; 52 | } 53 | isFormParam(): boolean { 54 | return this.in === FORM_PARAM; 55 | } 56 | isTypeEnum(): boolean { 57 | return this.items.enum && !_.isEmpty(this.items.enum); 58 | } 59 | isTypeFile(): boolean { 60 | return this.type === TYPE_FILE; 61 | } 62 | isTypeDate(): boolean { 63 | return this.type === TYPE_DATE; 64 | } 65 | getParameterType(): string { 66 | if (this.isBodyParam()) { 67 | if (ApiModelUtils.isTypeArray(this)) { 68 | return this.schema.items.entity; 69 | } 70 | return this.schema.entity; 71 | } else if (!ApiModelUtils.isTypeArray(this)) { 72 | return this.type; 73 | } else if (this.isTypeEnum() && this.items.enum.length > 0) { 74 | return 'Enum [' + this.items.enum.join(',') + ']'; 75 | } 76 | return '[' + this.items.type + ']'; 77 | } 78 | getEnumMap(): {value: string}[] { 79 | return this.items.enum.map((enumVal: string) => { 80 | return {value: enumVal, label: enumVal, selected: this.items && this.items.default === enumVal} ; }); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/app/model/api-result.ts: -------------------------------------------------------------------------------- 1 | import {OperationObject} from './api-operation'; 2 | 3 | export class ApiResult { 4 | message: string; 5 | status: number; 6 | date: Date; 7 | endDate: Date; 8 | operation: OperationObject; 9 | constructor() { 10 | this.date = new Date(); 11 | this.operation = new OperationObject(); 12 | } 13 | getRequestTime(): number { 14 | if (this.date && this.endDate) { 15 | return this.endDate.getTime() - this.date.getTime(); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/app/model/api-utils.ts: -------------------------------------------------------------------------------- 1 | 2 | const TYPE_DEFINITION = '#/definitions/'; 3 | const TYPE_ARRAY = 'array'; 4 | const TYPE_OBJECT = 'object'; 5 | 6 | export class ApiModelUtils { 7 | static extractEntityName(definition: string) { 8 | if (definition) { 9 | return definition.replace(TYPE_DEFINITION, ''); 10 | } 11 | } 12 | static isArray(type: any): boolean { 13 | return type && type === TYPE_ARRAY; 14 | } 15 | static isObject(type: string): boolean { 16 | return type === TYPE_OBJECT; 17 | } 18 | static hasRef(obj: any): boolean { 19 | return !!obj.$ref; 20 | } 21 | static isTypeArray(item: any): boolean { 22 | return (item && this.isArray(item.type)) 23 | || (item.schema && item.schema.type && this.isArray(item.schema.type)); 24 | } 25 | static getSelectMap(items: Array): {value: string}[] { 26 | return items.map((item: string) => {return {value: item, label: item}; }); 27 | } 28 | static isType(item: {selected: string}, type: any): boolean { 29 | return item && item.selected && item.selected === type; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/app/model/apidoc.ts: -------------------------------------------------------------------------------- 1 | import {ApiModelUtils} from './api-utils'; 2 | import {IJsonSchema} from './api-json-schema'; 3 | import {OperationObject} from './api-operation'; 4 | import {ParameterObject} from './api-parameter'; 5 | 6 | 7 | export class InfoObject { 8 | title: string; 9 | description: string; 10 | termsOfService: string; 11 | contact: ContactObject; 12 | license: LicenseObject; 13 | version: string; 14 | constructor(_info?: any) { 15 | this.contact = new ContactObject(); 16 | this.license = new LicenseObject(); 17 | if (_info) { 18 | Object.assign(this, _info); 19 | if (_info.contact) { 20 | this.contact = new ContactObject(_info.contact); 21 | } 22 | if (_info.license) { 23 | this.license = new LicenseObject(_info.license); 24 | } 25 | } 26 | } 27 | } 28 | 29 | export class ContactObject { 30 | name: string; 31 | url: string; 32 | email: string; 33 | constructor(_contact?: any) { 34 | if (_contact) { 35 | Object.assign(this, _contact); 36 | } 37 | } 38 | } 39 | 40 | export class LicenseObject { 41 | name: string; 42 | url: string; 43 | constructor(_license?: any) { 44 | if (_license) { 45 | Object.assign(this, _license); 46 | } 47 | } 48 | } 49 | 50 | export class PathsObject { 51 | name: string; 52 | path: PathItemObject; 53 | selected: boolean; 54 | constructor(name?: string, _pathItem?: any) { 55 | this.name = name; 56 | this.path = new PathItemObject(); 57 | if (_pathItem) { 58 | this.path = new PathItemObject(name, _pathItem); 59 | } 60 | } 61 | } 62 | 63 | export class PathItemObject { 64 | $ref: string; 65 | path: string; 66 | parameters: (ParameterObject)[]; 67 | operations: Array; 68 | constructor(path?: string, _pathItemObj?: any) { 69 | this.path = path; 70 | this.operations = []; 71 | if (_pathItemObj) { 72 | Object.keys(_pathItemObj).forEach((method: string) => { 73 | this.operations.push(new OperationObject(path, method, _pathItemObj[method])); 74 | }); 75 | } 76 | } 77 | } 78 | 79 | export class DefinitionsObject { 80 | name: string; 81 | schema: SchemaObject; 82 | constructor(name?: string, _defObj?: any) { 83 | this.name = name; 84 | this.schema = new SchemaObject(); 85 | if (_defObj) { 86 | this.schema = new SchemaObject(_defObj); 87 | } 88 | } 89 | isRequired(fieldName: string): boolean { 90 | return this.schema.required.indexOf(fieldName) !== -1; 91 | } 92 | } 93 | 94 | export class ResponsesObject { 95 | code: string; 96 | response: ResponseObject; 97 | constructor(code: string, _respObj?: any) { 98 | this.code = code; 99 | if (_respObj) { 100 | this.response = new ResponseObject(_respObj[code]); 101 | } 102 | } 103 | } 104 | 105 | export class ResponsesDefinitionsObject { 106 | [index: string]: ResponseObject; 107 | } 108 | 109 | export class ResponseObject { 110 | description: string; 111 | schema: SchemaObject; 112 | headers: HeadersObject; 113 | examples: ExampleObject; 114 | items: ReferenceObject; 115 | constructor(_respObj?: any) { 116 | if (_respObj) { 117 | Object.assign(this, _respObj); 118 | if (_respObj.schema) { 119 | this.schema = new SchemaObject(_respObj.schema); 120 | } 121 | if (_respObj.examples) { 122 | this.examples = new ExampleObject(_respObj.examples); 123 | } 124 | if (_respObj.headers) { 125 | this.headers = new HeadersObject(_respObj.headers); 126 | } 127 | if (_respObj.items) { 128 | this.items = new ReferenceObject(_respObj.items); 129 | } 130 | } 131 | } 132 | } 133 | 134 | export class HeadersObject { 135 | [index: string]: ItemsObject; 136 | constructor(_headersObj?: any) { 137 | if (_headersObj) { 138 | Object.assign(this, _headersObj); 139 | } 140 | } 141 | } 142 | 143 | export class ExampleObject { 144 | [index: string]: any; 145 | constructor(_exampleObj?: any) { 146 | if (_exampleObj) { 147 | Object.assign(this, _exampleObj); 148 | } 149 | } 150 | } 151 | 152 | export class SecurityDefinitionsObject { 153 | [index: string]: SecuritySchemeObject; 154 | } 155 | 156 | export class SecuritySchemeObject { 157 | type: string; 158 | description: string; 159 | name: string; 160 | 'in': string; 161 | flow: string; 162 | authorizationUrl: string; 163 | tokenUrl: string; 164 | scopes: ScopesObject; 165 | } 166 | 167 | export class ScopesObject { 168 | [index: string]: any; 169 | } 170 | 171 | export class SecurityRequirementObject { 172 | [index: string]: string[]; 173 | } 174 | 175 | export class TagObject { 176 | name: string; 177 | description: string; 178 | externalDocs: ExternalDocumentationObject; 179 | constructor(_tagsObj?: any) { 180 | if (_tagsObj) { 181 | Object.assign(this, _tagsObj); 182 | if (_tagsObj.externalDocs) { 183 | this.externalDocs = new ExternalDocumentationObject(_tagsObj.externalDocs); 184 | } 185 | } 186 | } 187 | } 188 | 189 | export class ItemsObject { 190 | type: string; 191 | format: string; 192 | items: ItemsObject; 193 | collectionFormat: string; 194 | 'default': any; 195 | maximum: number; 196 | exclusiveMaximum: boolean; 197 | minimum: number; 198 | exclusiveMinimum: boolean; 199 | maxLength: number; 200 | minLength: number; 201 | pattern: string; 202 | maxItems: number; 203 | minItems: number; 204 | uniqueItems: boolean; 205 | 'enum': any[]; 206 | multipleOf: number; 207 | constructor(_itemsObject?: any) { 208 | this.enum = []; 209 | if (_itemsObject) { 210 | Object.assign(this, _itemsObject); 211 | if (_itemsObject.items) { 212 | this.items = new ItemsObject(_itemsObject.items); 213 | } 214 | } 215 | } 216 | } 217 | 218 | export class ReferenceObject { 219 | $ref: string; 220 | entity: string; 221 | items: {$ref: string, entity: string}; 222 | type: string; 223 | constructor(_refObj?: any) { 224 | if (_refObj) { 225 | Object.assign(this, _refObj); 226 | if (this.$ref) { 227 | this.entity = ApiModelUtils.extractEntityName(this.$ref); 228 | } 229 | if (this.items && this.items.$ref) { 230 | this.items.entity = ApiModelUtils.extractEntityName(this.items.$ref); 231 | } 232 | } 233 | } 234 | } 235 | 236 | export class ExternalDocumentationObject { 237 | [index: string]: any; 238 | description: string; 239 | url: string; 240 | constructor(_externDocObj?: any) { 241 | if (_externDocObj) { 242 | Object.assign(this, _externDocObj); 243 | } 244 | } 245 | } 246 | 247 | export class SchemaObject implements IJsonSchema { 248 | [index: string]: any; 249 | discriminator: string; 250 | readOnly: boolean; 251 | xml: XMLObject; 252 | externalDocs: ExternalDocumentationObject; 253 | example: any; 254 | items: ReferenceObject; 255 | $ref: string; 256 | entity: string; 257 | type: string; 258 | required: string[]; 259 | properties: { 260 | [name: string]: IJsonSchema; 261 | }; 262 | constructor(_schemaObj?: any) { 263 | this.required = []; 264 | this.properties = {}; 265 | if (_schemaObj) { 266 | Object.assign(this, _schemaObj); 267 | if (_schemaObj.xml) { 268 | this.xml = new XMLObject(_schemaObj.xml); 269 | } 270 | if (_schemaObj.externalDocs) { 271 | this.externalDocs = new ExternalDocumentationObject(_schemaObj.externalDocs); 272 | } 273 | if (_schemaObj.items) { 274 | this.items = new ReferenceObject(_schemaObj.items); 275 | } 276 | if (_schemaObj.$ref) { 277 | this.entity = ApiModelUtils.extractEntityName(this.$ref); 278 | } 279 | } 280 | } 281 | isPropertyTypeArray(value: any): boolean { 282 | return ApiModelUtils.isArray(value.type); 283 | } 284 | getPropertyByName(name: string): string { 285 | if (this.properties[name]) { 286 | return this.properties[name].description; 287 | } 288 | } 289 | } 290 | 291 | export class XMLObject { 292 | [index: string]: any; 293 | name: string; 294 | namespace: string; 295 | prefix: string; 296 | attribute: boolean; 297 | wrapped: boolean; 298 | constructor(_xmlObject?: any) { 299 | if (_xmlObject) { 300 | Object.assign(this, _xmlObject); 301 | } 302 | } 303 | } 304 | -------------------------------------------------------------------------------- /src/app/modules/app/app.component.ts: -------------------------------------------------------------------------------- 1 | 2 | import {Component} from '@angular/core'; 3 | import {ThemeableComponent} from './themeable.component'; 4 | 5 | @Component({ 6 | selector: 'swagger-app', 7 | template: require('./app.html'), 8 | }) 9 | 10 | export class AppComponent extends ThemeableComponent { 11 | constructor() { 12 | super(); 13 | console.log('Application initializing'); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/app/modules/app/app.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 29 | -------------------------------------------------------------------------------- /src/app/modules/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { AppComponent } from './app.component'; 4 | import { RoutesModule } from './app.routing'; 5 | import {HomeComponent} from './home/home'; 6 | import {SettingsComponent} from './settings/settings'; 7 | import {ApiDocService} from '../../services/apidoc.service'; 8 | import {HttpModule} from '@angular/http'; 9 | import {MainModule} from '../main/main.module'; 10 | import {CommonModule, LocationStrategy, HashLocationStrategy} from '@angular/common'; 11 | import {MaterializeModule} from '../materialize/materialize.module'; 12 | import {FormsModule} from '@angular/forms'; 13 | import {HeaderComponent} from './header/header'; 14 | 15 | @NgModule({ 16 | declarations: [AppComponent, HomeComponent, SettingsComponent, HeaderComponent], 17 | imports: [BrowserModule, CommonModule, HttpModule, FormsModule, RoutesModule, MaterializeModule, MainModule], 18 | bootstrap: [AppComponent], 19 | providers: [ApiDocService, {provide: LocationStrategy, useClass: HashLocationStrategy}] 20 | }) 21 | export class AppModule {} 22 | -------------------------------------------------------------------------------- /src/app/modules/app/app.routing.ts: -------------------------------------------------------------------------------- 1 | import {Routes, RouterModule} from '@angular/router'; 2 | import {HomeComponent} from './home/home'; 3 | import {SettingsComponent} from './settings/settings'; 4 | import {ModuleWithProviders} from '@angular/core'; 5 | 6 | const routes: Routes = [ 7 | {path: 'home', component: HomeComponent}, 8 | {path: 'settings', component: SettingsComponent}, 9 | {path: '', redirectTo: '/home', pathMatch: 'full'}, 10 | ]; 11 | 12 | export const RoutesModule: ModuleWithProviders = RouterModule.forRoot(routes); 13 | -------------------------------------------------------------------------------- /src/app/modules/app/header/header.html: -------------------------------------------------------------------------------- 1 | 18 | -------------------------------------------------------------------------------- /src/app/modules/app/header/header.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | import {ApiDocService} from '../../../services/apidoc.service'; 3 | import {Router} from '@angular/router'; 4 | import {ThemeableComponent} from '../themeable.component'; 5 | 6 | @Component({ 7 | selector: 'header', 8 | template: require('./header.html'), 9 | }) 10 | export class HeaderComponent extends ThemeableComponent { 11 | constructor(private router: Router, private apiDocService: ApiDocService) { 12 | super(); 13 | $('#mobileHomeLink').addClass('active'); 14 | } 15 | goToPage($event: Event, ...route: Array): void { 16 | $event.preventDefault(); 17 | 18 | route = 19 | route[0] === 'apis' 20 | ? [...route, 1] 21 | : route; 22 | 23 | this.router.navigate(route); 24 | } 25 | onChangeApi(event: Event): void { 26 | event.preventDefault(); 27 | $('#modal1').openModal(); 28 | } 29 | onCloseModal(event: Event): void { 30 | event.preventDefault(); 31 | $('#modal1').closeModal(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/app/modules/app/home/home.html: -------------------------------------------------------------------------------- 1 |
2 |

{{apiDoc.info.title}}

3 |

{{apiDoc.info.description}}

4 |

Version: {{apiDoc.info.version}}

5 |

link View site


6 |
7 | -------------------------------------------------------------------------------- /src/app/modules/app/home/home.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | import {ApiDocService} from '../../../services/apidoc.service'; 3 | import {ApiDefinition} from '../../../model/api-definition'; 4 | import {ThemeableComponent} from '../themeable.component'; 5 | 6 | @Component({ 7 | selector: 'home', 8 | template: require('./home.html') 9 | }) 10 | export class HomeComponent extends ThemeableComponent { 11 | private apiDoc: ApiDefinition; 12 | constructor(private apiDocService: ApiDocService) { 13 | super(); 14 | this.apiDoc = new ApiDefinition(); 15 | apiDocService.getApi().subscribe((apiDoc: ApiDefinition) => this.apiDoc = apiDoc); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/app/modules/app/settings/settings.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 | 7 | 8 |
9 |
10 |
11 |
12 | Can't read swagger JSON from {{apiUrl}} 13 |
14 |
15 |
16 |
17 | 21 |
22 |
23 |
24 |
25 |
26 |
27 | 31 |
32 |
33 |
34 |
35 |
36 | 39 |
40 |
41 |
42 |
-------------------------------------------------------------------------------- /src/app/modules/app/settings/settings.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | import {ApiDocService} from '../../../services/apidoc.service'; 3 | import {ApiDefinition} from '../../../model/api-definition'; 4 | import * as Config from '../../../utils/env.config'; 5 | import {ThemeableComponent} from '../themeable.component'; 6 | 7 | @Component({ 8 | selector: 'settings', 9 | template: require('./settings.html'), 10 | }) 11 | export class SettingsComponent extends ThemeableComponent { 12 | private chartOptions: any; 13 | private chartType: {selected: string}; 14 | private currentTheme: {selected: string}; 15 | private apiDoc: ApiDefinition; 16 | private apiUrl: string; 17 | private selectedType: string; 18 | private selectedTheme: string; 19 | private themes: Array; 20 | constructor(private apiDocService: ApiDocService) { 21 | super(); 22 | this.chartType = {selected: null}; 23 | this.currentTheme = {selected: null}; 24 | this.chartOptions = [ 25 | {label: 'Line chart', value: Config.CHART_TYPE_LINE, selected: true}, 26 | {label: 'Bar chart', value: Config.CHART_TYPE_BAR} 27 | ]; 28 | 29 | this.apiDoc = new ApiDefinition(); 30 | apiDocService.getApi().subscribe((apiDoc: ApiDefinition) => this.apiDoc = apiDoc); 31 | 32 | if (!localStorage.getItem(Config.LOCAL_STORAGE_CHART_TYPE)) { 33 | localStorage.setItem(Config.LOCAL_STORAGE_CHART_TYPE, this.chartOptions[0].value); 34 | } else { 35 | this.selectedType = localStorage.getItem(Config.LOCAL_STORAGE_CHART_TYPE); 36 | } 37 | 38 | if (!localStorage.getItem(Config.LOCAL_STORAGE_API_URL)) { 39 | this.apiUrl = apiDocService.getDefaultApi(); 40 | } else { 41 | this.apiUrl = localStorage.getItem(Config.LOCAL_STORAGE_API_URL); 42 | } 43 | 44 | this.createThemes(); 45 | } 46 | createThemes(): void { 47 | 48 | this.themes = Config.THEMES; 49 | 50 | if (!localStorage.getItem(Config.LOCAL_STORAGE_THEME)) { 51 | localStorage.setItem(Config.LOCAL_STORAGE_THEME, this.themes[0].value); 52 | } else { 53 | this.selectedTheme = localStorage.getItem(Config.LOCAL_STORAGE_THEME); 54 | } 55 | } 56 | validSettings(): void { 57 | if (this.chartType && this.chartType.selected) { 58 | localStorage.setItem(Config.LOCAL_STORAGE_CHART_TYPE, this.chartType.selected); 59 | } 60 | if (this.apiUrl) { 61 | localStorage.setItem(Config.LOCAL_STORAGE_API_URL, this.apiUrl); 62 | } 63 | if (this.currentTheme && this.currentTheme.selected) { 64 | localStorage.setItem(Config.LOCAL_STORAGE_THEME, this.currentTheme.selected); 65 | } 66 | window.location.reload(); 67 | } 68 | } 69 | 70 | -------------------------------------------------------------------------------- /src/app/modules/app/themeable.component.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Abstract themeable component 3 | * Created by mdesigaud on 23/11/2016. 4 | */ 5 | 6 | import * as Config from '../../utils/env.config'; 7 | import * as _ from 'lodash'; 8 | 9 | export abstract class ThemeableComponent { 10 | constructor() { 11 | if (!localStorage.getItem(Config.LOCAL_STORAGE_THEME)) { 12 | localStorage.setItem(Config.LOCAL_STORAGE_THEME, Config.THEMES[0].value); 13 | } 14 | } 15 | getThemeOption(option: any): string { 16 | let theme = this.getCurrentTheme(); 17 | return theme[option]; 18 | } 19 | getCurrentTheme(): any { 20 | let themeName = localStorage.getItem(Config.LOCAL_STORAGE_THEME); 21 | return _.find(Config.THEMES, {value: themeName}); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/app/modules/main/data-type/data-type-link.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{getDtoType()}} 4 | 5 | 6 | [{{getDtoType()}}] 7 | 8 | [{{getDtoType()}}] 9 | {{getDtoType()}} 10 | -------------------------------------------------------------------------------- /src/app/modules/main/data-type/data-type-link.ts: -------------------------------------------------------------------------------- 1 | import {Component, Input, Output, EventEmitter} from '@angular/core'; 2 | import {ApiDefinition} from '../../../model/api-definition'; 3 | import {ResponseObject} from '../../../model/apidoc'; 4 | import {ApiDocService} from '../../../services/apidoc.service'; 5 | import {ParameterObject} from '../../../model/api-parameter'; 6 | 7 | @Component({ 8 | selector: 'data-type-link', 9 | template: require('./data-type-link.html') 10 | }) 11 | export class DataTypeLinkComponent { 12 | @Input() data: ResponseObject|ParameterObject; 13 | @Output('select-type') selectType: EventEmitter = new EventEmitter(); 14 | private apiDoc: ApiDefinition; 15 | constructor(apiDocService: ApiDocService) { 16 | this.apiDoc = new ApiDefinition(); 17 | apiDocService.getApi().subscribe((apiDoc: ApiDefinition) => this.apiDoc = apiDoc); 18 | } 19 | isDtoType(): boolean { 20 | return this.apiDoc.isDtoType(this.data); 21 | } 22 | getDtoType(): string { 23 | return this.apiDoc.getDtoType(this.data); 24 | } 25 | isTypeArray(): boolean { 26 | return this.apiDoc.isTypeArray(this.data); 27 | } 28 | onSelectType(event: Event): void { 29 | event.preventDefault(); 30 | this.selectType.emit(this.getDtoType()); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/app/modules/main/detail/detail.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | arrow_back Back 5 | 6 |
7 |
8 |
9 |
10 |
General information
11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
SummaryDescriptionMethodUrl
{{operation.summary}}{{operation.description}}{{operation.name}}{{apiDoc.baseUrl}}{{operation.getRequestUrl()}}
27 |
28 |
29 |
Response content type
30 | 31 |
32 |
33 | 34 | 35 | 36 | 37 | 38 | 40 | 41 | 42 |
Parameters
43 |
44 | 45 |
46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 74 | 78 | 79 | 82 | 83 | 84 | 85 |
Parameter nameParameter valueDescriptionData typeParameter type
{{parameter.name}} 58 |
59 | 60 | 61 |
62 | 66 |
67 | 68 |
69 |
70 | 72 |
73 |
75 |
76 | 77 |
{{parameter.description}} 80 | 81 | {{parameter.in}}
86 |
87 |
88 |
Response messages
89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 103 | 104 | 105 | 106 |
Http status codeReasonResponse ModelHeaders
{{response.code}}{{response.response.description}} 101 | 102 |
107 |
108 |
109 | 110 | 111 |
112 |
113 |
114 |
-------------------------------------------------------------------------------- /src/app/modules/main/detail/detail.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit, OnDestroy} from '@angular/core'; 2 | import {ApiDocService} from '../../../services/apidoc.service'; 3 | import {PathsObject} from '../../../model/apidoc'; 4 | import {ApiDefinition} from '../../../model/api-definition'; 5 | import {ApiModelUtils} from '../../../model/api-utils'; 6 | import {OperationObject} from '../../../model/api-operation'; 7 | import {ParameterObject} from '../../../model/api-parameter'; 8 | import {ActivatedRoute, Router} from '@angular/router'; 9 | import {Subscription} from 'rxjs'; 10 | import {ThemeableComponent} from '../../app/themeable.component'; 11 | 12 | @Component({ 13 | selector: 'doc-detail', 14 | template: require('./detail.html'), 15 | }) 16 | export class ApiDocDetailComponent extends ThemeableComponent implements OnInit, OnDestroy { 17 | operation: OperationObject; 18 | private pathId: number; 19 | private operationId: number; 20 | private apiDoc: ApiDefinition; 21 | private routeSub: Subscription; 22 | 23 | constructor(private apiDocService: ApiDocService, private router: Router, private route: ActivatedRoute) { 24 | super(); 25 | this.operation = new OperationObject(); 26 | this.apiDoc = new ApiDefinition(); 27 | } 28 | 29 | ngOnInit() { 30 | this.routeSub = this.route.params.subscribe(params => { 31 | this.pathId = parseInt(params['path'], 10); 32 | this.operationId = parseInt(params['operation'], 10); 33 | 34 | this.apiDocService.getApi().subscribe((apiDoc: ApiDefinition) => { 35 | this.apiDoc = apiDoc; 36 | let path: PathsObject = this.apiDocService.apiDoc.paths[this.pathId - 1]; 37 | if (path) { 38 | this.operation = path.path.operations[this.operationId - 1]; 39 | 40 | if (!this.operation) { 41 | this.router.navigate(['apis', this.pathId]); 42 | } 43 | } else { 44 | this.router.navigate(['apis', this.pathId]); 45 | } 46 | }); 47 | }); 48 | } 49 | 50 | ngOnDestroy() { 51 | this.routeSub.unsubscribe(); 52 | } 53 | 54 | goToListPage(event: Event): void { 55 | event.preventDefault(); 56 | this.router.navigate(['apis', this.pathId]); 57 | } 58 | generate(event: Event, parameter: ParameterObject): void { 59 | event.preventDefault(); 60 | this.operation.originalData = this.apiDoc.getBodyDescription(parameter.getParameterType(), this.operation.isConsumeXml()); 61 | if (ApiModelUtils.isTypeArray(parameter)) { 62 | this.operation.originalData = [this.operation.originalData]; 63 | } 64 | 65 | if (this.operation.isConsumeJson()) { 66 | this.operation.dataJson = JSON.stringify(this.operation.originalData, null, 4); 67 | 68 | } else if (this.operation.isConsumeXml()) { 69 | this.operation.dataJson = vkbeautify.xml(x2js.js2xml(this.operation.originalData)); 70 | } 71 | setTimeout(() => { 72 | $('textarea').trigger('autoresize'); 73 | }, 0); 74 | 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/app/modules/main/left-menu/left-menu.html: -------------------------------------------------------------------------------- 1 |
2 | 26 |
-------------------------------------------------------------------------------- /src/app/modules/main/left-menu/left-menu.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | import {ApiDocService} from '../../../services/apidoc.service'; 3 | import {ApiDefinition} from '../../../model/api-definition'; 4 | import {PathsObject} from '../../../model/apidoc'; 5 | import {Router} from '@angular/router'; 6 | import {ThemeableComponent} from '../../app/themeable.component'; 7 | 8 | @Component({ 9 | selector: 'left-menu', 10 | template: require('./left-menu.html'), 11 | }) 12 | export class LeftMenuComponent extends ThemeableComponent { 13 | private apiDoc: ApiDefinition; 14 | constructor(private apiDocService: ApiDocService, private router: Router) { 15 | super(); 16 | this.apiDoc = new ApiDefinition(); 17 | apiDocService.getApi().subscribe((apiDoc: ApiDefinition) => { 18 | this.apiDoc = apiDoc; 19 | }); 20 | } 21 | onSelectApi(event: Event, apiPath: PathsObject): void { 22 | event.preventDefault(); 23 | let index = 0; 24 | // This would also work (in most cases): 25 | // console.log(`index of clicked path ${apiPath.name} is ${this.apiDoc.paths.indexOf(apiPath)}`); 26 | this.apiDoc.paths.forEach( (path: PathsObject, idx: number) => { 27 | if (path.name === apiPath.name) { 28 | index = idx; 29 | } 30 | }); 31 | // console.log(`calculated index is: ${index}`) 32 | this.markApiPathSelected(apiPath); 33 | this.router.navigate(['apis', index + 1]); 34 | } 35 | markApiPathSelected (path: PathsObject) { 36 | this.apiDoc.paths.forEach((docPath: PathsObject) => docPath.selected = false); 37 | path.selected = true; 38 | } 39 | getSelectedTheme(apiPath: any, option: string): any|undefined { 40 | if (apiPath.selected) { 41 | return {'background-color': super.getThemeOption(option), 'color': '#FFFFFF'}; 42 | } 43 | } 44 | getTextColor(apiPath: any, option: string): any|undefined { 45 | if (apiPath.selected) { 46 | return super.getThemeOption(option); 47 | } 48 | return 'black-text'; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/app/modules/main/list/list.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | {{operation.name}} {{operation.summary}}
6 |
7 |
8 |

{{operation.summary}} {{operation.parameters.length}} parameter(s)


9 |

{{operation.description}}


10 |

11 | 12 | 13 |


14 |
15 |
16 | 19 | 20 | 23 |
24 |
25 |
26 | 27 |
-------------------------------------------------------------------------------- /src/app/modules/main/list/list.ts: -------------------------------------------------------------------------------- 1 | 2 | import {Component, OnInit, OnDestroy} from '@angular/core'; 3 | import {ApiDocService} from '../../../services/apidoc.service'; 4 | import {ApiDefinition} from '../../../model/api-definition'; 5 | import {OperationObject} from '../../../model/api-operation'; 6 | import {PathsObject, DefinitionsObject} from '../../../model/apidoc'; 7 | import {Router, ActivatedRoute} from '@angular/router'; 8 | import {Subscription} from 'rxjs'; 9 | 10 | import * as _ from 'lodash'; 11 | 12 | @Component({ 13 | selector: 'doc-list', 14 | template: require('./list.html'), 15 | }) 16 | export class ApiDocListComponent implements OnInit, OnDestroy { 17 | private apiPath: PathsObject; 18 | private definition: DefinitionsObject; 19 | private pathId: number; 20 | private apiDoc: ApiDefinition; 21 | 22 | private sub: Subscription; 23 | 24 | constructor(private apiDocService: ApiDocService, private router: Router, private route: ActivatedRoute) {} 25 | 26 | ngOnInit() { 27 | this.apiPath = new PathsObject(); 28 | this.definition = new DefinitionsObject(); 29 | this.apiDoc = new ApiDefinition(); 30 | 31 | this.sub = this.route.params.subscribe(params => { 32 | this.pathId = +params['path']; 33 | 34 | this.apiDocService.getApi().subscribe((apiDoc: ApiDefinition) => { 35 | this.apiDoc = apiDoc; 36 | this.apiPath = this.apiDocService.apiDoc.paths[this.pathId - 1]; 37 | if (!this.apiPath) { 38 | this.router.navigate(['apis', 1]); 39 | } 40 | }); 41 | }); 42 | } 43 | 44 | ngOnDestroy() { 45 | this.sub.unsubscribe(); 46 | } 47 | 48 | hasStats(operation: OperationObject): boolean { 49 | if (operation && localStorage.getItem(operation.slug) !== null) { 50 | let requestTimes: {date: Date, time: number}[] = JSON.parse(localStorage.getItem(operation.slug)); 51 | return requestTimes && !_.isEmpty(requestTimes); 52 | } 53 | return false; 54 | } 55 | goToDetailPage(event: Event, index: number): void { 56 | event.preventDefault(); 57 | this.router.navigate(['apis', this.pathId, 'detail', index + 1]); 58 | } 59 | } 60 | 61 | -------------------------------------------------------------------------------- /src/app/modules/main/main.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import {ApiDocDetailComponent} from './detail/detail'; 3 | import {ApiDocListComponent} from './list/list'; 4 | import {mainRouting} from './main.routing'; 5 | import {MainComponent} from './main'; 6 | import {SharedModule} from '../shared.module'; 7 | import {CommonModule} from '@angular/common'; 8 | import {MaterializeModule} from '../materialize/materialize.module'; 9 | import {FormsModule} from '@angular/forms'; 10 | import {DataTypeLinkComponent} from './data-type/data-type-link'; 11 | import {LeftMenuComponent} from './left-menu/left-menu'; 12 | import {SearchFilterPipe, TagFilterPipe, CountPipe} from '../../pipes/pipes'; 13 | 14 | @NgModule({ 15 | declarations: [ MainComponent, ApiDocDetailComponent, ApiDocListComponent, DataTypeLinkComponent, LeftMenuComponent, 16 | CountPipe, TagFilterPipe, SearchFilterPipe ], 17 | imports: [CommonModule, mainRouting, MaterializeModule, FormsModule, SharedModule], 18 | }) 19 | export class MainModule {} 20 | -------------------------------------------------------------------------------- /src/app/modules/main/main.routing.ts: -------------------------------------------------------------------------------- 1 | import {Routes, RouterModule} from '@angular/router'; 2 | import {ApiDocListComponent} from './list/list'; 3 | import {ApiDocDetailComponent} from './detail/detail'; 4 | import {MainComponent} from './main'; 5 | import {ModuleWithProviders} from '@angular/core'; 6 | 7 | const mainRoutes: Routes = [ 8 | { path: 'apis', 9 | component: MainComponent, 10 | children: [ 11 | {path: ':path/detail/:operation', component: ApiDocDetailComponent}, 12 | {path: ':path', component: ApiDocListComponent} 13 | ] 14 | } 15 | ]; 16 | 17 | export const mainRouting: ModuleWithProviders = RouterModule.forChild(mainRoutes); 18 | -------------------------------------------------------------------------------- /src/app/modules/main/main.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'doc-main', 5 | template: ` 6 |
7 | 8 | 9 |
` 10 | }) 11 | 12 | export class MainComponent {} 13 | -------------------------------------------------------------------------------- /src/app/modules/materialize/directives/materialize-collapsible.ts: -------------------------------------------------------------------------------- 1 | import {Directive, AfterViewInit, ElementRef} from '@angular/core'; 2 | 3 | @Directive({ 4 | selector: 'ul[materialize-collapsible]' 5 | }) 6 | export class MaterializeCollapsibleDirective implements AfterViewInit { 7 | constructor(private el: ElementRef) {} 8 | ngAfterViewInit(): void { 9 | setTimeout(() => { 10 | $(this.el.nativeElement).collapsible({ 11 | accordion : false 12 | }); 13 | }); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/app/modules/materialize/directives/materialize-collection.ts: -------------------------------------------------------------------------------- 1 | import {Directive, ElementRef, HostListener} from '@angular/core'; 2 | 3 | @Directive({ 4 | selector: 'ul[materialize-collection]' 5 | }) 6 | export class MaterializeCollectionDirective { 7 | constructor(private el: ElementRef) {} 8 | @HostListener('click', ['$event']) 9 | onClickList(event: Event): void { 10 | $(this.el.nativeElement).find('li.active').removeClass('active'); 11 | $(event.target).parents('li:first').addClass('active'); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/app/modules/materialize/directives/materialize-header.ts: -------------------------------------------------------------------------------- 1 | import {Directive, ElementRef, HostListener, AfterViewInit} from '@angular/core'; 2 | import {ThemeableComponent} from '../../app/themeable.component'; 3 | 4 | @Directive({ 5 | selector: 'ul[materialize-header]' 6 | }) 7 | export class MaterializeHeaderDirective extends ThemeableComponent implements AfterViewInit { 8 | private activeClass: string; 9 | constructor(private el: ElementRef) { 10 | super(); 11 | this.activeClass = this.getThemeOption('activeClass'); 12 | } 13 | ngAfterViewInit(): void { 14 | if (location.hash.indexOf('home') !== -1) { 15 | $(this.el.nativeElement).find('#homeLink').addClass('active').addClass(this.activeClass); 16 | $(this.el.nativeElement).find('#mobileHomeLink').addClass('active').addClass(this.activeClass); 17 | } 18 | if (location.hash.indexOf('apis') !== -1) { 19 | $(this.el.nativeElement).find('#apiLink').addClass('active').addClass(this.activeClass); 20 | $(this.el.nativeElement).find('#mobileApiLink').addClass('active').addClass(this.activeClass); 21 | } 22 | if (location.hash.indexOf('settings') !== -1) { 23 | $(this.el.nativeElement).find('#settingsLink').addClass('active').addClass(this.activeClass); 24 | $(this.el.nativeElement).find('#mobileApiLink').addClass('active').addClass(this.activeClass); 25 | } 26 | } 27 | @HostListener('click', ['$event']) 28 | onClickList(event: Event): void { 29 | event.preventDefault(); 30 | $(this.el.nativeElement).parent().find('a.active').removeClass('active').removeClass(this.activeClass); 31 | $(event.target).parents('li:first').find('a').addClass('active').addClass(this.activeClass); 32 | } 33 | } 34 | 35 | @Directive({ 36 | selector: 'a[materialize-collapse-button]' 37 | }) 38 | export class MaterializeCollapseButtonDirective implements AfterViewInit { 39 | constructor(private el: ElementRef) {} 40 | ngAfterViewInit(): void { 41 | $(this.el.nativeElement).sideNav(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/app/modules/materialize/input-file/materialize-input-file.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | File 4 | 5 |
6 |
7 | 8 |
9 |
-------------------------------------------------------------------------------- /src/app/modules/materialize/input-file/materialize-input-file.ts: -------------------------------------------------------------------------------- 1 | import {Component, Input, Output, EventEmitter} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'materialize-input-file', 5 | template: require('./materialize-input-file.html') 6 | }) 7 | export class MaterializeInputFileComponent { 8 | @Input() model: any; 9 | @Output() changeEmitter: EventEmitter = new EventEmitter(); 10 | onChange(event: any): void { 11 | let reader: FileReader = new FileReader(); 12 | reader.onloadend = (loadEvent: any) => { 13 | let file: File = event.target.files[0]; 14 | this.model.selected = {file: file, content: loadEvent.target.result}; 15 | this.changeEmitter.emit(this.model); 16 | }; 17 | reader.readAsDataURL(event.target.files[0]); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/app/modules/materialize/materialize.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {MaterializeInputFileComponent} from './input-file/materialize-input-file'; 3 | import {BodyModalComponent} from './modals/body-modal'; 4 | import {ChartModalComponent} from './modals/chart-modal'; 5 | import {TypeModalComponent} from './modals/type.modal'; 6 | import {SimpleMaterializeSelectComponent} from './select/simple-materialize-select'; 7 | import {MultipleMaterializeSelectComponent} from './select/multiple-materialize-select'; 8 | import {SharedModule} from '../shared.module'; 9 | import {CommonModule} from '@angular/common'; 10 | import {MaterializeCollapsibleDirective} from './directives/materialize-collapsible'; 11 | import {MaterializeCollectionDirective} from './directives/materialize-collection'; 12 | import {MaterializeHeaderDirective, MaterializeCollapseButtonDirective} from './directives/materialize-header'; 13 | 14 | @NgModule ({ 15 | imports: [CommonModule, SharedModule], 16 | declarations: [MaterializeInputFileComponent, BodyModalComponent, ChartModalComponent, 17 | TypeModalComponent, SimpleMaterializeSelectComponent, 18 | MultipleMaterializeSelectComponent, MaterializeCollapsibleDirective, 19 | MaterializeCollectionDirective, MaterializeHeaderDirective, MaterializeCollapseButtonDirective], 20 | exports: [MaterializeInputFileComponent, BodyModalComponent, ChartModalComponent, 21 | TypeModalComponent, SimpleMaterializeSelectComponent, MultipleMaterializeSelectComponent, 22 | MaterializeCollapsibleDirective, MaterializeCollectionDirective, MaterializeHeaderDirective, MaterializeCollapseButtonDirective], 23 | }) 24 | export class MaterializeModule { 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/app/modules/materialize/modals/body-modal.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/modules/materialize/modals/body-modal.ts: -------------------------------------------------------------------------------- 1 | import {Component, Input, ElementRef, NgZone} from '@angular/core'; 2 | import {ApiDefinition} from '../../../model/api-definition'; 3 | import {OperationObject} from '../../../model/api-operation'; 4 | import {ApiResult} from '../../../model/api-result'; 5 | import {ApiDocService} from '../../../services/apidoc.service'; 6 | import {MaterializeModal} from './materialize-modal'; 7 | import {Response} from '@angular/http'; 8 | 9 | import * as _ from 'lodash'; 10 | 11 | @Component({ 12 | selector: 'body-modal', 13 | template: require('./body-modal.html') 14 | }) 15 | export class BodyModalComponent extends MaterializeModal { 16 | @Input() operation: OperationObject; 17 | private apiResult: ApiResult; 18 | constructor(private apiDocService: ApiDocService, el: ElementRef, private zone: NgZone) { 19 | super(el); 20 | this.apiResult = new ApiResult(); 21 | this.operation = new OperationObject(); 22 | this.apiDocService.getApi().subscribe((apiDoc: ApiDefinition) => this.apiDoc = apiDoc); 23 | } 24 | tryApi(event: Event): void { 25 | // 26 | event.preventDefault(); 27 | this.apiResult.date = new Date(); 28 | // 29 | this.apiDocService.sendRequest(this.operation).subscribe((apiResult: ApiResult) => { 30 | this.apiResult = apiResult; 31 | this.beautifyResponse(event); 32 | this.storeRequest(); 33 | }, (err: Response) => { 34 | console.log('Request error', err); 35 | this.apiResult.operation = this.operation; 36 | this.apiResult.endDate = new Date(); 37 | this.apiResult.status = err.status; 38 | this.apiResult.message = this.tryParseErrorResponse(err); 39 | this.beautifyResponse(event); 40 | }); 41 | } 42 | tryParseErrorResponse(err: Response): any { 43 | let body: string = err.text(); 44 | if (_.isEmpty(body)) { 45 | return body; 46 | } 47 | try { 48 | return err.json(); 49 | } catch (e) { 50 | return body; 51 | } 52 | } 53 | storeRequest(): void { 54 | if (this.operation.slug) { 55 | if (!localStorage.getItem(this.operation.slug)) { 56 | localStorage.setItem(this.operation.slug, JSON.stringify([])); 57 | } 58 | let requestTimes: {date: Date, time: number}[] = JSON.parse(localStorage.getItem(this.operation.slug)); 59 | requestTimes.push({date: this.apiResult.date, time: this.apiResult.getRequestTime()}); 60 | localStorage.setItem(this.operation.slug, JSON.stringify(requestTimes)); 61 | } 62 | } 63 | beautifyResponse(event: Event): void { 64 | let codeElement: any = $(this.el.nativeElement).find('pre code'); 65 | this.openModal(event); 66 | this.zone.run(() => { 67 | if (this.operation.isProduceJson()) { 68 | codeElement.html(hljs.highlight('json', JSON.stringify(this.apiResult.message, null, 4)).value); 69 | } else if (this.operation.isProduceXml()) { 70 | codeElement.text(vkbeautify.xml(this.apiResult.message)); 71 | } else { 72 | codeElement.text(this.apiResult.message); 73 | } 74 | }); 75 | } 76 | getFullUrl(): string { 77 | return this.apiDoc.baseUrl 78 | + this.apiResult.operation.getRequestUrl(); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/app/modules/materialize/modals/chart-modal.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/modules/materialize/modals/chart-modal.ts: -------------------------------------------------------------------------------- 1 | import {MaterializeModal} from './materialize-modal'; 2 | import {Component, ElementRef, NgZone, ViewChild} from '@angular/core'; 3 | import {PathsObject} from '../../../model/apidoc'; 4 | import {OperationObject} from '../../../model/api-operation'; 5 | import {ApiDefinition} from '../../../model/api-definition'; 6 | import {ApiDocService} from '../../../services/apidoc.service'; 7 | import {MultipleMaterializeSelectComponent} from '../select/multiple-materialize-select'; 8 | import * as Config from '../../../utils/env.config'; 9 | 10 | import * as _ from 'lodash'; 11 | 12 | @Component({ 13 | selector: 'chart-modal', 14 | template: require('./chart-modal.html'), 15 | }) 16 | export class ChartModalComponent extends MaterializeModal { 17 | @ViewChild('operationSelect') operationSelect: MultipleMaterializeSelectComponent; 18 | private chart: any; 19 | private chartData: any; 20 | private operations: Array; 21 | private chartOperations: Array; 22 | private currentOperation: OperationObject; 23 | private selectedOperation: any; 24 | constructor(public el: ElementRef, private zone: NgZone, private apiDocService: ApiDocService) { 25 | super(el); 26 | this.operations = []; 27 | this.chartOperations = []; 28 | this.resetData(); 29 | this.selectedOperation = {}; 30 | this.currentOperation = new OperationObject(); 31 | this.apiDocService.getApi().subscribe((apiDoc: ApiDefinition) => this.apiDoc = apiDoc); 32 | } 33 | resetData(): void { 34 | this.chartData = { 35 | labels: [], 36 | datasets: [], 37 | options: { 38 | animation: false, 39 | responsive: true, 40 | tooltipTemplate: (tooltip: any) => { 41 | return tooltip.datasetLabel + ' - ' + tooltip.value + ' ms'; 42 | }, 43 | multiTooltipTemplate: (tooltip: any) => { 44 | return tooltip.datasetLabel + ' - ' + tooltip.value + ' ms'; 45 | } 46 | } 47 | }; 48 | if (this.chart) { 49 | this.chart.destroy(); 50 | } 51 | } 52 | getRandomColor() { 53 | let r = () => { return Math.floor(Math.random() * 256); }; 54 | return 'rgba(' + r() + ',' + r() + ',' + r() + ',0.5)'; 55 | } 56 | getContext(): any { 57 | let canvas: any = $(this.el.nativeElement).find('canvas')[0]; 58 | return canvas.getContext('2d'); 59 | } 60 | showGraph(operation: OperationObject): void { 61 | this.currentOperation = operation; 62 | this.resetData(); 63 | this.openModal(null); 64 | setTimeout(() => this.operationSelect.refresh(), 0); 65 | this.zone.run(() => { 66 | this.listOperations(); 67 | this.createChart(this.operations); 68 | }); 69 | } 70 | getDataSetByType(): any { 71 | let chartType: string = this.getChartType(); 72 | let dataSet: any = { 73 | label: '', 74 | strokeColor: 'rgba(220,220,220,1)', 75 | data: [] 76 | }; 77 | if (chartType === Config.CHART_TYPE_LINE) { 78 | dataSet.pointColor = 'rgba(220,220,220,1)'; 79 | dataSet.pointStrokeColor = '#fff'; 80 | dataSet.pointHighlightFill = '#fff'; 81 | dataSet.pointHighlightStroke = 'rgba(220,220,220,1)'; 82 | } else { 83 | dataSet.highlightFill = 'rgba(220,220,220,0.75)'; 84 | dataSet.highlightStroke = 'rgba(220,220,220,1)'; 85 | } 86 | return dataSet; 87 | } 88 | createChart(operations: Array): void { 89 | if (operations && !_.isEmpty(operations)) { 90 | 91 | this.resetData(); 92 | let max = 0; 93 | 94 | operations.forEach((operation: OperationObject) => { 95 | 96 | operation.chartColor = this.getRandomColor(); 97 | 98 | let dataSetData = this.getDataSetByType(); 99 | dataSetData['backgroundColor'] = operation.chartColor; 100 | 101 | let requestTimes: {date: Date, time: number}[] = JSON.parse(localStorage.getItem(operation.slug)); 102 | if (requestTimes.length > max) { 103 | this.chartData.labels = []; 104 | max = requestTimes.length; 105 | for (let i = 0; i < max; i++) { 106 | this.chartData.labels.push(i + 1); 107 | } 108 | } 109 | 110 | dataSetData.data = _.map(requestTimes, 'time'); 111 | dataSetData.label = operation.summary; 112 | 113 | this.chartData.datasets.push(dataSetData); 114 | }); 115 | 116 | this.chartOperations = operations; 117 | 118 | let ctx: any = this.getContext(); 119 | 120 | let chartType: string = this.getChartType(); 121 | if (chartType === Config.CHART_TYPE_LINE) { 122 | this.chart = Chart.Line(ctx, {data: this.chartData, options: this.chartData.options}); 123 | } else { 124 | this.chart = Chart.Bar(ctx, {data: this.chartData, options: this.chartData.options}); 125 | } 126 | } 127 | } 128 | getChartType(): string { 129 | return localStorage.getItem(Config.LOCAL_STORAGE_CHART_TYPE) ? 130 | localStorage.getItem(Config.LOCAL_STORAGE_CHART_TYPE) : Config.CHART_TYPE_BAR; 131 | } 132 | listOperations(): void { 133 | this.operations = []; 134 | this.apiDoc.paths.forEach((path: PathsObject) => { 135 | let operations: Array = path.path.operations.filter((operation: OperationObject) => { 136 | return localStorage.getItem(operation.slug) !== null; 137 | }); 138 | if (!_.isEmpty(operations)) { 139 | this.operations = this.operations.concat(operations); 140 | } 141 | }); 142 | } 143 | getOperationsMap(): {value: string, label: string}[] { 144 | return this.operations.map((operation: OperationObject) => { 145 | let data: any = {}; 146 | data.value = operation.operationId; 147 | data.label = operation.operationId; 148 | data.disabled = operation.operationId === this.currentOperation.operationId; 149 | data.selected = true; 150 | return data; 151 | }); 152 | } 153 | onSelectOperation(event: any): void { 154 | if (event && event.hasOwnProperty('selected')) { 155 | let operations: Array = []; 156 | if (!event.selected) { 157 | this.createChart([this.currentOperation]); 158 | } else { 159 | operations = this.apiDoc.getOperationsByProperty(event.selected, 'operationId'); 160 | operations.push(this.currentOperation); 161 | this.createChart(operations); 162 | } 163 | } 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/app/modules/materialize/modals/materialize-modal.ts: -------------------------------------------------------------------------------- 1 | import {ApiDefinition} from '../../../model/api-definition'; 2 | import {ElementRef} from '@angular/core'; 3 | import {AfterViewInit} from '@angular/core'; 4 | 5 | export abstract class MaterializeModal implements AfterViewInit { 6 | modalRef: any; 7 | protected apiDoc: ApiDefinition; 8 | constructor(public el: ElementRef) { 9 | this.apiDoc = new ApiDefinition(); 10 | } 11 | ngAfterViewInit(): void { 12 | this.modalRef = $(this.el.nativeElement).find('div.modal'); 13 | this.modalRef.modal(); 14 | } 15 | openModal(event?: Event): void { 16 | if (event) { 17 | event.preventDefault(); 18 | } 19 | this.modalRef.modal('open'); 20 | } 21 | closeModal(event?: Event): void { 22 | if (event) { 23 | event.preventDefault(); 24 | } 25 | this.modalRef.modal('close'); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/app/modules/materialize/modals/type-modal.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/modules/materialize/modals/type.modal.ts: -------------------------------------------------------------------------------- 1 | import {Component, ElementRef} from '@angular/core'; 2 | import {ApiDefinition} from '../../../model/api-definition'; 3 | import {DefinitionsObject} from '../../../model/apidoc'; 4 | import {ApiDocService} from '../../../services/apidoc.service'; 5 | import {MaterializeModal} from './materialize-modal'; 6 | 7 | import * as _ from 'lodash'; 8 | 9 | @Component({ 10 | selector: 'type-modal', 11 | template: require('./type-modal.html') 12 | }) 13 | export class TypeModalComponent extends MaterializeModal { 14 | definition: DefinitionsObject; 15 | properties: Array; 16 | entityHistory: Array; 17 | constructor(apiDocService: ApiDocService, el: ElementRef) { 18 | super(el); 19 | this.definition = new DefinitionsObject(); 20 | this.properties = []; 21 | this.entityHistory = []; 22 | apiDocService.getApi().subscribe((apiDoc: ApiDefinition) => this.apiDoc = apiDoc); 23 | } 24 | selectBreadcrumb($event: Event, index: number): void { 25 | $event.preventDefault(); 26 | if (index < this.entityHistory.length - 1) { 27 | this.entityHistory = this.entityHistory.slice(index, index + 1); 28 | if (this.entityHistory.length >= 1) { 29 | this.definition = this.apiDoc.getDefinitionByEntity(_.last(this.entityHistory)); 30 | if (this.definition) { 31 | this.properties = Object.keys(this.definition.schema.properties).map((key) => { 32 | return {value: this.definition.schema.properties[key], key: key}; 33 | }); 34 | } 35 | } 36 | } 37 | } 38 | onSelectType(eventData: Event): void { 39 | this.entityHistory = []; 40 | this.selectType(null, eventData); 41 | } 42 | selectType(event: Event, property: any, openModal = true): void { 43 | if (event) { 44 | event.preventDefault(); 45 | } 46 | let entity: string; 47 | if (_.isString(property)) { 48 | entity = property; 49 | } else { 50 | entity = this.isArrayDtoType(property) 51 | ? this.apiDoc.getEntityName(property.value.items.$ref) 52 | : this.apiDoc.getEntityName(property.value.$ref); 53 | } 54 | this.definition = this.apiDoc.getDefinitionByEntity(entity); 55 | if (this.definition) { 56 | this.properties = Object.keys(this.definition.schema.properties) 57 | .map((key) => {return {value: this.definition.schema.properties[key], key: key}; }); 58 | } 59 | 60 | this.entityHistory.push(entity); 61 | 62 | if (openModal) { 63 | this.openModal(event); 64 | } 65 | } 66 | isSimpleType(property: any): boolean { 67 | return !this.definition.schema.isPropertyTypeArray(property.value) 68 | && !_.isUndefined(property.value.type) 69 | && !this.apiDoc.hasDefinition(property.value.type); 70 | } 71 | hasDefinition(property: any): boolean { 72 | return !this.definition.schema.isPropertyTypeArray(property.value) 73 | && this.apiDoc.hasDefinition(property.value.$ref, true); 74 | } 75 | isArrayDtoType(property: any): boolean { 76 | return this.definition.schema.isPropertyTypeArray(property.value) 77 | && property.value.items && this.apiDoc.hasDefinition(property.value.items.$ref, true); 78 | } 79 | isArraySimpleType(property: any): boolean { 80 | return this.definition.schema.isPropertyTypeArray(property.value) 81 | && !_.isUndefined(property.value.items.type) 82 | && !this.apiDoc.hasDefinition(property.value.items.type); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/app/modules/materialize/select/materialize-select.html: -------------------------------------------------------------------------------- 1 |
2 | 5 | 6 |
-------------------------------------------------------------------------------- /src/app/modules/materialize/select/materialize-select.ts: -------------------------------------------------------------------------------- 1 | 2 | import {ElementRef, EventEmitter, AfterViewInit} from '@angular/core'; 3 | 4 | export abstract class MaterializeSelect implements AfterViewInit { 5 | name: string; 6 | label: string; 7 | model: any; 8 | selectValueChange: EventEmitter; 9 | protected selectInput: any; 10 | private multiple: boolean; 11 | constructor(public el: ElementRef, multiple = false) { 12 | this.multiple = multiple; 13 | } 14 | ngAfterViewInit(): void { 15 | this.selectInput = $(this.el.nativeElement).find('select'); 16 | 17 | let divParent: any = $(this.el.nativeElement).find('.input-field'); 18 | divParent.on('change', 'select', () => { 19 | this.onChangeValue(); 20 | this.updateValue(this.selectInput.val()); 21 | }); 22 | } 23 | updateValue(value: string|Array): void { 24 | this.model.selected = value; 25 | this.selectValueChange.emit(this.model); 26 | } 27 | abstract isSelected(option: any): boolean; 28 | abstract isDisabled(option: any): boolean; 29 | refresh(): void { 30 | this.selectInput.material_select(); 31 | } 32 | abstract onChangeValue(): void; 33 | } 34 | -------------------------------------------------------------------------------- /src/app/modules/materialize/select/multiple-materialize-select.spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Multiple select unit tests 3 | * Created by Michael DESIGAUD on 20/11/2016. 4 | */ 5 | 6 | 7 | import {TestBed, ComponentFixture, async} from '@angular/core/testing'; 8 | import {MultipleMaterializeSelectComponent} from './multiple-materialize-select'; 9 | import {DebugElement} from '@angular/core'; 10 | import {By} from '@angular/platform-browser'; 11 | 12 | 13 | let comp: MultipleMaterializeSelectComponent; 14 | let fixture: ComponentFixture; 15 | let selectEl: DebugElement; 16 | 17 | // Component inputs 18 | let expectedModel: any; 19 | let expectedOptions: [{label: string, value: string, selected: boolean, disabled: boolean}] = [ 20 | {label: 'Application Json', value: 'application/json', selected: true, disabled : false}, 21 | {label: 'Application Xml', value: 'application/xml', selected: false, disabled : false}]; 22 | 23 | describe('MultipleMaterializeSelectComponent', () => { 24 | beforeEach(() => { 25 | 26 | // refine the test module by declaring the test component 27 | TestBed.configureTestingModule({ 28 | declarations: [MultipleMaterializeSelectComponent] 29 | }).compileComponents(); 30 | 31 | // create component and test fixture 32 | fixture = TestBed.createComponent(MultipleMaterializeSelectComponent); 33 | 34 | // get test component from the fixture 35 | comp = fixture.componentInstance; 36 | 37 | selectEl = fixture.debugElement.query(By.css('select')); 38 | 39 | expectedModel = {}; 40 | comp.model = expectedModel; 41 | comp.options = expectedOptions; 42 | comp.selected = expectedOptions[0].value; 43 | 44 | spyOn(console, 'log'); 45 | spyOn(comp, 'refresh').and.callThrough(); 46 | 47 | fixture.detectChanges(); // trigger initial data binding 48 | }); 49 | 50 | it('Should be correctly initialized', () => { 51 | expect(selectEl).toBeDefined(); 52 | expect(comp['multiple']).toBeTruthy(); 53 | expect(comp['selectInput']).toBeDefined(); 54 | expect(comp.refresh).toHaveBeenCalled(); 55 | 56 | // Test html is ok 57 | expect(selectEl.children.length).toBe(expectedOptions.length); 58 | 59 | let ul = $(comp['selectInput']).prev(); 60 | expect(ul).toBeDefined(); 61 | expect(ul.children('li').toArray().length).toBe(expectedOptions.length); 62 | }); 63 | 64 | it('OnChange event should work correctly', async (() => { 65 | comp.selectValueChange.subscribe((value: any) => { 66 | expect(value).toBeDefined(); 67 | expect(value).toEqual({selected: [expectedOptions[0].value]}); 68 | }); 69 | 70 | $(selectEl.nativeElement).trigger('change'); 71 | 72 | expect(comp.model.selected).toEqual([expectedOptions[0].value]); 73 | expect(comp.isSelected(expectedOptions[0])).toBeTruthy(); 74 | expect(comp.isDisabled(expectedOptions[0])).toBeUndefined(); 75 | })); 76 | }); 77 | -------------------------------------------------------------------------------- /src/app/modules/materialize/select/multiple-materialize-select.ts: -------------------------------------------------------------------------------- 1 | import {Component, ElementRef, Input, Output, EventEmitter, AfterViewInit} from '@angular/core'; 2 | import {MaterializeSelect} from './materialize-select'; 3 | 4 | @Component({ 5 | selector: 'materialize-select-multiple', 6 | template: require('./materialize-select.html') 7 | }) 8 | export class MultipleMaterializeSelectComponent extends MaterializeSelect implements AfterViewInit { 9 | @Input() name: string; 10 | @Input() label: string; 11 | @Input() model: any; 12 | @Input() options: [{label: string, value: string, selected: boolean, disabled: boolean}]; 13 | @Output('on-change') selectValueChange: EventEmitter = new EventEmitter(); 14 | @Input() selected: string; 15 | constructor(el: ElementRef) { 16 | super(el, true); 17 | } 18 | ngAfterViewInit(): void { 19 | super.ngAfterViewInit(); 20 | this.refresh(); 21 | if (this.selected) { 22 | setTimeout(() => { 23 | this.updateValue([this.selected]); 24 | this.setValue(); 25 | }, 0); 26 | } 27 | } 28 | setValue(): void { 29 | let ul = this.selectInput.prev(); 30 | ul.children('li').toArray().forEach((li: any, i: number) => { 31 | $(li).removeClass('active'); 32 | let value: string = this.selectInput.children('option').toArray()[i].value; 33 | if (value === this.selected) { 34 | $(li).addClass('active'); 35 | this.selectInput.val([this.selected]); 36 | this.refresh(); 37 | } 38 | }); 39 | } 40 | isSelected(option: any): boolean { 41 | if (option.selected) { 42 | return true; 43 | } 44 | } 45 | isDisabled(option: any): boolean { 46 | if (option.disabled) { 47 | return true; 48 | } 49 | } 50 | onChangeValue(): void { 51 | let newValuesArr: any = [], ul = this.selectInput.prev(); 52 | ul.children('li').toArray().forEach((li: any, i: number) => { 53 | if ($(li).hasClass('active')) { 54 | newValuesArr.push(this.selectInput.children('option').toArray()[i].value); 55 | } 56 | }); 57 | if (newValuesArr.hasOwnProperty('selected')) { 58 | this.selectValueChange.emit(newValuesArr); 59 | } 60 | this.selectInput.val(newValuesArr); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/app/modules/materialize/select/simple-materialize-select.spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Simple select unit tests 3 | * Created by Michael DESIGAUD on 19/11/2016. 4 | */ 5 | 6 | import {TestBed, ComponentFixture, async} from '@angular/core/testing'; 7 | import {SimpleMaterializeSelectComponent} from './simple-materialize-select'; 8 | import {DebugElement} from '@angular/core'; 9 | import {By} from '@angular/platform-browser'; 10 | 11 | 12 | let comp: SimpleMaterializeSelectComponent; 13 | let fixture: ComponentFixture; 14 | let selectEl: DebugElement; 15 | 16 | // Component inputs 17 | let expectedModel: any; 18 | let expectedOptions: [{label: string, value: string}] = [ 19 | {label: 'Application Json', value: 'application/json'}, 20 | {label: 'Application Xml', value: 'application/xml'}]; 21 | 22 | describe('SimpleMaterializeSelectComponent', () => { 23 | beforeEach(() => { 24 | 25 | // refine the test module by declaring the test component 26 | TestBed.configureTestingModule({ 27 | declarations: [SimpleMaterializeSelectComponent] 28 | }).compileComponents(); 29 | 30 | // create component and test fixture 31 | fixture = TestBed.createComponent(SimpleMaterializeSelectComponent); 32 | 33 | // get test component from the fixture 34 | comp = fixture.componentInstance; 35 | 36 | selectEl = fixture.debugElement.query(By.css('select')); 37 | 38 | expectedModel = {}; 39 | comp.model = expectedModel; 40 | comp.options = expectedOptions; 41 | comp.selected = expectedOptions[0].value; 42 | 43 | spyOn(console, 'log'); 44 | spyOn(comp, 'refresh').and.callThrough(); 45 | 46 | fixture.detectChanges(); // trigger initial data binding 47 | }); 48 | 49 | it('Should be correctly initialized', () => { 50 | expect(selectEl).toBeDefined(); 51 | expect(comp['multiple']).toBeFalsy(); 52 | expect(comp['selectInput']).toBeDefined(); 53 | expect(comp.refresh).toHaveBeenCalled(); 54 | 55 | // Test html is ok 56 | expect(selectEl.children.length).toBe(expectedOptions.length); 57 | }); 58 | 59 | it('OnChange event should work correctly', async (() => { 60 | comp.selectValueChange.subscribe((value: any) => { 61 | expect(value).toBeDefined(); 62 | expect(value).toEqual({selected: expectedOptions[0].value}); 63 | }); 64 | 65 | $(selectEl.nativeElement).trigger('change'); 66 | 67 | expect(console.log).toHaveBeenCalledWith(expectedOptions[0].value); 68 | expect(comp.model.selected).toBe(expectedOptions[0].value); 69 | expect(comp.isSelected(expectedOptions[0])).toBeTruthy(); 70 | expect(comp.isDisabled(expectedOptions[0])).toBeNull(); 71 | })); 72 | }); 73 | 74 | -------------------------------------------------------------------------------- /src/app/modules/materialize/select/simple-materialize-select.ts: -------------------------------------------------------------------------------- 1 | import {Component, ElementRef, Input, Output, EventEmitter, AfterViewInit, OnChanges} from '@angular/core'; 2 | import {MaterializeSelect} from './materialize-select'; 3 | 4 | import * as _ from 'lodash'; 5 | 6 | @Component({ 7 | selector: 'materialize-select-simple', 8 | template: require('./materialize-select.html') 9 | }) 10 | export class SimpleMaterializeSelectComponent extends MaterializeSelect implements AfterViewInit, OnChanges { 11 | @Input() name: string; 12 | @Input() label: string; 13 | @Input() model: any; 14 | @Input() options: [{label: string, value: string}]; 15 | @Input() selected: string; 16 | @Output('on-change') selectValueChange: EventEmitter = new EventEmitter(); 17 | constructor(el: ElementRef) { 18 | super(el, false); 19 | } 20 | ngAfterViewInit(): void { 21 | super.ngAfterViewInit(); 22 | this.refresh(); 23 | } 24 | ngOnChanges(changes: any): void { 25 | let dropDownSelect: any = $(this.el.nativeElement).find('ul.select-dropdown'); 26 | let options: any = $(dropDownSelect).find('li'); 27 | 28 | if (this.selectInput && !_.isEmpty(changes.options.currentValue) && _.isEmpty(options)) { 29 | this.updateValue(this.selected); 30 | this.selectInput.material_select(); 31 | } 32 | } 33 | onChangeValue(): void { 34 | console.log(this.selectInput.val()); 35 | } 36 | isDisabled(option: any): boolean { 37 | return null; 38 | } 39 | isSelected(option: any): boolean { 40 | if (option.value === this.selected) { 41 | return true; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/app/modules/shared.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { ValuesPipe } from '../pipes/pipes'; 4 | 5 | @NgModule({ 6 | imports: [CommonModule], 7 | declarations: [ValuesPipe], 8 | exports: [ValuesPipe] 9 | }) 10 | export class SharedModule {} 11 | -------------------------------------------------------------------------------- /src/app/pipes/pipes.ts: -------------------------------------------------------------------------------- 1 | import {PipeTransform, Pipe} from '@angular/core'; 2 | import {PathsObject} from '../model/apidoc'; 3 | import {OperationObject} from '../model/api-operation'; 4 | 5 | import * as _ from 'lodash'; 6 | 7 | @Pipe({ name: 'values', pure: false }) 8 | export class ValuesPipe implements PipeTransform { 9 | transform(value: any, args: any[] = null): any { 10 | return value ? Object.keys(value).map((key) => {return {value: value[key], key: key}; }) : value; 11 | } 12 | } 13 | 14 | @Pipe({ name: 'count', pure: false }) 15 | export class CountPipe implements PipeTransform { 16 | transform(value: any, args: any[] = null): any { 17 | return value && _.isArray(value)? value.length : 0; 18 | } 19 | } 20 | 21 | @Pipe({ name: 'tagFiler', pure: false }) 22 | export class TagFilterPipe implements PipeTransform { 23 | transform(value: Array, args: any): any { 24 | if (!args || _.isEmpty(args)) { 25 | return value; 26 | } 27 | return value.filter((path: PathsObject) => { 28 | let data: Array = path.path.operations.filter((operation: OperationObject) => { 29 | return operation.tags && operation.tags.indexOf(args) !== -1; 30 | }); 31 | return data && !_.isEmpty(data); 32 | }); 33 | } 34 | } 35 | 36 | @Pipe({ name: 'searchFiler', pure: false }) 37 | export class SearchFilterPipe implements PipeTransform { 38 | transform(value: Array, args: any): any { 39 | let filter: any = args; 40 | if (filter && _.isArray(value)) { 41 | let allUndefined = true; 42 | Object.keys(filter).forEach((key: string) => { 43 | if (filter[key]) { 44 | allUndefined = false; 45 | } 46 | }); 47 | 48 | if (allUndefined) { 49 | return value; 50 | } 51 | 52 | return value.filter((path: PathsObject) => { 53 | return path.name.toLowerCase().indexOf(filter.name.toLowerCase()) !== -1; 54 | }); 55 | } else { 56 | return value; 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/app/services/api_http_mocks.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Api http mocks for unit tests 3 | * Created by Michael DESIGAUD on 20/11/2016. 4 | */ 5 | 6 | export const API_MOCK_URL = 'http://petstore.swagger.io/v2/swagger.json'; 7 | 8 | export const HTTP_GET_API_MOCK = { 9 | "swagger":"2.0", 10 | "info":{ 11 | "description":"This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key `special-key` to test the authorization filters.", 12 | "version":"1.0.0", 13 | "title":"Swagger Petstore", 14 | "termsOfService":"http://swagger.io/terms/", 15 | "contact":{ 16 | "email":"apiteam@swagger.io" 17 | }, 18 | "license":{ 19 | "name":"Apache 2.0", 20 | "url":"http://www.apache.org/licenses/LICENSE-2.0.html" 21 | } 22 | }, 23 | "host":"petstore.swagger.io", 24 | "basePath":"/v2", 25 | "tags":[ 26 | { 27 | "name":"pet", 28 | "description":"Everything about your Pets", 29 | "externalDocs":{ 30 | "description":"Find out more", 31 | "url":"http://swagger.io" 32 | } 33 | }, 34 | { 35 | "name":"store", 36 | "description":"Access to Petstore orders" 37 | }, 38 | { 39 | "name":"user", 40 | "description":"Operations about user", 41 | "externalDocs":{ 42 | "description":"Find out more about our store", 43 | "url":"http://swagger.io" 44 | } 45 | } 46 | ], 47 | "schemes":[ 48 | "http" 49 | ], 50 | "paths":{ 51 | "/pet":{ 52 | "post":{ 53 | "tags":[ 54 | "pet" 55 | ], 56 | "summary":"Add a new pet to the store", 57 | "description":"", 58 | "operationId":"addPet", 59 | "consumes":[ 60 | "application/json", 61 | "application/xml" 62 | ], 63 | "produces":[ 64 | "application/xml", 65 | "application/json" 66 | ], 67 | "parameters":[ 68 | { 69 | "in":"body", 70 | "name":"body", 71 | "description":"Pet object that needs to be added to the store", 72 | "required":true, 73 | "schema":{ 74 | "$ref":"#/definitions/Pet" 75 | } 76 | } 77 | ], 78 | "responses":{ 79 | "405":{ 80 | "description":"Invalid input" 81 | } 82 | }, 83 | "security":[ 84 | { 85 | "petstore_auth":[ 86 | "write:pets", 87 | "read:pets" 88 | ] 89 | } 90 | ] 91 | }, 92 | "put":{ 93 | "tags":[ 94 | "pet" 95 | ], 96 | "summary":"Update an existing pet", 97 | "description":"", 98 | "operationId":"updatePet", 99 | "consumes":[ 100 | "application/json", 101 | "application/xml" 102 | ], 103 | "produces":[ 104 | "application/xml", 105 | "application/json" 106 | ], 107 | "parameters":[ 108 | { 109 | "in":"body", 110 | "name":"body", 111 | "description":"Pet object that needs to be added to the store", 112 | "required":true, 113 | "schema":{ 114 | "$ref":"#/definitions/Pet" 115 | } 116 | } 117 | ], 118 | "responses":{ 119 | "400":{ 120 | "description":"Invalid ID supplied" 121 | }, 122 | "404":{ 123 | "description":"Pet not found" 124 | }, 125 | "405":{ 126 | "description":"Validation exception" 127 | } 128 | }, 129 | "security":[ 130 | { 131 | "petstore_auth":[ 132 | "write:pets", 133 | "read:pets" 134 | ] 135 | } 136 | ] 137 | } 138 | }, 139 | "/pet/findByStatus":{ 140 | "get":{ 141 | "tags":[ 142 | "pet" 143 | ], 144 | "summary":"Finds Pets by status", 145 | "description":"Multiple status values can be provided with comma separated strings", 146 | "operationId":"findPetsByStatus", 147 | "produces":[ 148 | "application/xml", 149 | "application/json" 150 | ], 151 | "parameters":[ 152 | { 153 | "name":"status", 154 | "in":"query", 155 | "description":"Status values that need to be considered for filter", 156 | "required":true, 157 | "type":"array", 158 | "items":{ 159 | "type":"string", 160 | "enum":[ 161 | "available", 162 | "pending", 163 | "sold" 164 | ], 165 | "default":"available" 166 | }, 167 | "collectionFormat":"multi" 168 | } 169 | ], 170 | "responses":{ 171 | "200":{ 172 | "description":"successful operation", 173 | "schema":{ 174 | "type":"array", 175 | "items":{ 176 | "$ref":"#/definitions/Pet" 177 | } 178 | } 179 | }, 180 | "400":{ 181 | "description":"Invalid status value" 182 | } 183 | }, 184 | "security":[ 185 | { 186 | "petstore_auth":[ 187 | "write:pets", 188 | "read:pets" 189 | ] 190 | } 191 | ] 192 | } 193 | }, 194 | "/pet/findByTags":{ 195 | "get":{ 196 | "tags":[ 197 | "pet" 198 | ], 199 | "summary":"Finds Pets by tags", 200 | "description":"Muliple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.", 201 | "operationId":"findPetsByTags", 202 | "produces":[ 203 | "application/xml", 204 | "application/json" 205 | ], 206 | "parameters":[ 207 | { 208 | "name":"tags", 209 | "in":"query", 210 | "description":"Tags to filter by", 211 | "required":true, 212 | "type":"array", 213 | "items":{ 214 | "type":"string" 215 | }, 216 | "collectionFormat":"multi" 217 | } 218 | ], 219 | "responses":{ 220 | "200":{ 221 | "description":"successful operation", 222 | "schema":{ 223 | "type":"array", 224 | "items":{ 225 | "$ref":"#/definitions/Pet" 226 | } 227 | } 228 | }, 229 | "400":{ 230 | "description":"Invalid tag value" 231 | } 232 | }, 233 | "security":[ 234 | { 235 | "petstore_auth":[ 236 | "write:pets", 237 | "read:pets" 238 | ] 239 | } 240 | ], 241 | "deprecated":true 242 | } 243 | }, 244 | "/pet/{petId}":{ 245 | "get":{ 246 | "tags":[ 247 | "pet" 248 | ], 249 | "summary":"Find pet by ID", 250 | "description":"Returns a single pet", 251 | "operationId":"getPetById", 252 | "produces":[ 253 | "application/xml", 254 | "application/json" 255 | ], 256 | "parameters":[ 257 | { 258 | "name":"petId", 259 | "in":"path", 260 | "description":"ID of pet to return", 261 | "required":true, 262 | "type":"integer", 263 | "format":"int64" 264 | } 265 | ], 266 | "responses":{ 267 | "200":{ 268 | "description":"successful operation", 269 | "schema":{ 270 | "$ref":"#/definitions/Pet" 271 | } 272 | }, 273 | "400":{ 274 | "description":"Invalid ID supplied" 275 | }, 276 | "404":{ 277 | "description":"Pet not found" 278 | } 279 | }, 280 | "security":[ 281 | { 282 | "api_key":[] 283 | } 284 | ] 285 | }, 286 | "post":{ 287 | "tags":[ 288 | "pet" 289 | ], 290 | "summary":"Updates a pet in the store with form data", 291 | "description":"", 292 | "operationId":"updatePetWithForm", 293 | "consumes":[ 294 | "application/x-www-form-urlencoded" 295 | ], 296 | "produces":[ 297 | "application/xml", 298 | "application/json" 299 | ], 300 | "parameters":[ 301 | { 302 | "name":"petId", 303 | "in":"path", 304 | "description":"ID of pet that needs to be updated", 305 | "required":true, 306 | "type":"integer", 307 | "format":"int64" 308 | }, 309 | { 310 | "name":"name", 311 | "in":"formData", 312 | "description":"Updated name of the pet", 313 | "required":false, 314 | "type":"string" 315 | }, 316 | { 317 | "name":"status", 318 | "in":"formData", 319 | "description":"Updated status of the pet", 320 | "required":false, 321 | "type":"string" 322 | } 323 | ], 324 | "responses":{ 325 | "405":{ 326 | "description":"Invalid input" 327 | } 328 | }, 329 | "security":[ 330 | { 331 | "petstore_auth":[ 332 | "write:pets", 333 | "read:pets" 334 | ] 335 | } 336 | ] 337 | }, 338 | "delete":{ 339 | "tags":[ 340 | "pet" 341 | ], 342 | "summary":"Deletes a pet", 343 | "description":"", 344 | "operationId":"deletePet", 345 | "produces":[ 346 | "application/xml", 347 | "application/json" 348 | ], 349 | "parameters":[ 350 | { 351 | "name":"api_key", 352 | "in":"header", 353 | "required":false, 354 | "type":"string" 355 | }, 356 | { 357 | "name":"petId", 358 | "in":"path", 359 | "description":"Pet id to delete", 360 | "required":true, 361 | "type":"integer", 362 | "format":"int64" 363 | } 364 | ], 365 | "responses":{ 366 | "400":{ 367 | "description":"Invalid ID supplied" 368 | }, 369 | "404":{ 370 | "description":"Pet not found" 371 | } 372 | }, 373 | "security":[ 374 | { 375 | "petstore_auth":[ 376 | "write:pets", 377 | "read:pets" 378 | ] 379 | } 380 | ] 381 | } 382 | }, 383 | "/pet/{petId}/uploadImage":{ 384 | "post":{ 385 | "tags":[ 386 | "pet" 387 | ], 388 | "summary":"uploads an image", 389 | "description":"", 390 | "operationId":"uploadFile", 391 | "consumes":[ 392 | "multipart/form-data" 393 | ], 394 | "produces":[ 395 | "application/json" 396 | ], 397 | "parameters":[ 398 | { 399 | "name":"petId", 400 | "in":"path", 401 | "description":"ID of pet to update", 402 | "required":true, 403 | "type":"integer", 404 | "format":"int64" 405 | }, 406 | { 407 | "name":"additionalMetadata", 408 | "in":"formData", 409 | "description":"Additional data to pass to server", 410 | "required":false, 411 | "type":"string" 412 | }, 413 | { 414 | "name":"file", 415 | "in":"formData", 416 | "description":"file to upload", 417 | "required":false, 418 | "type":"file" 419 | } 420 | ], 421 | "responses":{ 422 | "200":{ 423 | "description":"successful operation", 424 | "schema":{ 425 | "$ref":"#/definitions/ApiResponse" 426 | } 427 | } 428 | }, 429 | "security":[ 430 | { 431 | "petstore_auth":[ 432 | "write:pets", 433 | "read:pets" 434 | ] 435 | } 436 | ] 437 | } 438 | }, 439 | "/store/inventory":{ 440 | "get":{ 441 | "tags":[ 442 | "store" 443 | ], 444 | "summary":"Returns pet inventories by status", 445 | "description":"Returns a map of status codes to quantities", 446 | "operationId":"getInventory", 447 | "produces":[ 448 | "application/json" 449 | ], 450 | "parameters":[ 451 | 452 | ], 453 | "responses":{ 454 | "200":{ 455 | "description":"successful operation", 456 | "schema":{ 457 | "type":"object", 458 | "additionalProperties":{ 459 | "type":"integer", 460 | "format":"int32" 461 | } 462 | } 463 | } 464 | }, 465 | "security":[ 466 | { 467 | "api_key":[ 468 | 469 | ] 470 | } 471 | ] 472 | } 473 | }, 474 | "/store/order":{ 475 | "post":{ 476 | "tags":[ 477 | "store" 478 | ], 479 | "summary":"Place an order for a pet", 480 | "description":"", 481 | "operationId":"placeOrder", 482 | "produces":[ 483 | "application/xml", 484 | "application/json" 485 | ], 486 | "parameters":[ 487 | { 488 | "in":"body", 489 | "name":"body", 490 | "description":"order placed for purchasing the pet", 491 | "required":true, 492 | "schema":{ 493 | "$ref":"#/definitions/Order" 494 | } 495 | } 496 | ], 497 | "responses":{ 498 | "200":{ 499 | "description":"successful operation", 500 | "schema":{ 501 | "$ref":"#/definitions/Order" 502 | } 503 | }, 504 | "400":{ 505 | "description":"Invalid Order" 506 | } 507 | } 508 | } 509 | }, 510 | "/store/order/{orderId}":{ 511 | "get":{ 512 | "tags":[ 513 | "store" 514 | ], 515 | "summary":"Find purchase order by ID", 516 | "description":"For valid response try integer IDs with value >= 1 and <= 10. Other values will generated exceptions", 517 | "operationId":"getOrderById", 518 | "produces":[ 519 | "application/xml", 520 | "application/json" 521 | ], 522 | "parameters":[ 523 | { 524 | "name":"orderId", 525 | "in":"path", 526 | "description":"ID of pet that needs to be fetched", 527 | "required":true, 528 | "type":"integer", 529 | "maximum":10.0, 530 | "minimum":1.0, 531 | "format":"int64" 532 | } 533 | ], 534 | "responses":{ 535 | "200":{ 536 | "description":"successful operation", 537 | "schema":{ 538 | "$ref":"#/definitions/Order" 539 | } 540 | }, 541 | "400":{ 542 | "description":"Invalid ID supplied" 543 | }, 544 | "404":{ 545 | "description":"Order not found" 546 | } 547 | } 548 | }, 549 | "delete":{ 550 | "tags":[ 551 | "store" 552 | ], 553 | "summary":"Delete purchase order by ID", 554 | "description":"For valid response try integer IDs with positive integer value. Negative or non-integer values will generate API errors", 555 | "operationId":"deleteOrder", 556 | "produces":[ 557 | "application/xml", 558 | "application/json" 559 | ], 560 | "parameters":[ 561 | { 562 | "name":"orderId", 563 | "in":"path", 564 | "description":"ID of the order that needs to be deleted", 565 | "required":true, 566 | "type":"integer", 567 | "minimum":1.0, 568 | "format":"int64" 569 | } 570 | ], 571 | "responses":{ 572 | "400":{ 573 | "description":"Invalid ID supplied" 574 | }, 575 | "404":{ 576 | "description":"Order not found" 577 | } 578 | } 579 | } 580 | }, 581 | "/user":{ 582 | "post":{ 583 | "tags":[ 584 | "user" 585 | ], 586 | "summary":"Create user", 587 | "description":"This can only be done by the logged in user.", 588 | "operationId":"createUser", 589 | "produces":[ 590 | "application/xml", 591 | "application/json" 592 | ], 593 | "parameters":[ 594 | { 595 | "in":"body", 596 | "name":"body", 597 | "description":"Created user object", 598 | "required":true, 599 | "schema":{ 600 | "$ref":"#/definitions/User" 601 | } 602 | } 603 | ], 604 | "responses":{ 605 | "default":{ 606 | "description":"successful operation" 607 | } 608 | } 609 | } 610 | }, 611 | "/user/createWithArray":{ 612 | "post":{ 613 | "tags":[ 614 | "user" 615 | ], 616 | "summary":"Creates list of users with given input array", 617 | "description":"", 618 | "operationId":"createUsersWithArrayInput", 619 | "produces":[ 620 | "application/xml", 621 | "application/json" 622 | ], 623 | "parameters":[ 624 | { 625 | "in":"body", 626 | "name":"body", 627 | "description":"List of user object", 628 | "required":true, 629 | "schema":{ 630 | "type":"array", 631 | "items":{ 632 | "$ref":"#/definitions/User" 633 | } 634 | } 635 | } 636 | ], 637 | "responses":{ 638 | "default":{ 639 | "description":"successful operation" 640 | } 641 | } 642 | } 643 | }, 644 | "/user/createWithList":{ 645 | "post":{ 646 | "tags":[ 647 | "user" 648 | ], 649 | "summary":"Creates list of users with given input array", 650 | "description":"", 651 | "operationId":"createUsersWithListInput", 652 | "produces":[ 653 | "application/xml", 654 | "application/json" 655 | ], 656 | "parameters":[ 657 | { 658 | "in":"body", 659 | "name":"body", 660 | "description":"List of user object", 661 | "required":true, 662 | "schema":{ 663 | "type":"array", 664 | "items":{ 665 | "$ref":"#/definitions/User" 666 | } 667 | } 668 | } 669 | ], 670 | "responses":{ 671 | "default":{ 672 | "description":"successful operation" 673 | } 674 | } 675 | } 676 | }, 677 | "/user/login":{ 678 | "get":{ 679 | "tags":[ 680 | "user" 681 | ], 682 | "summary":"Logs user into the system", 683 | "description":"", 684 | "operationId":"loginUser", 685 | "produces":[ 686 | "application/xml", 687 | "application/json" 688 | ], 689 | "parameters":[ 690 | { 691 | "name":"username", 692 | "in":"query", 693 | "description":"The user name for login", 694 | "required":true, 695 | "type":"string" 696 | }, 697 | { 698 | "name":"password", 699 | "in":"query", 700 | "description":"The password for login in clear text", 701 | "required":true, 702 | "type":"string" 703 | } 704 | ], 705 | "responses":{ 706 | "200":{ 707 | "description":"successful operation", 708 | "schema":{ 709 | "type":"string" 710 | }, 711 | "headers":{ 712 | "X-Rate-Limit":{ 713 | "type":"integer", 714 | "format":"int32", 715 | "description":"calls per hour allowed by the user" 716 | }, 717 | "X-Expires-After":{ 718 | "type":"string", 719 | "format":"date-time", 720 | "description":"date in UTC when token expires" 721 | } 722 | } 723 | }, 724 | "400":{ 725 | "description":"Invalid username/password supplied" 726 | } 727 | } 728 | } 729 | }, 730 | "/user/logout":{ 731 | "get":{ 732 | "tags":[ 733 | "user" 734 | ], 735 | "summary":"Logs out current logged in user session", 736 | "description":"", 737 | "operationId":"logoutUser", 738 | "produces":[ 739 | "application/xml", 740 | "application/json" 741 | ], 742 | "parameters":[ 743 | 744 | ], 745 | "responses":{ 746 | "default":{ 747 | "description":"successful operation" 748 | } 749 | } 750 | } 751 | }, 752 | "/user/{username}":{ 753 | "get":{ 754 | "tags":[ 755 | "user" 756 | ], 757 | "summary":"Get user by user name", 758 | "description":"", 759 | "operationId":"getUserByName", 760 | "produces":[ 761 | "application/xml", 762 | "application/json" 763 | ], 764 | "parameters":[ 765 | { 766 | "name":"username", 767 | "in":"path", 768 | "description":"The name that needs to be fetched. Use user1 for testing. ", 769 | "required":true, 770 | "type":"string" 771 | } 772 | ], 773 | "responses":{ 774 | "200":{ 775 | "description":"successful operation", 776 | "schema":{ 777 | "$ref":"#/definitions/User" 778 | } 779 | }, 780 | "400":{ 781 | "description":"Invalid username supplied" 782 | }, 783 | "404":{ 784 | "description":"User not found" 785 | } 786 | } 787 | }, 788 | "put":{ 789 | "tags":[ 790 | "user" 791 | ], 792 | "summary":"Updated user", 793 | "description":"This can only be done by the logged in user.", 794 | "operationId":"updateUser", 795 | "produces":[ 796 | "application/xml", 797 | "application/json" 798 | ], 799 | "parameters":[ 800 | { 801 | "name":"username", 802 | "in":"path", 803 | "description":"name that need to be updated", 804 | "required":true, 805 | "type":"string" 806 | }, 807 | { 808 | "in":"body", 809 | "name":"body", 810 | "description":"Updated user object", 811 | "required":true, 812 | "schema":{ 813 | "$ref":"#/definitions/User" 814 | } 815 | } 816 | ], 817 | "responses":{ 818 | "400":{ 819 | "description":"Invalid user supplied" 820 | }, 821 | "404":{ 822 | "description":"User not found" 823 | } 824 | } 825 | }, 826 | "delete":{ 827 | "tags":[ 828 | "user" 829 | ], 830 | "summary":"Delete user", 831 | "description":"This can only be done by the logged in user.", 832 | "operationId":"deleteUser", 833 | "produces":[ 834 | "application/xml", 835 | "application/json" 836 | ], 837 | "parameters":[ 838 | { 839 | "name":"username", 840 | "in":"path", 841 | "description":"The name that needs to be deleted", 842 | "required":true, 843 | "type":"string" 844 | } 845 | ], 846 | "responses":{ 847 | "400":{ 848 | "description":"Invalid username supplied" 849 | }, 850 | "404":{ 851 | "description":"User not found" 852 | } 853 | } 854 | } 855 | } 856 | }, 857 | "securityDefinitions":{ 858 | "petstore_auth":{ 859 | "type":"oauth2", 860 | "authorizationUrl":"http://petstore.swagger.io/oauth/dialog", 861 | "flow":"implicit", 862 | "scopes":{ 863 | "write:pets":"modify pets in your account", 864 | "read:pets":"read your pets" 865 | } 866 | }, 867 | "api_key":{ 868 | "type":"apiKey", 869 | "name":"api_key", 870 | "in":"header" 871 | } 872 | }, 873 | "definitions":{ 874 | "Order":{ 875 | "type":"object", 876 | "properties":{ 877 | "id":{ 878 | "type":"integer", 879 | "format":"int64" 880 | }, 881 | "petId":{ 882 | "type":"integer", 883 | "format":"int64" 884 | }, 885 | "quantity":{ 886 | "type":"integer", 887 | "format":"int32" 888 | }, 889 | "shipDate":{ 890 | "type":"string", 891 | "format":"date-time" 892 | }, 893 | "status":{ 894 | "type":"string", 895 | "description":"Order Status", 896 | "enum":[ 897 | "placed", 898 | "approved", 899 | "delivered" 900 | ] 901 | }, 902 | "complete":{ 903 | "type":"boolean", 904 | "default":false 905 | } 906 | }, 907 | "xml":{ 908 | "name":"Order" 909 | } 910 | }, 911 | "Category":{ 912 | "type":"object", 913 | "properties":{ 914 | "id":{ 915 | "type":"integer", 916 | "format":"int64" 917 | }, 918 | "name":{ 919 | "type":"string" 920 | } 921 | }, 922 | "xml":{ 923 | "name":"Category" 924 | } 925 | }, 926 | "User":{ 927 | "type":"object", 928 | "properties":{ 929 | "id":{ 930 | "type":"integer", 931 | "format":"int64" 932 | }, 933 | "username":{ 934 | "type":"string" 935 | }, 936 | "firstName":{ 937 | "type":"string" 938 | }, 939 | "lastName":{ 940 | "type":"string" 941 | }, 942 | "email":{ 943 | "type":"string" 944 | }, 945 | "password":{ 946 | "type":"string" 947 | }, 948 | "phone":{ 949 | "type":"string" 950 | }, 951 | "userStatus":{ 952 | "type":"integer", 953 | "format":"int32", 954 | "description":"User Status" 955 | } 956 | }, 957 | "xml":{ 958 | "name":"User" 959 | } 960 | }, 961 | "Tag":{ 962 | "type":"object", 963 | "properties":{ 964 | "id":{ 965 | "type":"integer", 966 | "format":"int64" 967 | }, 968 | "name":{ 969 | "type":"string" 970 | } 971 | }, 972 | "xml":{ 973 | "name":"Tag" 974 | } 975 | }, 976 | "Pet":{ 977 | "type":"object", 978 | "required":[ 979 | "name", 980 | "photoUrls" 981 | ], 982 | "properties":{ 983 | "id":{ 984 | "type":"integer", 985 | "format":"int64" 986 | }, 987 | "category":{ 988 | "$ref":"#/definitions/Category" 989 | }, 990 | "name":{ 991 | "type":"string", 992 | "example":"doggie" 993 | }, 994 | "photoUrls":{ 995 | "type":"array", 996 | "xml":{ 997 | "name":"photoUrl", 998 | "wrapped":true 999 | }, 1000 | "items":{ 1001 | "type":"string" 1002 | } 1003 | }, 1004 | "tags":{ 1005 | "type":"array", 1006 | "xml":{ 1007 | "name":"tag", 1008 | "wrapped":true 1009 | }, 1010 | "items":{ 1011 | "$ref":"#/definitions/Tag" 1012 | } 1013 | }, 1014 | "status":{ 1015 | "type":"string", 1016 | "description":"pet status in the store", 1017 | "enum":[ 1018 | "available", 1019 | "pending", 1020 | "sold" 1021 | ] 1022 | } 1023 | }, 1024 | "xml":{ 1025 | "name":"Pet" 1026 | } 1027 | }, 1028 | "ApiResponse":{ 1029 | "type":"object", 1030 | "properties":{ 1031 | "code":{ 1032 | "type":"integer", 1033 | "format":"int32" 1034 | }, 1035 | "type":{ 1036 | "type":"string" 1037 | }, 1038 | "message":{ 1039 | "type":"string" 1040 | } 1041 | } 1042 | } 1043 | }, 1044 | "externalDocs":{ 1045 | "description":"Find out more about Swagger", 1046 | "url":"http://swagger.io" 1047 | } 1048 | }; 1049 | -------------------------------------------------------------------------------- /src/app/services/apidoc.service.spec.ts: -------------------------------------------------------------------------------- 1 | import {ApiDocService} from './apidoc.service'; 2 | import { 3 | Http, BaseRequestOptions, XHRBackend, HttpModule, Response, ResponseOptions, Request, 4 | RequestOptionsArgs 5 | } from '@angular/http'; 6 | import {async, TestBed, inject} from '@angular/core/testing'; 7 | import {MockBackend, MockConnection} from '@angular/http/testing'; 8 | 9 | import * as HttpApiMocks from './api_http_mocks'; 10 | import {ApiDefinition} from '../model/api-definition'; 11 | import {ContactObject, LicenseObject} from '../model/apidoc'; 12 | import {Observable} from 'rxjs'; 13 | 14 | /** 15 | * Apidoc service unit tests 16 | * Created by Michael DESIGAUD on 20/11/2016. 17 | */ 18 | 19 | describe('ApiDocService', () => { 20 | 21 | let apiDoc: ApiDefinition; 22 | 23 | beforeEach(async(() => { 24 | TestBed.configureTestingModule({ 25 | providers: [ 26 | BaseRequestOptions, 27 | MockBackend, 28 | ApiDocService, 29 | { 30 | deps: [ 31 | MockBackend, 32 | BaseRequestOptions 33 | ], 34 | provide: Http, 35 | useFactory: (backend: XHRBackend, defaultOptions: BaseRequestOptions) => { 36 | return new Http(backend, defaultOptions); 37 | } 38 | } 39 | ], 40 | imports: [ 41 | HttpModule 42 | ] 43 | }); 44 | TestBed.compileComponents(); 45 | })); 46 | 47 | it('Initialization should be ok', async(inject([MockBackend, ApiDocService], (backend: MockBackend, service: ApiDocService) => { 48 | expect(service.apiUrl).toBe(HttpApiMocks.API_MOCK_URL); 49 | expect(service.apiValid).toBeFalsy(); 50 | }))); 51 | 52 | it('getApi method', async(inject([MockBackend, ApiDocService], (backend: MockBackend, service: ApiDocService) => { 53 | backend.connections.subscribe( 54 | (connection: MockConnection) => { 55 | connection.mockRespond(new Response( 56 | new ResponseOptions({ 57 | body: JSON.stringify(HttpApiMocks.HTTP_GET_API_MOCK) 58 | }))); 59 | }); 60 | 61 | service.getApi().subscribe((api: any) => { 62 | apiDoc = new ApiDefinition(HttpApiMocks.HTTP_GET_API_MOCK); 63 | expect(api).toBeDefined(); 64 | expect(service.apiValid).toBeTruthy(); 65 | expect(apiDoc).toBeDefined(); 66 | 67 | expect(apiDoc.info).toBeDefined(); 68 | expect(apiDoc.info.contact).toBeDefined(); 69 | expect(apiDoc.info.contact).toEqual(new ContactObject(HttpApiMocks.HTTP_GET_API_MOCK.info.contact)); 70 | expect(apiDoc.info.license).toBeDefined(); 71 | expect(apiDoc.info.license).toEqual(new LicenseObject(HttpApiMocks.HTTP_GET_API_MOCK.info.license)); 72 | expect(apiDoc.info.description).toBeDefined(); 73 | expect(apiDoc.info.description).toEqual(HttpApiMocks.HTTP_GET_API_MOCK.info.description); 74 | expect(apiDoc.info.title).toBeDefined(); 75 | expect(apiDoc.info.title).toEqual(HttpApiMocks.HTTP_GET_API_MOCK.info.title); 76 | 77 | expect(apiDoc.paths).toBeDefined(); 78 | expect(apiDoc.paths.length).toBe(Object.keys(HttpApiMocks.HTTP_GET_API_MOCK.paths).length); 79 | expect(apiDoc.definitions.length).toBe(Object.keys(HttpApiMocks.HTTP_GET_API_MOCK.definitions).length); 80 | expect(apiDoc.tags.length).toBe(Object.keys(HttpApiMocks.HTTP_GET_API_MOCK.tags).length); 81 | 82 | expect(apiDoc.externalDocs).toBeDefined(); 83 | expect(apiDoc.externalDocs.description).toBeDefined(); 84 | expect(apiDoc.externalDocs.description).toEqual(HttpApiMocks.HTTP_GET_API_MOCK.externalDocs.description); 85 | expect(apiDoc.externalDocs.url).toBeDefined(); 86 | expect(apiDoc.externalDocs.url).toEqual(HttpApiMocks.HTTP_GET_API_MOCK.externalDocs.url); 87 | }); 88 | }))); 89 | }); 90 | -------------------------------------------------------------------------------- /src/app/services/apidoc.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {Http, Response, Request, RequestOptions, Headers} from '@angular/http'; 3 | import {Observer} from 'rxjs/Observer'; 4 | import {Observable} from 'rxjs/Observable'; 5 | import {ApiResult} from '../model/api-result'; 6 | import {ApiDefinition} from '../model/api-definition'; 7 | import {OperationObject} from '../model/api-operation'; 8 | import {ParameterObject} from '../model/api-parameter'; 9 | import * as Config from '../utils/env.config'; 10 | 11 | import * as _ from 'lodash'; 12 | 13 | const HEADER_CONTENT_TYPE = 'Content-Type'; 14 | const HEADER_ACCEPT = 'Accept'; 15 | 16 | const DEFAULT_API = 'http://petstore.swagger.io/v2/swagger.json'; 17 | 18 | 19 | @Injectable() 20 | export class ApiDocService { 21 | apiDoc: ApiDefinition; 22 | apiUrl: string; 23 | apiValid: boolean = false; 24 | constructor(private http: Http) { 25 | if (!localStorage.getItem(Config.LOCAL_STORAGE_API_URL)) { 26 | localStorage.setItem(Config.LOCAL_STORAGE_API_URL, this.getDefaultApi()); 27 | } 28 | if (!localStorage.getItem(Config.LOCAL_STORAGE_CHART_TYPE)) { 29 | localStorage.setItem(Config.LOCAL_STORAGE_CHART_TYPE, Config.CHART_TYPE_LINE); 30 | } 31 | this.apiUrl = localStorage.getItem(Config.LOCAL_STORAGE_API_URL); 32 | } 33 | getDefaultApi(): string { 34 | return DEFAULT_API; 35 | } 36 | onChangeApi(apiUrl: string): void { 37 | localStorage.setItem('API-URL', apiUrl); 38 | window.location.reload(); 39 | } 40 | getApi(): Observable { 41 | if (this.apiDoc) { 42 | console.log('Getting doc definition from cache'); 43 | return Observable.create((observer: Observer) => { 44 | this.apiValid = true; 45 | return observer.next(this.apiDoc); 46 | }); 47 | } 48 | console.log('Getting doc definition from server'); 49 | return this.http.get(this.apiUrl).map((res: Response) => { 50 | this.apiDoc = new ApiDefinition(res.json()); 51 | this.apiValid = true; 52 | return this.apiDoc; 53 | }); 54 | } 55 | sendRequest(operation: OperationObject): Observable { 56 | let apiResult: ApiResult = new ApiResult(); 57 | 58 | let reqOptions: RequestOptions = new RequestOptions(); 59 | reqOptions.method = operation.name; 60 | reqOptions.url = this.apiDoc.baseUrl + operation.getRequestUrl(false); 61 | 62 | let headers: Headers = new Headers(); 63 | if (operation.consumes && !_.isEmpty(operation.consumes)) { 64 | if (operation.consumes.length === 1 || !operation.consume.selected) { 65 | headers.set(HEADER_CONTENT_TYPE, operation.consumes[0]); 66 | } else { 67 | headers.set(HEADER_CONTENT_TYPE, operation.consume.selected); 68 | } 69 | } else if (operation.consume.selected) { 70 | headers.set(HEADER_CONTENT_TYPE, operation.consume.selected); 71 | } 72 | if (!operation.isDeleteMethod() && operation.produces && !_.isEmpty(operation.produces)) { 73 | if (operation.produces.length === 1 || !operation.produce.selected) { 74 | headers.set(HEADER_ACCEPT, operation.produces[0]); 75 | } else { 76 | headers.set(HEADER_ACCEPT, operation.produce.selected); 77 | } 78 | } else if (operation.produce.selected) { 79 | headers.set(HEADER_ACCEPT, operation.produce.selected); 80 | } 81 | 82 | operation.parameters.forEach((param: ParameterObject) => { 83 | if (param.isHeaderParam()) { 84 | headers.set(param.name, param.value.selected); 85 | } 86 | }); 87 | 88 | if (operation.isWriteMethod()) { 89 | if (operation.isConsumeJson()) { 90 | reqOptions.body = operation.dataJson; 91 | } 92 | if (operation.isConsumeXml()) { 93 | reqOptions.body = operation.dataJson; 94 | } 95 | if (operation.isConsumeFormUrlEncoded()) { 96 | let formBody = ''; 97 | operation.parameters.forEach((param: ParameterObject) => { 98 | if (param.isFormParam()) { 99 | if (formBody !== '') { 100 | formBody += '&'; 101 | } 102 | formBody += param.name + '=' + param.value.selected; 103 | } 104 | }); 105 | reqOptions.body = formBody; 106 | } 107 | // TODO override HTTP class 108 | if (operation.isConsumeMultipartFormData()) { 109 | let boundary: string = '------FormData' + Math.random(); 110 | let body = ''; 111 | operation.parameters.forEach((parameter: ParameterObject) => { 112 | if (parameter.isFormParam()) { 113 | body += '--' + boundary + '\r\n'; 114 | if (parameter.isTypeFile()) { 115 | let file: File = parameter.value.selected.file; 116 | body += 'Content-Disposition: form-data; name="' + parameter.name + '"; filename="' + file.name + '"\r\n'; 117 | body += 'Content-Type: ' + file.type + '\r\n\r\n'; 118 | } else { 119 | body += 'Content-Disposition: form-data; name="' + parameter.name + '";\r\n\r\n'; 120 | body += parameter.value.selected + '\r\n'; 121 | } 122 | } 123 | }); 124 | body += '--' + boundary + '--'; 125 | headers.set(HEADER_CONTENT_TYPE, headers.get(HEADER_CONTENT_TYPE) + '; boundary=' + boundary); 126 | reqOptions.body = body; 127 | } 128 | } 129 | reqOptions.headers = headers; 130 | console.log('Calling api with options', reqOptions); 131 | 132 | // Angular 2 RC5 fix via setting body to '' (see https: //github.com/angular/angular/issues/10612) 133 | if (reqOptions.body === null || typeof reqOptions.body === 'undefined') { 134 | reqOptions.body = ''; 135 | } 136 | 137 | return this.http.request(new Request(reqOptions)).map((res: Response) => { 138 | apiResult.operation = operation; 139 | apiResult.endDate = new Date(); 140 | try { 141 | if (operation.isProduceJson()) { 142 | apiResult.message = res.json(); 143 | } else { 144 | apiResult.message = res.text(); 145 | } 146 | } catch (error) { 147 | apiResult.message = res.text(); 148 | if (_.isEmpty(apiResult.message.trim())) { 149 | apiResult.message = 'No content'; 150 | } 151 | } 152 | apiResult.status = res.status; 153 | 154 | return apiResult; 155 | }); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/app/utils/env.config.ts: -------------------------------------------------------------------------------- 1 | export const LOCAL_STORAGE_CHART_TYPE = 'CHART_TYPE'; 2 | export const LOCAL_STORAGE_API_URL = 'API-URL'; 3 | export const CHART_TYPE_LINE = 'Line'; 4 | export const CHART_TYPE_BAR = 'Bar'; 5 | export const LOCAL_STORAGE_THEME = 'THEME'; 6 | 7 | export const THEMES = [ 8 | { 9 | label: 'Redfroggy', 10 | value: 'redfroggy', 11 | activeClass: 'red darken-4 white-text', 12 | itemSelected: '#e0e0e0', 13 | itemTextColor: 'black-text', 14 | mainClass: 'grey darken-4', 15 | buttonClass: 'red darken-2', 16 | button: '#D32F2F', 17 | 'fab-button': 'red darken-4', 18 | socialMediaClass: 'redfroggy-theme', 19 | ribbon: '' + 20 | ' ' 24 | }, 25 | { 26 | label: 'Blue', 27 | value: 'blue', 28 | activeClass: 'pink accent-2 white-text', 29 | itemSelected: '#ff5a92', 30 | itemTextColor: 'white-text', 31 | mainClass: 'cyan', 32 | buttonClass: 'pink accent-2', 33 | button: '#ff5a92', 34 | 'fab-button': 'teal', 35 | socialMediaClass: 'blue-theme', 36 | ribbon: '' + 37 | 'Fork me on GitHub' 41 | } 42 | ]; 43 | -------------------------------------------------------------------------------- /src/app/vendor.ts: -------------------------------------------------------------------------------- 1 | // Angular 2 | import '@angular/platform-browser'; 3 | import '@angular/platform-browser-dynamic'; 4 | import '@angular/platform-server'; 5 | import '@angular/core'; 6 | import '@angular/common'; 7 | import '@angular/http'; 8 | import '@angular/router'; 9 | import '@angular/compiler'; 10 | import '@angular/forms'; 11 | import '@angular/upgrade'; 12 | 13 | 14 | // RxJS 15 | import 'rxjs'; 16 | 17 | // Polyfills 18 | import 'core-js/es6'; 19 | import 'core-js/es7/reflect'; 20 | require('zone.js/dist/zone'); 21 | 22 | require('zone.js/dist/long-stack-trace-zone'); 23 | 24 | let $ = jQuery = require('jquery/dist/jquery'); 25 | window['jQuery'] = $; 26 | window['$'] = $; 27 | 28 | // Angular 2 29 | import '@angular/core'; 30 | import '@angular/common'; 31 | import '@angular/http'; 32 | 33 | // RxJS 34 | import 'rxjs/add/operator/map'; 35 | import 'rxjs/add/operator/mergeMap'; 36 | import 'rxjs/add/observable/of'; 37 | 38 | // TypeScript 39 | import 'typescript/lib/typescript'; 40 | 41 | // Lodash 42 | import 'lodash/lodash'; 43 | 44 | // Twitter Bootstrap 45 | import 'bootstrap/dist/js/bootstrap.js'; 46 | 47 | // Materialize css 48 | import 'materialize-css/dist/css/materialize.css'; 49 | import 'materialize-css/dist/js/materialize.js'; 50 | 51 | // Font awesome 52 | import 'font-awesome/css/font-awesome.css'; 53 | 54 | // Moment 55 | window.moment = require('moment/moment.js'); 56 | 57 | // ChartJS 58 | window.Chart = require('chart.js/src/chart.js'); 59 | 60 | // x2js 61 | let X2JS = require('x2js/x2js.js'); 62 | window.x2js = new X2JS(); 63 | 64 | // Highlight 65 | import 'highlight.js/styles/default.css'; 66 | let hljs = require('highlight.js/lib/highlight'); 67 | require('highlight.js/lib/index'); 68 | window.hljs = hljs; 69 | 70 | window.vkbeautify = require('vkbeautify'); 71 | 72 | -------------------------------------------------------------------------------- /src/assets/icons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedFroggy/swagger2-angular2-materialize/01338c566bf2a4250dcf7138495d9ccf2451631a/src/assets/icons/favicon.ico -------------------------------------------------------------------------------- /src/assets/styles/chart.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Docs (http://getbootstrap.com) 3 | * Copyright 2011-2014 Twitter, Inc. 4 | * Licensed under the Creative Commons Attribution 3.0 Unported License. For 5 | * details, see http://creativecommons.org/licenses/by/3.0/. 6 | */ 7 | 8 | .h1, .h2, .h3, h1, h2, h3 { 9 | margin-top: 20px; 10 | margin-bottom: 10px; 11 | } 12 | 13 | .h1, h1 { 14 | font-size: 36px; 15 | } 16 | 17 | .btn-group-lg > .btn, .btn-lg { 18 | font-size: 18px; 19 | } 20 | 21 | section { 22 | padding-top: 30px; 23 | } 24 | 25 | .bd-pageheader { 26 | margin-top: 51px; 27 | } 28 | 29 | .page-header { 30 | padding-bottom: 9px; 31 | margin: 40px 0 20px; 32 | border-bottom: 1px solid #eee; 33 | } 34 | 35 | .navbar-default .navbar-nav > li > a { 36 | color: #777; 37 | } 38 | 39 | .navbar { 40 | padding: 0; 41 | } 42 | 43 | .navbar-nav .nav-item { 44 | margin-left: 0 !important; 45 | } 46 | 47 | .nav > li > a { 48 | position: relative; 49 | display: block; 50 | padding: 10px 15px; 51 | } 52 | 53 | .nav .navbar-brand { 54 | float: left; 55 | height: 50px; 56 | padding: 15px 15px; 57 | font-size: 18px; 58 | line-height: 20px; 59 | margin-right: 0 !important; 60 | } 61 | 62 | .navbar-brand { 63 | color: #777; 64 | float: left; 65 | height: 50px; 66 | padding: 15px 15px; 67 | font-size: 18px; 68 | line-height: 20px; 69 | } 70 | 71 | .navbar-toggler { 72 | margin-top: 8px; 73 | margin-right: 15px; 74 | } 75 | 76 | .navbar-default .navbar-nav > li > a:focus, .navbar-default .navbar-nav > li > a:hover { 77 | color: #333; 78 | background-color: transparent; 79 | } 80 | 81 | .bd-pageheader, .bs-docs-masthead { 82 | position: relative; 83 | padding: 30px 0; 84 | color: #cdbfe3; 85 | text-align: center; 86 | text-shadow: 0 1px 0 rgba(0, 0, 0, .1); 87 | background-color: #6f5499; 88 | background-image: -webkit-gradient(linear, left top, left bottom, from(#563d7c), to(#6f5499)); 89 | background-image: -webkit-linear-gradient(top, #563d7c 0, #6f5499 100%); 90 | background-image: -o-linear-gradient(top, #563d7c 0, #6f5499 100%); 91 | background-image: linear-gradient(to bottom, #563d7c 0, #6f5499 100%); 92 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#563d7c', endColorstr='#6F5499', GradientType=0); 93 | background-repeat: repeat-x; 94 | } 95 | 96 | .bd-pageheader { 97 | margin-bottom: 40px; 98 | font-size: 20px; 99 | } 100 | 101 | .bd-pageheader h1 { 102 | margin-top: 0; 103 | color: #fff; 104 | } 105 | 106 | .bd-pageheader p { 107 | margin-bottom: 0; 108 | font-weight: 300; 109 | line-height: 1.4; 110 | } 111 | 112 | .bd-pageheader .btn { 113 | margin: 10px 0; 114 | } 115 | 116 | .scrollable-menu .nav-link { 117 | color: #337ab7; 118 | font-size: 14px; 119 | } 120 | 121 | .scrollable-menu .nav-link:hover { 122 | color: #23527c; 123 | background-color: #eee; 124 | } 125 | 126 | @media (min-width: 992px) { 127 | .bd-pageheader h1, .bd-pageheader p { 128 | margin-right: 380px; 129 | } 130 | } 131 | 132 | @media (min-width: 768px) { 133 | .bd-pageheader { 134 | padding-top: 60px; 135 | padding-bottom: 60px; 136 | font-size: 24px; 137 | text-align: left; 138 | } 139 | 140 | .bd-pageheader h1 { 141 | font-size: 60px; 142 | line-height: 1; 143 | } 144 | 145 | .navbar-nav > li > a.nav-link { 146 | padding-top: 15px; 147 | padding-bottom: 15px; 148 | font-size: 14px; 149 | } 150 | 151 | .navbar > .container .navbar-brand, .navbar > .container-fluid .navbar-brand { 152 | margin-left: -15px; 153 | } 154 | } 155 | 156 | @media (max-width: 767px) { 157 | .hidden-xs { 158 | display: none !important; 159 | } 160 | 161 | .navbar .container { 162 | width: 100%; 163 | max-width: 100%; 164 | } 165 | .navbar .container, 166 | .navbar .container .navbar-header { 167 | padding: 0; 168 | margin: 0; 169 | } 170 | } 171 | 172 | @media (max-width: 400px) { 173 | code, kbd { 174 | font-size: 60%; 175 | } 176 | } 177 | 178 | /** 179 | * VH and VW units can cause issues on iOS devices: http://caniuse.com/#feat=viewport-units 180 | * 181 | * To overcome this, create media queries that target the width, height, and orientation of iOS devices. 182 | * It isn't optimal, but there is really no other way to solve the problem. In this example, I am fixing 183 | * the height of element '.scrollable-menu' —which is a full width and height cover image. 184 | * 185 | * iOS Resolution Quick Reference: http://www.iosres.com/ 186 | */ 187 | 188 | .scrollable-menu { 189 | height: 90vh !important; 190 | width: 100vw; 191 | overflow-x: hidden; 192 | padding: 0 0 20px; 193 | } 194 | 195 | /** 196 | * iPad with portrait orientation. 197 | */ 198 | @media all and (device-width: 768px) and (device-height: 1024px) and (orientation: portrait) { 199 | .scrollable-menu { 200 | height: 1024px !important; 201 | } 202 | } 203 | 204 | /** 205 | * iPad with landscape orientation. 206 | */ 207 | @media all and (device-width: 768px) and (device-height: 1024px) and (orientation: landscape) { 208 | .scrollable-menu { 209 | height: 768px !important; 210 | } 211 | } 212 | 213 | /** 214 | * iPhone 5 215 | * You can also target devices with aspect ratio. 216 | */ 217 | @media screen and (device-aspect-ratio: 40/71) { 218 | .scrollable-menu { 219 | height: 500px !important; 220 | } 221 | } 222 | 223 | .navbar-default .navbar-toggle .icon-bar { 224 | background-color: #888; 225 | } 226 | 227 | .navbar-toggle:focus { 228 | outline: 0 229 | } 230 | 231 | .navbar-toggle .icon-bar { 232 | display: block; 233 | width: 22px; 234 | height: 2px; 235 | border-radius: 1px 236 | } 237 | 238 | .navbar-toggle .icon-bar + .icon-bar { 239 | margin-top: 4px 240 | } 241 | 242 | pre { 243 | white-space: pre-wrap; /* CSS 3 */ 244 | white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ 245 | white-space: -pre-wrap; /* Opera 4-6 */ 246 | white-space: -o-pre-wrap; /* Opera 7 */ 247 | word-wrap: break-word; /* Internet Explorer 5.5+ */ 248 | } 249 | 250 | .chart-legend, .bar-legend, .line-legend, .pie-legend, .radar-legend, .polararea-legend, .doughnut-legend { 251 | list-style-type: none; 252 | margin-top: 5px; 253 | text-align: center; 254 | -webkit-padding-start: 0; 255 | -moz-padding-start: 0; 256 | padding-left: 0 257 | } 258 | 259 | .chart-legend li, .bar-legend li, .line-legend li, .pie-legend li, .radar-legend li, .polararea-legend li, .doughnut-legend li { 260 | display: inline-block; 261 | white-space: nowrap; 262 | position: relative; 263 | margin-bottom: 4px; 264 | border-radius: 5px; 265 | padding: 2px 8px 2px 28px; 266 | font-size: smaller; 267 | cursor: default 268 | } 269 | 270 | .chart-legend li span, .bar-legend li span, .line-legend li span, .pie-legend li span, .radar-legend li span, .polararea-legend li span, .doughnut-legend li span { 271 | display: block; 272 | position: absolute; 273 | left: 0; 274 | top: 0; 275 | width: 20px; 276 | height: 20px; 277 | border-radius: 5px 278 | } 279 | .chart {display: block;width: 100%;} -------------------------------------------------------------------------------- /src/assets/styles/styles.css: -------------------------------------------------------------------------------- 1 | /* Main marketing message and sign up button */ 2 | .jumbotron { 3 | text-align: center; 4 | border-bottom: 1px solid #e5e5e5; 5 | } 6 | .jumbotron .btn { 7 | font-size: 21px; 8 | padding: 14px 24px; 9 | } 10 | /* invalid color */ 11 | .input-field input[type=text].ng-invalid { 12 | border-bottom: 1px solid #E57373; 13 | box-shadow: 0 1px 0 0 #E57373; 14 | } 15 | 16 | a.text-lowercase { 17 | text-transform: none; 18 | } 19 | 20 | div.api-detail { 21 | margin-top:8px; 22 | } 23 | 24 | .collection .collection-item.active { 25 | background-color: #e0e0e0; 26 | } 27 | 28 | span.legend { 29 | float: left; 30 | width: 50px; 31 | height: 20px; 32 | margin-bottom: 5px; 33 | margin-right: 5px; 34 | border: 1px solid rgba(0, 0, 0, .2); 35 | } 36 | 37 | .padding-top-15 { 38 | padding-top: 15px !important; 39 | } 40 | 41 | .padding-right-15 { 42 | padding-right: 10px; 43 | } 44 | 45 | i.redfroggy-theme { 46 | color:#D32F2F; 47 | } 48 | 49 | i.blue-theme { 50 | color:#ff5a92; 51 | } 52 | 53 | div.footer-api { 54 | line-height: 42px !important; 55 | background-color: inherit !important; 56 | } 57 | 58 | .breadcrumb:before { 59 | color: black; 60 | margin: -4px 10px 0 8px; 61 | } 62 | 63 | .breadcrumb:last-child { 64 | color: #039be5 !important; 65 | } 66 | 67 | span.footer-label { 68 | color: white; 69 | font-weight: bold; 70 | } 71 | 72 | span.github-ribbon { 73 | position:absolute; 74 | top:0; 75 | right:0; 76 | border:0; 77 | z-index: 998; 78 | } 79 | 80 | -------------------------------------------------------------------------------- /src/custom-typings.d.ts: -------------------------------------------------------------------------------- 1 | interface Window { 2 | vkbeautify: VkBeautify; 3 | Chart: Chart; 4 | x2js: X2JS; 5 | hljs: any; 6 | moment: any; 7 | } 8 | 9 | interface Chart { 10 | Line(ctx: any, config: {data: any, options: any}): any; 11 | Bar(ctx: any, config: {data: any, options: any}): any; 12 | } 13 | 14 | interface VkBeautify { 15 | json(message: any, depth: number): any; 16 | xml(message: any): any; 17 | } 18 | 19 | interface X2JS { 20 | js2xml(data: any): string; 21 | } 22 | 23 | declare let vkbeautify: VkBeautify; 24 | declare let x2js: X2JS; 25 | declare let hljs: any; 26 | declare let Chart: Chart; 27 | declare let enableProdMode: Function; 28 | 29 | declare module 'vkbeautify' { 30 | export = vkbeautify; 31 | } 32 | 33 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Swagger 2 UI 6 | 7 | 8 | 9 | 10 | 11 | 12 | Initializing application... 13 | 14 | -------------------------------------------------------------------------------- /src/robots.txt: -------------------------------------------------------------------------------- 1 | # robotstxt.org 2 | 3 | User-agent: * 4 | Disallow: -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "removeComments": false, 9 | "noImplicitAny": true, 10 | "suppressImplicitAnyIndexErrors": true, 11 | "sourceMap": true, 12 | "sourceRoot": "src", 13 | "types": [ 14 | "jasmine", 15 | "node", 16 | "jquery", 17 | "materialize-css", 18 | "chai", 19 | "core-js", 20 | "lodash", 21 | "node", 22 | "highlight.js" 23 | ] 24 | }, 25 | "compileOnSave": false, 26 | "buildOnSave": false, 27 | "exclude": [ 28 | "node_modules", 29 | "dist" 30 | ], 31 | "awesomeTypescriptLoaderOptions": { 32 | "useWebpackText": true, 33 | "forkChecker": true 34 | } 35 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "class-name": true, 7 | "comment-format": [ 8 | true, 9 | "check-space" 10 | ], 11 | "curly": true, 12 | "eofline": true, 13 | "forin": true, 14 | "indent": [ 15 | true, 16 | "spaces" 17 | ], 18 | "label-position": true, 19 | "label-undefined": true, 20 | "max-line-length": [ 21 | true, 22 | 140 23 | ], 24 | "member-access": false, 25 | "member-ordering": [ 26 | true, 27 | "static-before-instance", 28 | "variables-before-functions" 29 | ], 30 | "no-arg": true, 31 | "no-bitwise": true, 32 | "no-console": [ 33 | true, 34 | "debug", 35 | "info", 36 | "time", 37 | "timeEnd", 38 | "trace" 39 | ], 40 | "no-construct": true, 41 | "no-debugger": true, 42 | "no-duplicate-key": true, 43 | "no-duplicate-variable": true, 44 | "no-empty": false, 45 | "no-eval": true, 46 | "no-inferrable-types": true, 47 | "no-shadowed-variable": true, 48 | "no-string-literal": false, 49 | "no-switch-case-fall-through": true, 50 | "no-trailing-whitespace": true, 51 | "no-unused-expression": true, 52 | "no-unused-variable": true, 53 | "no-unreachable": true, 54 | "no-use-before-declare": true, 55 | "no-var-keyword": true, 56 | "object-literal-sort-keys": false, 57 | "one-line": [ 58 | true, 59 | "check-open-brace", 60 | "check-catch", 61 | "check-else", 62 | "check-whitespace" 63 | ], 64 | "quotemark": [ 65 | true, 66 | "single" 67 | ], 68 | "radix": true, 69 | "semicolon": [ 70 | "always" 71 | ], 72 | "triple-equals": [ 73 | true, 74 | "allow-null-check" 75 | ], 76 | "typedef-whitespace": [ 77 | true, 78 | { 79 | "call-signature": "nospace", 80 | "index-signature": "nospace", 81 | "parameter": "nospace", 82 | "property-declaration": "nospace", 83 | "variable-declaration": "nospace" 84 | } 85 | ], 86 | "variable-name": false, 87 | "whitespace": [ 88 | true, 89 | "check-branch", 90 | "check-decl", 91 | "check-operator", 92 | "check-separator", 93 | "check-type" 94 | ], 95 | 96 | "use-input-property-decorator": true, 97 | "use-output-property-decorator": true, 98 | "use-host-property-decorator": true, 99 | "no-input-rename": true, 100 | "no-output-rename": true, 101 | "use-life-cycle-interface": true, 102 | "use-pipe-transform-interface": true, 103 | "component-class-suffix": true, 104 | "directive-class-suffix": true, 105 | "pipe-naming": [true, "camelCase", "my"] 106 | } 107 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by mdesigaud on 16/11/2016. 3 | */ 4 | 5 | module.exports = require('./config/webpack.dev.config.js'); -------------------------------------------------------------------------------- /webpack.config.old.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Webpack configuration file 3 | * Created by Michael DESIGAUD on 24/03/2016. 4 | */ 5 | 6 | //region Required tools 7 | // Webpack core 8 | var webpack = require('webpack'); 9 | // Used to format path 10 | var path = require('path'); 11 | // Autoprefixer 12 | var autoprefixer = require('autoprefixer'); 13 | // Precss 14 | var precss = require('precss'); 15 | // Used to merge configuration object (common + env) 16 | var MergePlugin = require('webpack-merge'); 17 | // Used to clean build directories 18 | var CleanWebpackPlugin = require('clean-webpack-plugin'); 19 | // Used to copy static resources 20 | var CopyWebpackPlugin = require('copy-webpack-plugin'); 21 | // Used to generate index.html file 22 | var HtmlWebpackPlugin = require('html-webpack-plugin'); 23 | // Type script transpilator 24 | var ForkCheckerPlugin = require('awesome-typescript-loader').ForkCheckerPlugin; 25 | // Used to compress resources with gzip 26 | var CompressionPlugin = require("compression-webpack-plugin"); 27 | // Deduplicate resources 28 | var DedupePlugin = require('webpack/lib/optimize/DedupePlugin'); 29 | // Uglify (minimize and aggregate) JS 30 | var UglifyJsPlugin = require('webpack/lib/optimize/UglifyJsPlugin'); 31 | // Order chunk to optimiser loading 32 | var OccurenceOrderPlugin = require('webpack/lib/optimize/OccurenceOrderPlugin'); 33 | // Group chunk in specifics files 34 | var CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin'); 35 | //endregion 36 | 37 | //region Project variables 38 | // npm command --environment=(dev|prod|test) 39 | var ENV = process.env.npm_config_environment || 'dev'; 40 | // npm environment variable 41 | var DIST = process.env.npm_config_dist_path || './dist'; 42 | // Project name, for package 43 | var NAME = process.env.npm_package_name; 44 | // Project description, for title 45 | var DESCRIPTION = process.env.npm_package_description; 46 | // Project version, for package 47 | var VERSION = process.env.npm_package_version; 48 | // ***************************************************** 49 | //endregion 50 | 51 | /** 52 | * Webpack configuration by environment 53 | */ 54 | var config = { 55 | //region Common configuration 56 | common: { 57 | metadata: { 58 | title: DESCRIPTION, 59 | gaId: "TODEFINE" 60 | }, 61 | entry: { 62 | 'vendor': path.resolve('./src/app/vendor.ts'), 63 | 'app': path.resolve('./src/app/boot.ts') 64 | }, 65 | output: { 66 | path: path.resolve(DIST), 67 | filename: '[name].bundle.js', 68 | sourceMapFilename: '[file].map', 69 | chunkFilename: '[name].[id].chunk.js' 70 | }, 71 | resolve: { 72 | modulesDirectories: [path.resolve('./node_modules')], 73 | root:path.resolve('./src'), 74 | extensions: ['', '.ts', '.js'], 75 | alias:{ 76 | "$":'jquery', 77 | "jQuery":'jquery' 78 | } 79 | }, 80 | postcss: [ 81 | precss(), 82 | autoprefixer({browsers: ['last 2 versions', 'Firefox ESR']}) 83 | ], 84 | module: { 85 | preLoaders: [ 86 | // Generate source map for debugging 87 | { 88 | test: /\.js$/, 89 | loader: 'source-map-loader', 90 | // fixme : fixed with rxjs 5 beta.3 release (@from https://github.com/AngularClass/angular2-webpack-starter) 91 | exclude: [ 92 | path.resolve('./node_modules/rxjs') 93 | ] 94 | } 95 | ], 96 | loaders: [ 97 | // Support for ts files. 98 | { 99 | test: /\.ts$/, 100 | loaders: ['awesome-typescript'], 101 | exclude: [ 102 | /\.(spec|e2e)\.ts$/ 103 | ] 104 | }, 105 | // Support for html files 106 | { 107 | test: /\.html$/, 108 | loaders: ['raw'], 109 | exclude: [ 110 | path.resolve('./src/index.html') 111 | ] 112 | }, 113 | // Extract plain-ol' vanilla CSS 114 | { 115 | test: /\.css$/, 116 | loader: "style-loader!css-loader!postcss-loader" 117 | }, 118 | //Extract fonts 119 | { test: /\.(woff|woff2)($|\?)/, loader: "url-loader?limit=10000&mimetype=application/font-woff" }, 120 | { test: /\.ttf($|\?)/, loader: "file-loader" }, 121 | { test: /\.eot($|\?)/, loader: "file-loader" }, 122 | { test: /\.svg($|\?)/, loader: "file-loader" } 123 | ] 124 | }, 125 | plugins: [ 126 | new ForkCheckerPlugin(), 127 | // Generate index.html with css and js injections 128 | new HtmlWebpackPlugin({ 129 | filename: "index.html", 130 | inject:'head', 131 | template: path.resolve("./src/index.html"), 132 | favicon: path.resolve("./src/assets/icons/favicon.ico"), 133 | title: DESCRIPTION 134 | }), 135 | // Separate js files in multi package 136 | new webpack.optimize.CommonsChunkPlugin({ 137 | name: ['app', 'vendor'], 138 | minChunks: Infinity 139 | }), 140 | new webpack.ProvidePlugin({ 141 | "$":'jquery', 142 | "jQuery":'jquery', 143 | 'window.jQuery': 'jquery', 144 | "hljs":'highlight.js/lib/highlight' 145 | }), 146 | // Copy static resources 147 | new CopyWebpackPlugin([ 148 | {from: path.resolve('./src/assets/icons')}, 149 | {from: path.resolve('./src/robots.txt')} 150 | ]) 151 | ] 152 | }, 153 | //endregion 154 | //region Development configuration 155 | dev: { 156 | debug: true, 157 | devtool: 'cheap-module-eval-source-map', 158 | plugins: [ 159 | // Clean dist directory 160 | new CleanWebpackPlugin([path.resolve(DIST + '/*')]), 161 | // Define JS global constants 162 | new webpack.DefinePlugin({ 163 | VERSION: JSON.stringify(VERSION + "-SNAPSHOT") 164 | }) 165 | ], 166 | devServer: { 167 | port: 3000, 168 | host: 'localhost', 169 | inline:false, 170 | historyApiFallback: true, 171 | watchOptions: { 172 | aggregateTimeout: 300, 173 | poll: 1000 174 | } 175 | } 176 | }, 177 | //endregion 178 | //region Test configuration 179 | test: { 180 | devtool: 'inline-source-map', 181 | resolve: { 182 | extensions: ['', '.ts', '.js'] 183 | }, 184 | module: { 185 | preLoaders: [ 186 | // Tslint loader support for *.ts files 187 | // 188 | // See: https://github.com/wbuchwalter/tslint-loader 189 | //{test: /\.ts$/, loader: 'tslint-loader', exclude: [helpers.root('node_modules')]}, 190 | 191 | // Source map loader support for *.js files 192 | // Extracts SourceMaps for source files that as added as sourceMappingURL comment. 193 | // 194 | // See: https://github.com/webpack/source-map-loader 195 | {test: /\.js$/, loader: "source-map", exclude: [path.resolve('node_modules')]} 196 | ], 197 | loaders: [ 198 | { 199 | test: /\.ts$/, 200 | loader: 'awesome-typescript', 201 | query: { 202 | "compilerOptions": { 203 | "removeComments": true 204 | 205 | } 206 | }, 207 | exclude: [/\.e2e\.ts$/] 208 | } 209 | ], 210 | postLoaders: [ 211 | 212 | // Instruments JS files with Istanbul for subsequent code coverage reporting. 213 | // Instrument only testing sources. 214 | // 215 | // See: https://github.com/deepsweet/istanbul-instrumenter-loader 216 | { 217 | test: /\.(js|ts)$/, loader: 'istanbul-instrumenter', 218 | include: path.resolve('./src'), 219 | exclude: [ 220 | /\.(e2e|spec)\.ts$/, 221 | /node_modules/ 222 | ] 223 | } 224 | 225 | ] 226 | }, 227 | plugins: [ 228 | // Clean reporting directory 229 | new CleanWebpackPlugin([path.resolve('./reports')], { verbose: false }) 230 | ], 231 | 232 | node: { 233 | global: 'window', 234 | process: false, 235 | crypto: 'empty', 236 | module: false, 237 | clearImmediate: false, 238 | setImmediate: false 239 | } 240 | }, 241 | //endregion 242 | //region Production configuration 243 | prod: { 244 | devtool: 'source-map', 245 | debug: false, 246 | output: { 247 | filename: '[name].[chunkhash].bundle.js', 248 | sourceMapFilename: '[name].[chunkhash].bundle.map', 249 | chunkFilename: '[name].[id].[chunkhash].chunk.js' 250 | }, 251 | module: { 252 | loaders: [ 253 | // Support for ts files. 254 | { 255 | test: /\.ts$/, 256 | loaders: ['awesome-typescript'], 257 | query: { 258 | 'compilerOptions': { 259 | 'removeComments': true 260 | } 261 | }, 262 | exclude: [ 263 | /\.(spec|e2e)\.ts$/ 264 | ] 265 | } 266 | ] 267 | }, 268 | plugins: [ 269 | // Clean dist directory 270 | new CleanWebpackPlugin([path.resolve(DIST + '/*')]), 271 | // De duplicate source 272 | new DedupePlugin(), 273 | // Order chunk 274 | new OccurenceOrderPlugin(true), 275 | // Define JS global constants 276 | new webpack.DefinePlugin({ 277 | VERSION: JSON.stringify(VERSION) 278 | }), 279 | // Uglify generated js 280 | new UglifyJsPlugin({ 281 | beautify: false, 282 | mangle: { 283 | screw_ie8: true, 284 | except: [] 285 | }, 286 | compress: {screw_ie8: true}, 287 | comments: false 288 | }), 289 | // Compress generated resources 290 | new CompressionPlugin({ 291 | test: /\.css$|\.html$|\.js$|\.map$/, 292 | threshold: 2048, 293 | minRatio: 0.8 294 | }) 295 | ] 296 | } 297 | //endregion 298 | }; 299 | 300 | // Load configuration for current environment 301 | if (ENV === 'test') { 302 | module.exports = config[ENV]; 303 | } else { 304 | module.exports = MergePlugin(config['common'], config[ENV]); 305 | } --------------------------------------------------------------------------------