├── docker ├── Caddyfile ├── Dockerfile ├── run.sh └── README.md ├── .gitignore ├── src ├── assets │ ├── icons │ │ ├── favicon.png │ │ ├── lenses-popup.png │ │ ├── avro.svg │ │ ├── landoop-black.svg │ │ ├── landoop-blue.svg │ │ ├── landoop-dark.svg │ │ └── landoop.svg │ └── css │ │ └── styles.css ├── factories │ ├── index.js │ ├── utils-factory.js │ ├── env-factory.js │ ├── avro4s-factory.js │ ├── toast-factory.js │ └── schema-registry-factory.js ├── schema-registry │ ├── index.js │ ├── home │ │ ├── home.html │ │ └── home.controller.js │ ├── config │ │ ├── config.html │ │ └── config.controller.js │ ├── pagination │ │ └── dirPaginationControlsTemplate.html │ ├── export │ │ ├── export.html │ │ └── export.controller.js │ ├── list │ │ ├── list.html │ │ └── list.controller.js │ ├── new │ │ ├── new.html │ │ └── new.controller.js │ └── view │ │ ├── view.controller.js │ │ └── view.html ├── app.js └── index.html ├── env.js ├── package.json ├── webpack.config.js └── README.md /docker/Caddyfile: -------------------------------------------------------------------------------- 1 | 0.0.0.0:8000 2 | tls off 3 | 4 | root /schema-registry-ui 5 | log stdout 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bower_components/ 2 | node_modules/ 3 | dist/ 4 | .idea/ 5 | .DS_Store 6 | TODO 7 | npm-debug.log 8 | *.iml -------------------------------------------------------------------------------- /src/assets/icons/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lensesio/schema-registry-ui/HEAD/src/assets/icons/favicon.png -------------------------------------------------------------------------------- /src/assets/icons/lenses-popup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lensesio/schema-registry-ui/HEAD/src/assets/icons/lenses-popup.png -------------------------------------------------------------------------------- /src/factories/index.js: -------------------------------------------------------------------------------- 1 | require("./utils-factory"); 2 | require("./schema-registry-factory"); 3 | require("./avro4s-factory"); 4 | require("./env-factory"); 5 | require("./toast-factory"); -------------------------------------------------------------------------------- /src/schema-registry/index.js: -------------------------------------------------------------------------------- 1 | require("./home/home.controller"); 2 | require("./config/config.controller"); 3 | require("./view/view.controller"); 4 | require("./export/export.controller"); 5 | require("./list/list.controller"); 6 | require("./new/new.controller"); -------------------------------------------------------------------------------- /src/schema-registry/home/home.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

Welcome to the schema registry ui


5 |

select a schema or create a new one


6 |
7 |
8 |
-------------------------------------------------------------------------------- /env.js: -------------------------------------------------------------------------------- 1 | var clusters = [ 2 | { 3 | NAME: "prod", 4 | // Schema Registry service URL (i.e. http://localhost:8081) 5 | SCHEMA_REGISTRY: "http://localhost:8081", // https://schema-registry.demo.landoop.com 6 | COLOR: "#141414", // optional 7 | readonlyMode: true // optional 8 | }, 9 | { 10 | NAME: "dev", 11 | SCHEMA_REGISTRY: "http://localhost:8383", 12 | COLOR: "red", // optional 13 | allowGlobalConfigChanges: true, // optional 14 | allowSchemaDeletion: true // Supported for Schema Registry version >= 3.3.0 15 | //allowTransitiveCompatibilities: true // if using a Schema Registry release >= 3.1.1 uncomment this line 16 | } 17 | ]; 18 | -------------------------------------------------------------------------------- /src/schema-registry/home/home.controller.js: -------------------------------------------------------------------------------- 1 | var angular = require('angular'); 2 | var angularAPP = angular.module('angularAPP'); 3 | 4 | var HomeCtrl = function ($log, SchemaRegistryFactory, toastFactory, $scope, env) { 5 | $log.info("Starting schema-registry controller - home"); 6 | $scope.readonlyMode = env.readonlyMode(); 7 | toastFactory.hideToast(); 8 | 9 | $scope.$watch(function () { 10 | return env.getSelectedCluster().NAME; 11 | }, function () { 12 | $scope.cluster = env.getSelectedCluster().NAME; 13 | }, true); 14 | }; 15 | 16 | HomeCtrl.$inject = ['$log', 'SchemaRegistryFactory', 'toastFactory', '$scope', 'env']; 17 | 18 | angularAPP.controller('HomeCtrl', HomeCtrl); -------------------------------------------------------------------------------- /src/factories/utils-factory.js: -------------------------------------------------------------------------------- 1 | var angular = require('angular'); 2 | var angularAPP = angular.module('angularAPP'); 3 | 4 | /** 5 | * Utils angularJS Factory 6 | */ 7 | 8 | var UtilsFactory = function ($log) { 9 | 10 | // Sort arrays by key 11 | function sortByKey(array, key, reverse) { 12 | return array.sort(function (a, b) { 13 | var x = a[key]; 14 | var y = b[key]; 15 | return ((x < y) ? -1 * reverse : ((x > y) ? 1 * reverse : 0)); 16 | }); 17 | } 18 | 19 | /* Public API */ 20 | return { 21 | 22 | sortByKey: function (array, key, reverse) { 23 | return sortByKey(array, key, reverse); 24 | }, 25 | sortByVersion: function (array) { 26 | var sorted = array.sort(function (a, b) { 27 | return a.version - b.version; 28 | }); 29 | return sorted; 30 | }, 31 | IsJsonString: function (str) { 32 | try { 33 | JSON.parse(str); 34 | } catch (e) { 35 | return false; 36 | } 37 | return true; 38 | } 39 | 40 | } 41 | 42 | }; 43 | 44 | UtilsFactory.$inject = ['$log']; 45 | 46 | angularAPP.factory('UtilsFactory', UtilsFactory); -------------------------------------------------------------------------------- /src/schema-registry/config/config.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | Url : {{schemaRegistryURL}} 5 | CONNECTIVITY ERROR
6 | Global Compatibility level : 7 | 8 | {{config.compatibilityLevel}} 9 | change 10 |
11 | schema-registry-ui: 0.9.5 12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |

Powered by Landoop

20 |
21 |
22 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine 2 | MAINTAINER Marios Andreopoulos 3 | 4 | WORKDIR / 5 | # Add needed tools 6 | RUN apk add --no-cache ca-certificates wget \ 7 | && echo "progress = dot:giga" | tee /etc/wgetrc 8 | 9 | # Add and Setup Caddy webserver 10 | RUN wget "https://github.com/mholt/caddy/releases/download/v0.10.11/caddy_v0.10.11_linux_amd64.tar.gz" -O /caddy.tgz \ 11 | && mkdir caddy \ 12 | && tar xzf caddy.tgz -C /caddy --no-same-owner \ 13 | && rm -f /caddy.tgz 14 | 15 | # Add and Setup Schema-Registry-Ui 16 | ENV SCHEMA_REGISTRY_UI_VERSION="0.9.5" 17 | RUN wget "https://github.com/Landoop/schema-registry-ui/releases/download/v.${SCHEMA_REGISTRY_UI_VERSION}/schema-registry-ui-${SCHEMA_REGISTRY_UI_VERSION}.tar.gz" \ 18 | -O /schema-registry-ui.tar.gz \ 19 | && mkdir /schema-registry-ui \ 20 | && tar xzf /schema-registry-ui.tar.gz -C /schema-registry-ui --no-same-owner \ 21 | && rm -f /schema-registry-ui.tar.gz \ 22 | && rm -f /schema-registry-ui/env.js \ 23 | && ln -s /tmp/env.js /schema-registry-ui/env.js 24 | 25 | # Add configuration and runtime files 26 | ADD Caddyfile /caddy/Caddyfile.template 27 | ADD run.sh / 28 | RUN chmod +x /run.sh 29 | 30 | EXPOSE 8000 31 | 32 | 33 | # USER nobody:nogroup 34 | ENTRYPOINT ["/run.sh"] 35 | -------------------------------------------------------------------------------- /src/schema-registry/pagination/dirPaginationControlsTemplate.html: -------------------------------------------------------------------------------- 1 |
3 | 5 | 6 | 7 | 8 | 10 | 11 | 12 | 13 | {{pageNumber}} 15 | 16 | 18 | 19 | 20 | 21 | 23 | 24 | 25 | 26 |
-------------------------------------------------------------------------------- /src/schema-registry/export/export.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 |
8 |

Export Schemas 9 |

10 |
11 |
12 |
13 | 14 |
15 |
Number of schemas: {{Cache.length}}
16 |
Total number of schemas (including all versions): {{allSchemasCache.length}}
17 | 18 | 19 | Export latest only version of each schema 20 |
21 | 22 | download latest 23 | download all 24 | 25 |
26 | 27 | 28 | 29 | 30 |
31 |
-------------------------------------------------------------------------------- /src/assets/icons/avro.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /src/assets/icons/landoop-black.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | background 4 | 5 | 6 | 7 | Layer 1 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/assets/icons/landoop-blue.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | background 4 | 5 | 6 | 7 | Layer 1 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/assets/icons/landoop-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | background 4 | 5 | 6 | 7 | Layer 1 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/factories/env-factory.js: -------------------------------------------------------------------------------- 1 | var angular = require('angular'); 2 | var angularAPP = angular.module('angularAPP'); 3 | 4 | var envFactory = function ($rootScope) { 5 | 6 | var clusterArray = (typeof clusters !== "undefined") ? angular.copy(clusters) : []; 7 | var selectedCluster = null; 8 | setCluster(); 9 | 10 | return { 11 | setSelectedCluster : function(clusterName) { setCluster(clusterName)}, 12 | getSelectedCluster : function() { return selectedCluster; }, 13 | getClusters : function() { return clusters} , 14 | 15 | SCHEMA_REGISTRY : function () { return selectedCluster.SCHEMA_REGISTRY; }, 16 | AVRO4S : 'https://platform.landoop.com/avro4s/avro4s', // Not currently used, will be used for converting Avro -> Scala Case classes 17 | COLOR : function () { return selectedCluster.COLOR; }, 18 | allowGlobalConfigChanges : function () { return selectedCluster.allowGlobalConfigChanges; }, 19 | allowTransitiveCompatibilities: function () { return selectedCluster.allowTransitiveCompatibilities; }, 20 | allowSchemaDeletion: function () { return selectedCluster.allowSchemaDeletion; }, 21 | readonlyMode: function() { return selectedCluster.readonlyMode; } 22 | }; 23 | 24 | function setCluster(clusterName) { 25 | if(clusterArray.length === 0) { 26 | $rootScope.missingEnvJS = true; 27 | console.log("NOT EXISTS env.js") 28 | } 29 | if(angular.isUndefined(clusterName)) { 30 | selectedCluster = clusterArray[0]; 31 | } else { 32 | var filteredArray = clusterArray.filter(function(el) {return el.NAME === clusterName}); 33 | selectedCluster = filteredArray.length === 1 ? filteredArray[0] : clusterArray[0] 34 | } 35 | } 36 | }; 37 | 38 | envFactory.$inject = ['$rootScope']; 39 | 40 | angularAPP.factory('env', envFactory); 41 | -------------------------------------------------------------------------------- /src/assets/icons/landoop.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | background 8 | 9 | 10 | 11 | 12 | 13 | 16 | 17 | 18 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/schema-registry/list/list.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |

5 | {{allSchemas.length}} Schemas 6 |

7 | 8 | New 12 |
13 |
14 | 15 | 16 | 17 | 18 | 24 | 25 | 26 |
27 |

{{schema.subjectName}}

28 |

29 | Compatibility level is set to {{schema.compatibilityLevel}} 30 |

31 |
32 |
35 | v.{{schema.version}} 36 |
37 |
38 |
39 | 40 |
41 |
42 | -------------------------------------------------------------------------------- /src/factories/avro4s-factory.js: -------------------------------------------------------------------------------- 1 | var angular = require('angular'); 2 | var angularAPP = angular.module('angularAPP'); 3 | 4 | var Avro4ScalaFactory = function ($rootScope, $http, $location, $q, $log) { 5 | 6 | /* Public API */ 7 | return { 8 | getScalaFiles: function (apiData) { 9 | $log.warn(apiData); 10 | $http.defaults.useXDomain = true; 11 | 12 | var singleLineApiData = apiData.split("\n").join(" "); 13 | 14 | var req = { 15 | method: 'POST', 16 | data: singleLineApiData, 17 | crossDomain: true, 18 | url: AVRO4S, 19 | headers: {'Content-Type': 'application/json', 'Accept': 'application/json'} 20 | }; 21 | 22 | $http(req) 23 | .success(function (data) { 24 | $log.info("Received a response with: " + data); 25 | var results = data.split("###"); 26 | $log.info(results); 27 | if (results[0] === "scala") { 28 | $log.info("It's Scala !! "); 29 | $log.info("It's Scala :" + results[1]); 30 | //alg0 31 | return results[1]; 32 | } 33 | }) 34 | .error(function (data, status) { 35 | $log.error("Bad data [" + data + "] status [" + status + "]"); 36 | }); 37 | } 38 | } 39 | }; 40 | 41 | Avro4ScalaFactory.$inject = ['$rootScope', '$http', '$location', '$q', '$log']; 42 | 43 | angularAPP.factory('Avro4ScalaFactory', Avro4ScalaFactory); 44 | 45 | // curl 'https://platform.landoop.com/avro4s/avro4s' -H 'Pragma: no-cache' -H 'Origin: https://avro4s-ui.landoop.com' -H 'Accept-Encoding: gzip, deflate, br' -H 'Accept-Language: en-US,en;q=0.8,el;q=0.6' -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36' -H 'Content-Type: application/json' -H 'Accept: application/json' -H 'Cache-Control: no-cache' -H 'Referer: https://avro4s-ui.landoop.com/' -H 'Connection: keep-alive' --data-binary '{ "type": "record", "name": "Evolution", "namespace": "com.landoop", "fields": [ { "name": "name", "type": "string" }, { "name": "number1", "type": "int" }, { "name": "number2", "type": "float" }, { "name": "text", "type": [ "string", "null" ], "default": "" } ] }' --compressed -------------------------------------------------------------------------------- /src/factories/toast-factory.js: -------------------------------------------------------------------------------- 1 | var angular = require('angular'); 2 | var angularAPP = angular.module('angularAPP'); 3 | 4 | var toastFactory = function ($rootScope, $mdToast, $window) { 5 | 6 | var last = { 7 | bottom: false, 8 | top: true, 9 | left: false, 10 | right: true 11 | }; 12 | 13 | var toastPosition = angular.extend({}, last); 14 | 15 | /* Public API of this factory*/ 16 | this.getToastPosition = function () { 17 | this.sanitizePosition(); 18 | 19 | return Object.keys(toastPosition) 20 | .filter(function (pos) { 21 | return toastPosition[pos]; 22 | }) 23 | .join(' '); 24 | }; 25 | 26 | this.sanitizePosition = function () { 27 | var current = toastPosition; 28 | if (current.bottom && last.top) current.top = false; 29 | if (current.top && last.bottom) current.bottom = false; 30 | if (current.right && last.left) current.left = false; 31 | if (current.left && last.right) current.right = false; 32 | last = angular.extend({}, current); 33 | }; 34 | 35 | this.showSimpleToast = function (message) { 36 | $mdToast.show( 37 | $mdToast.simple() 38 | .textContent(message) 39 | .position(this.getToastPosition()) 40 | .hideDelay(2000) 41 | ); 42 | }; 43 | 44 | this.showSimpleToastToTop = function (message) { 45 | this.showSimpleToast(message); 46 | $window.scrollTo(0, 0); 47 | }; 48 | 49 | this.showLongToast = function (message) { 50 | var last = this.getToastPosition(); 51 | 52 | $mdToast.show( 53 | $mdToast.simple() 54 | .textContent(message) 55 | .position(last) 56 | .hideDelay(5000) 57 | ); 58 | $window.scrollTo(0, 0); 59 | }; 60 | 61 | this.showActionToast = function (message) { 62 | var toast = $mdToast.simple() 63 | .textContent(message) 64 | .action('DELETE') 65 | .highlightAction(true) 66 | //.highlightClass('md-accent')// Accent is used by default, this just demonstrates the usage. 67 | .position(this.getToastPosition()) 68 | .hideDelay(2000); 69 | 70 | $mdToast.show(toast).then(function (response) { 71 | if (response === 'ok') { 72 | //alert('You clicked the \'UNDO\' action.'); 73 | } 74 | }); 75 | }; 76 | 77 | this.hideToast = function () { 78 | $mdToast.hide(); 79 | }; 80 | 81 | }; 82 | 83 | toastFactory.$inject = ['$rootScope', '$mdToast', '$window']; 84 | 85 | angularAPP.service('toastFactory', toastFactory); -------------------------------------------------------------------------------- /src/schema-registry/list/list.controller.js: -------------------------------------------------------------------------------- 1 | var angular = require('angular'); 2 | var angularAPP = angular.module('angularAPP'); 3 | 4 | var SubjectListCtrl = function ($scope, $rootScope, $log, $mdMedia, SchemaRegistryFactory, env) { 5 | 6 | $log.info("Starting schema-registry controller : list ( initializing subject cache )"); 7 | $scope.readonlyMode = env.readonlyMode(); 8 | 9 | function addCompatibilityValue() { 10 | angular.forEach($rootScope.allSchemas, function (schema) { 11 | SchemaRegistryFactory.getSubjectConfig(schema.subjectName).then( 12 | function success(config) { 13 | schema.compatibilityLevel = config.compatibilityLevel; 14 | }, 15 | function errorCallback(response) { 16 | $log.error(response); 17 | }); 18 | }) 19 | } 20 | 21 | /* 22 | * Watch the 'newCreated' and update the subject-cache accordingly 23 | */ 24 | 25 | $scope.$watch(function () { 26 | return $rootScope.listChanges; 27 | }, function (a) { 28 | if (a !== undefined && a === true) { 29 | loadCache(); //When new is created refresh the list 30 | $rootScope.listChanges = false; 31 | } 32 | }, true); 33 | // listen for the event in the relevant $scope 34 | $scope.$on('newEvolve', function (event, args) { 35 | loadCache(); 36 | }); 37 | 38 | $scope.$watch(function () { 39 | return env.getSelectedCluster().NAME; 40 | }, function (a) { 41 | $scope.cluster = env.getSelectedCluster().NAME; 42 | $scope.readonlyMode = env.readonlyMode(); 43 | loadCache(); //When cluster change, reload the list 44 | }, true); 45 | /** 46 | * Load cache by fetching all latest subjects 47 | */ 48 | function loadCache() { 49 | $rootScope.allSchemas = []; 50 | var promise = SchemaRegistryFactory.refreshLatestSubjectsCACHE(); 51 | promise.then(function (cachedData) { 52 | $rootScope.allSchemas = cachedData; 53 | addCompatibilityValue(); 54 | }, function (reason) { 55 | $log.error('Failed at loadCache : ' + reason); 56 | }, function (update) { 57 | $log.debug('Got notification: ' + update); 58 | }); 59 | } 60 | 61 | var itemsPerPage = (window.innerHeight - 355) / 48; 62 | Math.floor(itemsPerPage) < 3 ? $scope.itemsPerPage = 3 : $scope.itemsPerPage = Math.floor(itemsPerPage); 63 | }; 64 | 65 | SubjectListCtrl.$inject = ['$scope', '$rootScope', '$log', '$mdMedia', 'SchemaRegistryFactory', 'env']; 66 | 67 | angularAPP.controller('SubjectListCtrl', SubjectListCtrl); 68 | 69 | //In small devices the list is hidden 70 | // $scope.$mdMedia = $mdMedia; 71 | // $scope.$watch(function () { 72 | // return $mdMedia('gt-sm'); 73 | // }, function (display) { 74 | // $rootScope.showList = display; 75 | // }); 76 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "schema-registry-ui", 3 | "version": "0.9.5", 4 | "description": "A user interface for Confluent's Schema Registry", 5 | "readme": "README.md", 6 | "dependencies": { 7 | "angular": "1.5.9", 8 | "angular-animate": "1.5.9", 9 | "angular-aria": "1.5.9", 10 | "angular-diff-match-patch": "^0.1.14", 11 | "angular-json-tree": "^1.0.1", 12 | "angular-material": "^1.0.9", 13 | "angular-material-data-table": "^0.10.9", 14 | "angular-route": "1.5.9", 15 | "angular-sanitize": "1.5.9", 16 | "angular-ui-ace": "0.2.3", 17 | "angular-utils-pagination": "^0.11.1", 18 | "brace": "^0.10.0", 19 | "diff-match-patch": "^1.0.0", 20 | "file-saver": "^1.3.3", 21 | "font-awesome": "^4.6.3", 22 | "jszip": "3.1.3", 23 | "jszip-utils": "0.0.2" 24 | }, 25 | "devDependencies": { 26 | "babel-core": "^6.24.1", 27 | "babel-loader": "^7.0.0", 28 | "babel-plugin-transform-class-properties": "^6.24.1", 29 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 30 | "babel-plugin-transform-object-rest-spread": "^6.23.0", 31 | "babel-plugin-transform-runtime": "^6.23.0", 32 | "babel-preset-es2015": "^6.24.1", 33 | "babel-preset-stage-1": "^6.24.1", 34 | "clean-webpack-plugin": "^0.1.16", 35 | "copy-webpack-plugin": "^4.0.1", 36 | "cross-env": "^5.0.1", 37 | "css-loader": "^0.28.1", 38 | "exports-loader": "^0.6.4", 39 | "expose-loader": "^0.7.3", 40 | "extract-text-webpack-plugin": "^2.1.0", 41 | "file-loader": "^0.11.1", 42 | "html-loader": "^0.4.5", 43 | "html-webpack-plugin": "^2.28.0", 44 | "imports-loader": "^0.7.1", 45 | "style-loader": "^0.17.0", 46 | "url-loader": "^0.5.8", 47 | "webpack": "^2.5.1", 48 | "webpack-dev-server": "^2.4.5" 49 | }, 50 | "scripts": { 51 | "start": "node ./node_modules/cross-env/dist/bin/cross-env.js NODE_ENV=development node ./node_modules/webpack-dev-server/bin/webpack-dev-server --config webpack.config.js --progress", 52 | "start-prod": "node ./node_modules/cross-env/dist/bin/cross-env.js NODE_ENV=production node ./node_modules/webpack-dev-server/bin/webpack-dev-server --config webpack.config.js --progress", 53 | "build-dev": "node ./node_modules/cross-env/dist/bin/cross-env.js NODE_ENV=development node ./node_modules/webpack/bin/webpack --config webpack.config.js", 54 | "build-prod": "node ./node_modules/cross-env/dist/bin/cross-env.js NODE_ENV=production node ./node_modules/webpack/bin/webpack --config webpack.config.js", 55 | "test": "echo \"Error: no test specified\" && exit 1", 56 | "postinstall": "" 57 | }, 58 | "repository": { 59 | "type": "git", 60 | "url": "git+https://github.com/Landoop/schema-registry-ui.git" 61 | }, 62 | "keywords": [ 63 | "schema", 64 | "registry", 65 | "kafka", 66 | "avro" 67 | ], 68 | "author": "Landoop team", 69 | "contributors": [ 70 | { 71 | "name": "Christina Daskalaki", 72 | "email": "christina@landoop.com" 73 | }, 74 | { 75 | "name": "Antonios Chalkiopoulos", 76 | "email": "antonios@landoop.com" 77 | }, 78 | { 79 | "name": "Marios Andreopoulos", 80 | "email": "marios@landoop.com" 81 | }, 82 | { 83 | "name": "John Glampedakis", 84 | "email": "marios@landoop.com" 85 | } 86 | ], 87 | "license": "BSL", 88 | "bugs": { 89 | "url": "https://github.com/Landoop/schema-registry-ui/issues" 90 | }, 91 | "homepage": "https://github.com/Landoop/schema-registry-ui#readme" 92 | } 93 | -------------------------------------------------------------------------------- /src/schema-registry/export/export.controller.js: -------------------------------------------------------------------------------- 1 | var angular = require('angular'); 2 | var angularAPP = angular.module('angularAPP'); 3 | var JSZip = require('jszip'); 4 | var FileSaver = require('file-saver'); 5 | 6 | var ExportSchemasCtrl = function ($rootScope, $scope, env, SchemaRegistryFactory) { 7 | 8 | $scope.$on('$routeChangeSuccess', function () { 9 | $scope.cluster = env.getSelectedCluster().NAME;//$routeParams.cluster; 10 | }); 11 | 12 | var d = new Date(); 13 | $scope.date = '-' + d.getDate() + '' + (d.getMonth() + 1) + '' + d.getFullYear() + '' + d.getHours() + '' + d.getMinutes(); 14 | var script = '\n\n # To restore the schema - edit & run the following \n # cat "$schema" | sed -e \'s/"/\\"/g\' -e \'s/\\n//g\' -e \'1s/^/{ "schema": "/\' -e \'$s/$/"}/\' | curl -XPOST -i -H "Content-Type: application/vnd.schemaregistry.v1+json" --data @- "SCHEMA_REGISTRY_URL/subjects/$SUBJECT/versions" \n # done'; 15 | 16 | var latestZip = new JSZip(); 17 | var allZip = new JSZip(); 18 | 19 | 20 | if ($rootScope.Cache && $rootScope.Cache.length > 0) { 21 | angular.forEach($scope.allSchemas, function (schema, key) { 22 | latestZip.file(schema.subjectName + '.' + schema.version + '.json', schema.schema); 23 | }) 24 | } else { 25 | SchemaRegistryFactory.refreshLatestSubjectsCACHE().then(function (latestSchemas) { 26 | angular.forEach(latestSchemas, function (schema) { 27 | latestZip.file(schema.subjectName + '.' + schema.version + '.json', schema.schema); 28 | }) 29 | }) 30 | } 31 | 32 | 33 | $scope.$watch(function () { 34 | return $rootScope.showSpinner; 35 | }, function () { 36 | $scope.allSchemas = SchemaRegistryFactory.getAllSchemas($rootScope.Cache) 37 | }, true); 38 | 39 | $scope.$watch(function () { 40 | return $rootScope.allSchemasCache; 41 | }, function () { 42 | angular.forEach($rootScope.allSchemasCache, function (schema) { 43 | allZip.file(schema.subject + '.' + schema.version + '.json', schema.schema); 44 | }) 45 | }, true); 46 | 47 | function bindEvent(el, eventName, eventHandler) { 48 | if (el.addEventListener) { 49 | // standard way 50 | el.addEventListener(eventName, eventHandler, false); 51 | } else if (el.attachEvent) { 52 | // old IE 53 | el.attachEvent('on' + eventName, eventHandler); 54 | } 55 | } 56 | 57 | function downloadLatestSchemasWithBlob() { 58 | latestZip.generateAsync({type: "blob"}).then(function (blob) { 59 | FileSaver.saveAs(blob, "latestSchemas" + $scope.date + ".zip"); 60 | }, function (err) { 61 | latestLink.innerHTML += " " + err; 62 | }); 63 | return false; 64 | } 65 | 66 | var latestLink = document.getElementById('latestSchemas'); 67 | if (JSZip.support.blob) { 68 | bindEvent(latestLink, 'click', downloadLatestSchemasWithBlob); 69 | } else { 70 | latestLink.innerHTML += " (not supported on this browser)"; 71 | } 72 | 73 | function downloadAllSchemasWithBlob() { 74 | allZip.generateAsync({type: "blob"}).then(function (blob) { 75 | FileSaver.saveAs(blob, "allSchemas" + $scope.date + ".zip"); 76 | }, function (err) { 77 | allLink.innerHTML += " " + err; 78 | }); 79 | return false; 80 | } 81 | 82 | var allLink = document.getElementById('allSchemas'); 83 | if (JSZip.support.blob) { 84 | bindEvent(allLink, 'click', downloadAllSchemasWithBlob); 85 | } else { 86 | allLink.innerHTML += " (not supported on this browser)"; 87 | } 88 | 89 | }; 90 | 91 | ExportSchemasCtrl.$inject = ['$rootScope', '$scope', 'env', 'SchemaRegistryFactory', '$location']; 92 | 93 | angularAPP.controller('ExportSchemasCtrl', ExportSchemasCtrl); 94 | 95 | -------------------------------------------------------------------------------- /docker/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | PROXY_SKIP_VERIFY="${PROXY_SKIP_VERIFY:-false}" 4 | INSECURE_PROXY="" 5 | ALLOW_GLOBAL="${ALLOW_GLOBAL:-false}" 6 | ALLOW_TRANSITIVE="${ALLOW_TRANSITIVE:-false}" 7 | ALLOW_DELETION="${ALLOW_DELETION:-false}" 8 | READONLY_MODE="${READONLY_MODE:-false}" 9 | CADDY_OPTIONS="${CADDY_OPTIONS:-}" 10 | RELATIVE_PROXY_URL="${RELATIVE_PROXY_URL:-false}" 11 | PORT="${PORT:-8000}" 12 | 13 | { 14 | echo "Landoop Schema Registry UI ${SCHEMA_REGISTRY_UI_VERSION}" 15 | echo "Visit " 16 | echo "to find more about how you can configure this container." 17 | echo 18 | 19 | if echo "$PROXY_SKIP_VERIFY" | egrep -sq "true|TRUE|y|Y|yes|YES|1"; then 20 | INSECURE_PROXY=insecure_skip_verify 21 | echo "Unsecure: won't verify proxy certicate chain." 22 | fi 23 | 24 | # fix for certain installations 25 | cat /caddy/Caddyfile.template \ 26 | | sed -e "s/8000/$PORT/" > /tmp/Caddyfile 27 | 28 | if echo $PROXY | egrep -sq "true|TRUE|y|Y|yes|YES|1" \ 29 | && [[ ! -z "$SCHEMAREGISTRY_URL" ]]; then 30 | echo "Enabling proxy." 31 | cat <>/tmp/Caddyfile 32 | proxy /api/schema-registry $SCHEMAREGISTRY_URL { 33 | without /api/schema-registry 34 | $INSECURE_PROXY 35 | } 36 | EOF 37 | if echo "$RELATIVE_PROXY_URL" | egrep -sq "true|TRUE|y|Y|yes|YES|1"; then 38 | SCHEMAREGISTRY_URL=api/schema-registry 39 | else 40 | SCHEMAREGISTRY_URL=/api/schema-registry 41 | fi 42 | fi 43 | 44 | if echo "$ALLOW_TRANSITIVE" | egrep -sq "true|TRUE|y|Y|yes|YES|1"; then 45 | TRANSITIVE_SETTING=",allowTransitiveCompatibilities: true" 46 | echo "Enabling transitive compatibility modes support." 47 | fi 48 | 49 | if echo "$ALLOW_GLOBAL" | egrep -sq "true|TRUE|y|Y|yes|YES|1"; then 50 | GLOBAL_SETTING=",allowGlobalConfigChanges: true" 51 | echo "Enabling global compatibility level change support." 52 | fi 53 | 54 | if echo "$ALLOW_DELETION" | egrep -sq "true|TRUE|y|Y|yes|YES|1"; then 55 | DELETION_SETTING=",allowSchemaDeletion: true" 56 | echo "Enabling schema deletion support." 57 | fi 58 | 59 | if echo "$READONLY_MODE" | egrep -sq "true|TRUE|y|Y|yes|YES|1"; then 60 | READONLY_SETTING=",readonlyMode: true" 61 | echo "Enabling readonly mode." 62 | fi 63 | 64 | if [[ -z "$SCHEMAREGISTRY_URL" ]]; then 65 | echo "Schema Registry URL was not set via SCHEMAREGISTRY_URL environment variable." 66 | else 67 | echo "Setting Schema Registry URL to $SCHEMAREGISTRY_URL." 68 | cat </tmp/env.js 69 | var clusters = [ 70 | { 71 | NAME: "default", 72 | SCHEMA_REGISTRY: "$SCHEMAREGISTRY_URL" 73 | $GLOBAL_SETTING 74 | $TRANSITIVE_SETTING 75 | $DELETION_SETTING 76 | $READONLY_SETTING 77 | } 78 | ] 79 | EOF 80 | fi 81 | 82 | if [[ -n "${CADDY_OPTIONS}" ]]; then 83 | echo "Applying custom options to Caddyfile" 84 | cat <>/tmp/Caddyfile 85 | $CADDY_OPTIONS 86 | EOF 87 | fi 88 | 89 | # Here we emulate the output by Caddy. Why? Because we can't 90 | # redirect caddy to stderr as the logging would also get redirected. 91 | cat <&2 101 | 102 | exec /caddy/caddy -conf /tmp/Caddyfile -quiet 103 | -------------------------------------------------------------------------------- /src/schema-registry/new/new.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 |
7 |

8 | New Subject 9 |

10 |
11 |
12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | {{item.display}} 20 | 21 |
22 |
This field is required
23 |
24 | 25 |
26 |
27 |
28 | 29 |
30 |
31 | Schema: 32 | CURL command: 33 |
34 |
35 | schema / curl 36 | schema / curl 37 |
38 |
39 | 40 | 41 |
57 |
58 | 59 |
71 |
72 |
73 | 74 | 75 | 76 | ** This is a sample schema. Please edit! ** 77 | 78 | 86 | VALIDATE 87 | 88 | 96 | 97 | {{createOrEvolve}} 98 |
99 |
100 | 101 | 102 |
103 | 104 |
-------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const webpack = require("webpack"); 3 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 4 | const ExtractTextPlugin = require("extract-text-webpack-plugin"); 5 | const CopyWebpackPlugin = require("copy-webpack-plugin"); 6 | const CleanWebpackPlugin = require('clean-webpack-plugin'); 7 | 8 | 9 | const ENV = process.env.NODE_ENV || 'development'; 10 | const isProd = ENV === 'production'; 11 | 12 | console.log('Building for ' + ENV); 13 | 14 | const config = { 15 | watch: !isProd, 16 | devtool: isProd ? "cheap-source-map" : "cheap-module-source-map", 17 | entry: { 18 | app: "./src/app.js" 19 | }, 20 | output: { 21 | filename: "js/[name].[hash].bundle.js", 22 | path: path.resolve(__dirname, "dist"), 23 | publicPath: "" 24 | }, 25 | module: { 26 | rules: [ 27 | { 28 | test: /\.(js|jsx)$/, 29 | loader: "babel-loader", 30 | exclude: /node_modules/, 31 | query: { 32 | plugins: ['transform-decorators-legacy', 33 | 'transform-runtime', 34 | 'transform-object-rest-spread', 35 | 'transform-class-properties'], 36 | presets: [['es2015', { modules: false }], 'stage-1'] 37 | } 38 | }, 39 | { 40 | test: /\.css$/, 41 | use: ExtractTextPlugin.extract({ 42 | fallback: "style-loader", 43 | use: "css-loader" 44 | }) 45 | }, 46 | 47 | { 48 | test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/, 49 | loader: "url-loader?limit=10000&mimetype=application/font-woff&outputPath=&publicPath=../../" 50 | }, 51 | { 52 | test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/, 53 | loader: "file-loader" 54 | }, 55 | { 56 | test: /\.html$/, 57 | loader: "html-loader" 58 | } 59 | ] 60 | }, 61 | plugins: [ 62 | new ExtractTextPlugin({ 63 | filename: "assets/css/[name].[contenthash].css" 64 | }), 65 | new webpack.optimize.CommonsChunkPlugin({ 66 | name: "vendor", 67 | minChunks: function (module) { 68 | return module.context && module.context.indexOf("node_modules") !== -1; 69 | } 70 | }), 71 | new webpack.optimize.CommonsChunkPlugin({ 72 | name: "manifest" 73 | }), 74 | new HtmlWebpackPlugin({ template: "./src/index.html" }), 75 | new CopyWebpackPlugin([{ 76 | from: __dirname + '/src/assets', 77 | to: path.resolve(__dirname, "dist/src/assets") 78 | }]), 79 | new CopyWebpackPlugin([{ 80 | from: __dirname + '/env.js', 81 | to: path.resolve(__dirname, "dist/") 82 | }]), 83 | new webpack.HotModuleReplacementPlugin() 84 | ], 85 | resolve: { 86 | alias: { 87 | } 88 | }, 89 | devServer: { 90 | host: "localhost", 91 | port: "8080", 92 | contentBase: path.resolve(__dirname, "dist"), 93 | //compress: true, 94 | historyApiFallback: true, 95 | hot: true, 96 | inline: true, 97 | https: false, 98 | noInfo: true 99 | } 100 | }; 101 | 102 | if (isProd) { 103 | config.plugins.push( 104 | new CleanWebpackPlugin(['dist']), 105 | new webpack.optimize.UglifyJsPlugin({ 106 | compress: { 107 | warnings: false, 108 | screw_ie8: true, 109 | conditionals: true, 110 | unused: true, 111 | comparisons: true, 112 | sequences: true, 113 | dead_code: true, 114 | evaluate: true, 115 | join_vars: true, 116 | if_return: true 117 | }, 118 | output: { 119 | comments: false 120 | } 121 | })) 122 | } 123 | 124 | module.exports = config; -------------------------------------------------------------------------------- /src/schema-registry/config/config.controller.js: -------------------------------------------------------------------------------- 1 | var angular = require('angular'); 2 | var angularAPP = angular.module('angularAPP'); 3 | 4 | var SchemaRegistryConfigCtrl = function ($scope, $http, $log, $mdDialog, SchemaRegistryFactory, env) { 5 | 6 | $log.info("Starting schema-registry controller"); 7 | $scope.config = {}; 8 | $scope.connectionFailure = false; 9 | $scope.showButton = false; 10 | //Get the top level config 11 | $scope.$watch(function () { 12 | return env.getSelectedCluster().NAME; 13 | }, function () { 14 | $scope.schemaRegistryURL = env.SCHEMA_REGISTRY(); 15 | 16 | $scope.globalConfigOpts = ["NONE", "FULL", "FORWARD", "BACKWARD"]; 17 | 18 | if (env.allowTransitiveCompatibilities()) { 19 | $scope.globalConfigOpts.push("FULL_TRANSITIVE", "FORWARD_TRANSITIVE", "BACKWARD_TRANSITIVE"); 20 | } 21 | 22 | SchemaRegistryFactory.getGlobalConfig().then( 23 | function success(config) { 24 | $scope.allowChanges = !env.readonlyMode() && env.allowGlobalConfigChanges(); 25 | $scope.config = config; 26 | $scope.connectionFailure = false; 27 | $scope.form = $scope.config.compatibilityLevel; 28 | }, 29 | function failure(response) { 30 | $log.error("Failure with : " + JSON.stringify(response)); 31 | $scope.connectionFailure = true; 32 | }); 33 | }, true); 34 | 35 | $scope.updateGlobalConfig = function (config, event) { 36 | 37 | $mdDialog.show(dialog(config, event)).then(function () { 38 | SchemaRegistryFactory.putConfig(config).then(function () { 39 | $scope.form = $scope.config.compatibilityLevel = config; 40 | $scope.form = config; 41 | }); 42 | }); 43 | }; 44 | 45 | var backwardText = 'Backward compatibility (default):
A new schema is backward compatible if it can be used to read the data written in all previous schemas.
Backward compatibility is useful for loading data into systems like Hadoop since one can always query data of all versions using the latest schema.'; 46 | var forwardText = 'Forward compatibility:
A new schema is forward compatible if all previous schemas can read data written in this schema.
Forward compatibility is useful for consumer applications that can only deal with data in a particular version that may not always be the latest version.'; 47 | var fullText = "Full compatibility: A new schema is fully compatible if it's both backward and forward compatible."; 48 | var noneText = "No compatibility: A new schema can be any schema as long as it's a valid Avro."; 49 | var backward_transitive = "Backward transitive: Only available for schema registry 3.1.0 and above.
New schema is backward and forward compatible with all previously registered schemas."; 50 | var forward_transitive = "Forward transitive: Only available for schema registry 3.1.0 and above.
All previously registered schemas can read data produced by the new schema."; 51 | var full_transitive = "Full transitive: Only available for schema registry 3.1.0 and above.
New schema can read data produced by all previously registered schemas."; 52 | var text = ''; 53 | 54 | function dialog(config, event) { 55 | 56 | switch (config) { 57 | case "BACKWARD": 58 | text = backwardText; 59 | break; 60 | case "FORWARD": 61 | text = forwardText; 62 | break; 63 | case "FULL": 64 | text = fullText; 65 | break; 66 | case "NONE": 67 | text = noneText; 68 | break; 69 | case "BACKWARD_TRANSITIVE": 70 | text = backward_transitive; 71 | break; 72 | case "FORWARD_TRANSITIVE": 73 | text = forward_transitive; 74 | break; 75 | case "FULL_TRANSITIVE": 76 | text = full_transitive; 77 | break; 78 | default: 79 | text = '' 80 | } 81 | return $mdDialog.confirm() 82 | .title('Warning. You are about to change the \'Global Compatibility Level\'.') 83 | .htmlContent('This will affect the default behaviour and all subjects/schemas that do not have a compatibility level explicitly defined.

' + text) 84 | .targetEvent(event) 85 | .ok('UPDATE') 86 | .cancel('CANCEL'); 87 | } 88 | 89 | }; 90 | 91 | SchemaRegistryConfigCtrl.$inject = ['$scope', '$http', '$log', '$mdDialog', 'SchemaRegistryFactory', 'env']; 92 | 93 | angularAPP.controller('SchemaRegistryConfigCtrl', SchemaRegistryConfigCtrl); 94 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # schema-registry-ui 2 | 3 | [![release](http://github-release-version.herokuapp.com/github/landoop/schema-registry-ui/release.svg?style=flat)](https://github.com/landoop/schema-registry-ui/releases/latest) 4 | [![docker](https://img.shields.io/docker/pulls/landoop/schema-registry-ui.svg?style=flat)](https://hub.docker.com/r/landoop/schema-registry-ui/) 5 | [![Join the chat at https://gitter.im/Landoop/support](https://img.shields.io/gitter/room/nwjs/nw.js.svg?maxAge=2592000)](https://gitter.im/Landoop/support?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 6 | 7 | This is a web tool for the [confluentinc/schema-registry](https://github.com/confluentinc/schema-registry) in order to create / view / search / evolve / view history & configure **Avro** schemas of your Kafka cluster. 8 | 9 | ## Live Demo 10 | [schema-registry-ui.demo.lenses.io](http://schema-registry-ui.demo.lenses.io) 11 | 12 | ## Prerequisites 13 | You will need schema-registry installed with CORS enabled. 14 | 15 | In order to enable CORS, add in `/opt/confluent-3.x.x/etc/schema-registry/schema-registry.properties` 16 | 17 | ``` 18 | access.control.allow.methods=GET,POST,PUT,OPTIONS 19 | access.control.allow.origin=* 20 | ``` 21 | And then restart the [schema-registry] service 22 | 23 | ##### Get the set up locally 24 | We also provide the schema-registry and schema-registry-ui as part of the [fast-data-dev](https://github.com/Landoop/fast-data-dev) docker image for local development setup that also gives all the relevant backends. Just run: 25 | ``` 26 | docker run -d --name=fast-data-dev -p 8081:8081 landoop/fast-data-dev 27 | ``` 28 | Checkout more about fast-data-dev docker container [here](https://github.com/Landoop/fast-data-dev) 29 | 30 | ## Running it via Docker 31 | 32 | To run it via the provided docker image: 33 | 34 | ``` 35 | docker pull landoop/schema-registry-ui 36 | docker run --rm -p 8000:8000 \ 37 | -e "SCHEMAREGISTRY_URL=http://confluent-schema-registry-host:port" \ 38 | landoop/schema-registry-ui 39 | ``` 40 | 41 | Please see the [docker readme](https://github.com/Landoop/schema-registry-ui/tree/master/docker) for more information 42 | and how to enable various features or avoid CORS issues via the proxy flag. 43 | 44 | ## Build from source 45 | 46 | ``` 47 | git clone https://github.com/Landoop/schema-registry-ui.git 48 | cd schema-registry-ui 49 | npm install 50 | npm start 51 | ``` 52 | Web UI will be available at `http://localhost:8080` 53 | 54 | ### Nginx config 55 | 56 | If you use `nginx` to serve this ui, let angular manage routing with 57 | ``` 58 | location / { 59 | try_files $uri $uri/ /index.html =404; 60 | root /folder-with-schema-registry-ui/; 61 | } 62 | ``` 63 | 64 | ### Setup Schema Registry clusters 65 | 66 | Use multiple schema registry clusters in `env.js` : 67 | ``` 68 | var clusters = [ 69 | { 70 | NAME:"prod", 71 | // Schema Registry service URL (i.e. http://localhost:8081) 72 | SCHEMA_REGISTRY: "http://localhost:8081", // https://schema-registry.demo.landoop.com 73 | COLOR: "#141414", // optional 74 | readonlyMode: true // optional 75 | }, 76 | { 77 | NAME:"dev", 78 | SCHEMA_REGISTRY: "http://localhost:8383", 79 | COLOR: "red", // optional 80 | allowGlobalConfigChanges: true, // optional 81 | //allowTransitiveCompatibilities: true // if using a Confluent Platform release >= 3.1.1 uncomment this line 82 | } 83 | ]; 84 | 85 | ``` 86 | * Use `COLOR` to set different header colors for each set up cluster. 87 | * Use `allowGlobalConfigChanges` to enable configuring Global Compatibility Level from the UI. 88 | * Use `allowTransitiveCompatibilities` to enable transitive compatibility levels. This is supported in SR >= 3.1.1 89 | * Use `allowSchemaDeletion` to enable schema deletion from the UI. This is supported in SR >= 3.3.0 90 | * Use `readonlyMode` to prevent any configuration or schema changes from the UI. It overwrites the previous parameters (`allowGlobalConfigChanges`, `allowSchemaDeletion`). 91 | 92 | ## Changelog 93 | [Here](https://github.com/Landoop/schema-registry-ui/wiki/Changelog) 94 | 95 | ## License 96 | 97 | The project is licensed under the [BSL](http://www.landoop.com/bsl) license. 98 | 99 | ## Relevant Projects 100 | 101 | * [kafka-topics-ui](https://github.com/Landoop/kafka-topics-ui), UI to browse Kafka data and work with Kafka Topics 102 | * [kafka-connect-ui](https://github.com/Landoop/kafka-connect-ui), Set up and manage connectors for multiple connect clusters 103 | * [fast-data-dev](https://github.com/Landoop/fast-data-dev), Docker for Kafka developers (schema-registry,kafka-rest,zoo,brokers,landoop) 104 | * [Landoop-On-Cloudera](https://github.com/Landoop/Landoop-On-Cloudera), Install and manage your kafka streaming-platform on you Cloudera CDH cluster 105 | 106 | 107 | 108 | www.landoop.com 109 | -------------------------------------------------------------------------------- /docker/README.md: -------------------------------------------------------------------------------- 1 | ## Schema Registry UI ## 2 | 3 | [![](https://images.microbadger.com/badges/image/landoop/schema-registry-ui.svg)](http://microbadger.com/images/landoop/schema-registry-ui) 4 | 5 | This is a small docker image for Landoop's schema-registry-ui. 6 | It serves the schema-registry-ui from port 8000 by default. 7 | A live version can be found at 8 | 9 | The software is stateless and the only necessary option is your Schema Registry 10 | URL. 11 | 12 | To run it: 13 | 14 | docker run --rm -p 8000:8000 \ 15 | -e "SCHEMAREGISTRY_URL=http://schema.registry.url" \ 16 | landoop/schema-registry-ui 17 | 18 | Visit http://localhost:8000 to see the UI. 19 | 20 | ### Advanced Settings 21 | 22 | Four of the Schema Registry UI settings need to be enabled explicitly. These 23 | are: 24 | 25 | 1. Support for global compatibility level configuration support —i.e change the 26 | default compatibility level of your schema registry. 27 | 2. Support for transitive compatibility levels (Schema Registry version 3.1.1 or better). 28 | 3. Support for Schema deletion (Schema Registry version 3.3.0 or better). 29 | 4. Support for readonly mode (overwrites settings for global compatibility configuration and schema deletion) 30 | 31 | They are handled by the `ALLOW_GLOBAL`, `ALLOW_TRANSITIVE`, `ALLOW_DELETION` and `READONLY_MODE` 32 | environment variables. E.g: 33 | 34 | docker run --rm -p 8000:8000 \ 35 | -e "SCHEMAREGISTRY_URL=http://schema.registry.url" \ 36 | -e ALLOW_GLOBAL=1 \ 37 | -e ALLOW_TRANSITIVE=1 \ 38 | -e ALLOW_DELETION=1 \ 39 | -e READONLY_MODE=1 \ 40 | landoop/schema-registry-ui 41 | 42 | docker run --rm -p 8000:8000 \ 43 | -e "SCHEMAREGISTRY_URL=http://schema.registry.url" \ 44 | -e READONLY_MODE=1 \ 45 | landoop/schema-registry-ui 46 | 47 | ### Proxying Schema Registry 48 | 49 | If you have CORS issues or want to pass through firewalls and maybe share your 50 | server, we added the `PROXY` option. Run the container with `-e PROXY=true` and 51 | Caddy server will proxy the traffic to Schema Registry: 52 | 53 | docker run --rm -p 8000:8000 \ 54 | -e "SCHEMAREGISTRY_URL=http://schema.registry.url" \ 55 | -e "PROXY=true" \ 56 | landoop/schema-registry-ui 57 | 58 | > **Important**: When proxying, for the `SCHEMAREGISTRY_URL` you have to use an 59 | > IP address or a domain that can be resolved to it. **You can't use** 60 | > `localhost` even if you serve Schema Registry from your localhost. The reason 61 | > for this is that a docker container has its own network, so your _localhost_ 62 | > is different from the container's _localhost_. As an example, if you are in 63 | > your home network and have an IP address of `192.168.5.65` and run Schema 64 | > Registry from your computer, instead of `http://127.0.0.1:8082` you must use 65 | > `http://192.168.5.65:8082`. 66 | 67 | If your Schema Registry uses self-signed SSL certificates, you can use the 68 | `PROXY_SKIP_VERIFY=true` environment variable to instruct the proxy to 69 | not verify the backend TLS certificate. 70 | 71 | ## Configuration options 72 | 73 | ### Schema Registry UI 74 | 75 | You can control most of Kafka Topics UI settings via environment variables: 76 | 77 | * `SCHEMAREGISTRY_URL` 78 | * `ALLOW_GLOBAL=[true|false]` (default false) 79 | * `ALLOW_TRANSITIVE=[true|false]` (default false) 80 | * `ALLOW_DELETION=[true|false]` (default false). 81 | * `READONLY_MODE=[true|false]` (default false). 82 | 83 | ## Docker Options 84 | 85 | - `PROXY=[true|false]` 86 | 87 | Whether to proxy Schema Registry endpoint via the internal webserver 88 | - `PROXY_SKIP_VERIFY=[true|false]` 89 | 90 | Whether to accept self-signed certificates when proxying Schema Registry 91 | via https 92 | - `PORT=[PORT]` 93 | 94 | The port number to use for schema-registry-ui. The default is `8000`. 95 | Usually the main reason for using this is when you run the 96 | container with `--net=host`, where you can't use docker's publish 97 | flag (`-p HOST_PORT:8000`). 98 | - `CADDY_OPTIONS=[OPTIONS]` 99 | 100 | The webserver that powers the image is Caddy. Via this variable 101 | you can add options that will be appended to its configuration 102 | (Caddyfile). Variables than span multiple lines are supported. 103 | 104 | As an example, you can set Caddy to not apply timeouts via: 105 | 106 | -e "CADDY_OPTIONS=timeouts none" 107 | 108 | Or you can set basic authentication via: 109 | 110 | -e "CADDY_OPTIONS=basicauth / [USER] [PASS]" 111 | 112 | - `RELATIVE_PROXY_URL=[true|false]` 113 | 114 | When proxying Schema Registry, enabling this option will set the Schema 115 | Registry endpoint in the UI as a relative URL. This can help when running 116 | Schema Registry UI under a subpath of your server (e.g 117 | `http://url:8000/sr-ui` instead of `http://url:8000/`). 118 | 119 | # Schema Registry Configuration 120 | 121 | If you don't wish to proxy Schema Registry's api, you should permit CORS via setting 122 | `access.control.allow.methods=GET,POST,PUT,DELETE,OPTIONS` and 123 | `access.control.allow.origin=*`. 124 | 125 | # Logging 126 | 127 | In the latest iterations, the container will print informational messages during 128 | startup at stderr and web server logs at stdout. This way you may sent the logs 129 | (stdout) to your favorite log management solution. 130 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Pulling in css libs 5 | */ 6 | require('font-awesome/css/font-awesome.min.css'); 7 | require('angular-material/angular-material.min.css'); 8 | require('angular-material-data-table/dist/md-data-table.min.css'); 9 | require('angular-json-tree/dist/angular-json-tree.css'); 10 | require('./assets/css/styles.css'); 11 | /** 12 | * Requiring in libs here for the time being 13 | * Going forward the require/import should be done in the file that needs it 14 | */ 15 | require('expose-loader?diff_match_patch!diff-match-patch'); 16 | window.DIFF_INSERT = require('exports-loader?DIFF_INSERT!diff-match-patch/index'); 17 | window.DIFF_DELETE = require('exports-loader?DIFF_DELETE!diff-match-patch/index'); 18 | window.DIFF_EQUAL = require('exports-loader?DIFF_EQUAL!diff-match-patch/index'); 19 | 20 | require('jszip'); 21 | require('jszip-utils'); 22 | 23 | require('angular'); 24 | require('angular-utils-pagination/dirPagination'); 25 | require('angular-ui-ace'); 26 | require('angular-route'); 27 | require('angular-sanitize'); 28 | require('angular-material'); 29 | require('angular-animate'); 30 | require('angular-aria'); 31 | require('angular-material-data-table'); 32 | require('angular-diff-match-patch'); 33 | require('angular-json-tree'); 34 | 35 | 36 | var angularAPP = angular.module('angularAPP', [ 37 | 'ui.ace', 38 | 'angularUtils.directives.dirPagination', 39 | 'ngRoute', 40 | 'ngMaterial', 41 | 'ngAnimate', 42 | 'ngAria', 43 | 'md.data.table', 44 | 'diff-match-patch', 45 | 'angular-json-tree', 46 | 'ngSanitize' 47 | ]); 48 | 49 | /** 50 | * 51 | */ 52 | require('./schema-registry'); 53 | require('./factories'); 54 | /** 55 | * Templates 56 | */ 57 | var homeTemplate = require('./schema-registry/home/home.html'); 58 | var newTemplate = require('./schema-registry/new/new.html'); 59 | var exportTemplate = require('./schema-registry/export/export.html'); 60 | var viewTemplate = require('./schema-registry/view/view.html'); 61 | var configTemplate = require('./schema-registry/config/config.html'); 62 | var listTemplate = require('./schema-registry/list/list.html'); 63 | var dirPaginationControlsTemplate = require('./schema-registry/pagination/dirPaginationControlsTemplate.html'); 64 | 65 | 66 | var HeaderCtrl = function ($rootScope, $scope, $location, $log, SchemaRegistryFactory, env) { 67 | 68 | $scope.$on('$routeChangeSuccess', function () { 69 | $rootScope.clusters = env.getClusters(); 70 | $scope.cluster = env.getSelectedCluster(); 71 | $scope.color = $scope.cluster.COLOR; 72 | }); 73 | 74 | $scope.updateEndPoint = function (cluster) { 75 | $rootScope.connectionFailure = false; 76 | $location.path("/cluster/" + cluster) 77 | } 78 | }; 79 | 80 | HeaderCtrl.$inject = ['$rootScope', '$scope', '$location', '$log', 'SchemaRegistryFactory', 'env']; 81 | 82 | angularAPP.controller('HeaderCtrl', HeaderCtrl); 83 | 84 | /* Custom directives */ 85 | 86 | angularAPP.directive('validJson', function () { 87 | return { 88 | require: 'ngModel', 89 | priority: 1000, 90 | link: function (scope, elem, attrs, ngModel) { 91 | 92 | // view to model 93 | ngModel.$parsers.unshift(function (value) { 94 | var valid = true, 95 | obj; 96 | try { 97 | obj = JSON.parse(value); 98 | } catch (ex) { 99 | valid = false; 100 | } 101 | ngModel.$setValidity('validJson', valid); 102 | return valid ? obj : undefined; 103 | }); 104 | 105 | // model to view 106 | ngModel.$formatters.push(function (value) { 107 | return value;//JSON.stringify(value, null, '\t'); 108 | }); 109 | } 110 | }; 111 | }); 112 | 113 | 114 | angularAPP.filter('reverse', function () { 115 | return function (items) { 116 | return items.slice().reverse(); 117 | }; 118 | }); 119 | 120 | 121 | angularAPP.config(['$compileProvider', '$mdThemingProvider', '$routeProvider', 122 | function ($compileProvider, $mdThemingProvider, $routeProvider) { 123 | $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|ftp|mailto|tel|file|blob):/); 124 | 125 | $mdThemingProvider.theme('default') 126 | .primaryPalette('blue-grey') 127 | .accentPalette('blue') 128 | .warnPalette('grey'); 129 | 130 | $routeProvider 131 | .when('/', { 132 | template: homeTemplate, 133 | controller: 'HomeCtrl' 134 | }) 135 | .when('/cluster/:cluster', { 136 | template: homeTemplate, 137 | controller: 'HomeCtrl' 138 | }) 139 | .when('/cluster/:cluster/schema/new', { 140 | template: newTemplate, 141 | controller: 'NewSubjectCtrl as ctrl' 142 | }) 143 | .when('/cluster/:cluster/export', { 144 | template: exportTemplate, 145 | controller: 'ExportSchemasCtrl' 146 | }) 147 | .when('/cluster/:cluster/schema/:subject/version/:version', { 148 | template: viewTemplate, 149 | controller: 'SubjectsCtrl' 150 | }) 151 | .otherwise({ 152 | redirectTo: '/' 153 | }); 154 | } 155 | // $locationProvider.html5Mode(true); 156 | ]); 157 | 158 | 159 | angularAPP.run(['env', '$routeParams', '$rootScope', '$templateCache', 160 | function loadRoute(env, $routeParams, $rootScope, $templateCache) { 161 | $rootScope.$on('$routeChangeSuccess', function () { 162 | env.setSelectedCluster($routeParams.cluster); 163 | }); 164 | 165 | $templateCache.put('config.html', configTemplate); 166 | $templateCache.put('list.html', listTemplate); 167 | $templateCache.put('angularUtils.directives.dirPagination.template', dirPaginationControlsTemplate); 168 | } 169 | ]); 170 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Schema Registry UI 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 |
21 | SCHEMA REGISTRY 22 |
23 | 24 | 25 | EXPORT SCHEMAS 26 | 27 | 28 | 29 | 30 | 31 | {{connectEndPoint.NAME}} 33 | 34 | 35 |
36 |
37 | 38 |
39 | 40 | 41 |

Missing Cluster Configuration

42 |
In order to configure schema-registry-ui you need to add env.js file in the 43 | root directory of the app. 44 |
Example env.js structure: 45 |
 46 |         
 47 | var clusters = [
 48 |    {
 49 |        NAME:"prod",
 50 |        // Schema Registry service URL (i.e. http://localhost:8081)
 51 |        SCHEMA_REGISTRY: "http://localhost:8081", // https://schema-registry.demo.landoop.com
 52 |        COLOR: "#141414" // optional
 53 |      },
 54 |      {
 55 |        NAME:"dev",
 56 |        SCHEMA_REGISTRY: "http://localhost:8383",
 57 |        COLOR: "red", // optional
 58 |        allowGlobalConfigChanges: true, // optional
 59 |        //allowTransitiveCompatibilities: true        // if using a Confluent Platform release >= 3.1.1 uncomment this line
 60 |      }
 61 |   ];
 62 |         
 63 |       
64 |
65 |
66 |
67 | 68 | 69 |
71 |
72 |
73 |
74 |
75 |
76 |
77 | 78 |
79 |
80 |
81 | 82 | 83 | 84 | 85 | 86 | 87 | 105 | 106 | 107 | 108 | 121 | 122 | 123 | 124 | 125 | -------------------------------------------------------------------------------- /src/schema-registry/view/view.controller.js: -------------------------------------------------------------------------------- 1 | var angular = require('angular'); 2 | var angularAPP = angular.module('angularAPP'); 3 | var ace = require('brace'); 4 | require('brace/mode/json'); 5 | require('brace/mode/batchfile'); 6 | require('brace/theme/chrome'); 7 | require('brace/worker/json'); 8 | require.context("brace/ext/", false); 9 | //var Range = ace.acequire('ace/range').Range; 10 | 11 | 12 | var SubjectsCtrl = function ($rootScope, $scope, $route, $routeParams, $log, $location, $mdDialog, SchemaRegistryFactory, UtilsFactory, toastFactory, Avro4ScalaFactory, env) { 13 | 14 | $log.info("Starting schema-registry controller: view ( " + $routeParams.subject + "/" + $routeParams.version + " )"); 15 | $rootScope.listChanges = false; 16 | toastFactory.hideToast(); 17 | 18 | /** 19 | * At start-up - get the entire subject `History` 20 | */ 21 | SchemaRegistryFactory.getSubjectHistory($routeParams.subject).then( 22 | function success(data) { 23 | $scope.completeSubjectHistory = SchemaRegistryFactory.getSubjectHistoryDiff(data); 24 | //$log.warn("Diff is:"); 25 | //$log.warn(JSON.stringify($scope.completeSubjectHistory)); 26 | } 27 | ); 28 | $scope.allowSchemaDeletion = env.allowSchemaDeletion(); 29 | $scope.readonlyMode = env.readonlyMode(); 30 | $scope.allowTransitiveCompatibilities = env.allowTransitiveCompatibilities(); 31 | 32 | $scope.$watch(function () { 33 | return $scope.aceString; 34 | }, function (a) { 35 | $scope.isAvroUpdatedAndCompatible = false; 36 | }, true); 37 | 38 | SchemaRegistryFactory.getSubjectConfig($routeParams.subject).then( 39 | function success(config) { 40 | $scope.compatibilitySelect = config.compatibilityLevel; 41 | $scope.existingValue = config.compatibilityLevel; 42 | }, 43 | function errorCallback(response) { 44 | $log.error(response); 45 | }); 46 | 47 | SchemaRegistryFactory.getGlobalConfig().then( 48 | function success(config) { 49 | $scope.globalConfig = config.compatibilityLevel; 50 | }, 51 | function failure(response) { 52 | $log.error("Failure with : " + JSON.stringify(response)); 53 | $scope.connectionFailure = true; 54 | }); 55 | 56 | 57 | /** 58 | * At start-up do something more ... 59 | */ 60 | function getSchema(){ 61 | var promise = SchemaRegistryFactory.getSubjectAtVersion($routeParams.subject, $routeParams.version); 62 | promise.then(function (selectedSubject) { 63 | $log.info('Success fetching [' + $routeParams.subject + '/' + $routeParams.version + '] with MetaData'); 64 | $rootScope.subjectObject = selectedSubject; 65 | 66 | $scope.arraySchema = typeof $rootScope.subjectObject.Schema[0] !== 'undefined' ? true : false; 67 | $scope.tableWidth = 100 / $scope.subjectObject.Schema.length; 68 | 69 | 70 | $rootScope.schema = selectedSubject.Schema.fields; 71 | 72 | $scope.aceString = angular.toJson(selectedSubject.Schema, true); 73 | $scope.aceStringOriginal = $scope.aceString; 74 | $scope.aceReady = true; 75 | SchemaRegistryFactory.getSubjectsVersions($routeParams.subject).then( 76 | function success(allVersions) { 77 | var otherVersions = []; 78 | angular.forEach(allVersions, function (version) { 79 | if (version !== $rootScope.subjectObject.version) { 80 | otherVersions.push(version); 81 | } 82 | }); 83 | $scope.otherVersions = otherVersions; 84 | $scope.multipleVersionsOn = $scope.otherVersions.length > 0; // TODO remove 85 | }, 86 | function failure(response) { 87 | // TODO 88 | } 89 | ) 90 | }, function (reason) { 91 | $log.error('Failed: ' + reason); 92 | }, function (update) { 93 | $log.info('Got notification: ' + update); 94 | }); 95 | } 96 | if ($routeParams.subject && $routeParams.version) { 97 | getSchema(); 98 | } 99 | $scope.$on('$routeChangeSuccess', function () { 100 | $scope.cluster = env.getSelectedCluster().NAME;//$routeParams.cluster; 101 | $scope.maxHeight = window.innerHeight - 215; 102 | if ($scope.maxHeight < 310) { 103 | $scope.maxHeight = 310 104 | } 105 | }); 106 | 107 | $scope.updateCompatibility = function (compatibilitySelect) { 108 | SchemaRegistryFactory.updateSubjectCompatibility($routeParams.subject, compatibilitySelect).then( 109 | function success() { 110 | $scope.existingValue = compatibilitySelect; 111 | $rootScope.listChanges = true; // trigger a cache re-load 112 | $scope.success = true; 113 | }); 114 | }; 115 | 116 | $scope.aceString = ""; 117 | $scope.aceStringOriginal = ""; 118 | $scope.multipleVersionsOn = false; 119 | 120 | $scope.isAvroUpdatedAndCompatible = false; 121 | $scope.testAvroCompatibility = function () { 122 | $log.debug("Testing Avro compatibility"); 123 | if ($scope.aceString === $scope.aceStringOriginal) { 124 | toastFactory.showSimpleToastToTop("You have not changed the schema"); 125 | } else { 126 | if (UtilsFactory.IsJsonString($scope.aceString)) { 127 | $scope.aceBackgroundColor = "rgba(0, 128, 0, 0.04)"; 128 | $log.debug("Edited schema is a valid json and is a augmented"); 129 | SchemaRegistryFactory.testSchemaCompatibility($routeParams.subject, $scope.aceString).then( 130 | function success(result) { 131 | if (result) { 132 | $log.info("Schema is compatible"); 133 | $scope.aceBackgroundColor = "rgba(0, 128, 0, 0.04)"; 134 | toastFactory.showSimpleToast("You can now evolve the schema"); 135 | $scope.isAvroUpdatedAndCompatible = true; 136 | } else { 137 | $scope.aceBackgroundColor = "rgba(255, 255, 0, 0.10)"; 138 | toastFactory.showLongToast("This schema is incompatible with the latest version"); 139 | } 140 | }, 141 | function failure(data) { 142 | if (data.error_code === 500) { 143 | $scope.aceBackgroundColor = "rgba(255, 255, 0, 0.10)"; 144 | toastFactory.showSimpleToastToTop("Not a valid avro"); 145 | } 146 | else { 147 | $log.error("Could not test compatibilitydasdas", data); 148 | } 149 | }); 150 | } else { 151 | $scope.aceBackgroundColor = "rgba(255, 255, 0, 0.10)"; 152 | toastFactory.showLongToast("Invalid Avro"); 153 | } 154 | } 155 | }; 156 | 157 | $scope.evolveAvroSchema = function () { 158 | if ($scope.aceString !== $scope.aceStringOriginal && 159 | UtilsFactory.IsJsonString($scope.aceString)) { 160 | SchemaRegistryFactory.testSchemaCompatibility($routeParams.subject, $scope.aceString).then( 161 | function success(result) { 162 | var latestSchema = SchemaRegistryFactory.getLatestSubjectFromCache($routeParams.subject); 163 | $log.warn("peiler"); 164 | $log.warn(latestSchema); 165 | var latestID = latestSchema.id; 166 | SchemaRegistryFactory.registerNewSchema($routeParams.subject, $scope.aceString).then( 167 | function success(schemaId) { 168 | $log.info("Latest schema ID was : " + latestID); 169 | $log.info("New schema ID is : " + schemaId); 170 | if (latestID === schemaId) { 171 | toastFactory.showSimpleToastToTop(" Schema is the same as latest ") 172 | } else { 173 | toastFactory.showSimpleToastToTop(" Schema evolved to ID: " + schemaId); 174 | $rootScope.$broadcast('newEvolve'); 175 | $location.path('/cluster/' + $scope.cluster + '/schema/' + $routeParams.subject + '/version/latest'); 176 | $route.reload(); 177 | } 178 | }, 179 | function failure(data) { 180 | } 181 | ); 182 | }, 183 | function failure(data) { 184 | 185 | } 186 | ); 187 | } else { 188 | $scope.aceBackgroundColor = "rgba(255, 255, 0, 0.10)"; 189 | toastFactory.showLongToast("Invalid Avro"); 190 | } 191 | }; 192 | 193 | $scope.isAvroAceEditable = false; 194 | $scope.aceBackgroundColor = "white"; 195 | $scope.cancelEditor = function () { 196 | $scope.selectedIndex = 0; 197 | $log.info("Canceling editor"); 198 | $scope.maxHeight = $scope.maxHeight + 64; 199 | $scope.form.json.$error.validJson = false; 200 | $scope.aceBackgroundColor = "white"; 201 | toastFactory.hideToast(); 202 | $log.info("Setting " + $scope.aceStringOriginal); 203 | $scope.isAvroAceEditable = false; 204 | $scope.isAvroUpdatedAndCompatible = false; 205 | $scope.aceString = $scope.aceStringOriginal; 206 | $scope.aceSchemaSession.setValue($scope.aceString); 207 | 208 | }; 209 | 210 | $scope.toggleEditor = function () { 211 | $scope.isAvroAceEditable = !$scope.isAvroAceEditable; 212 | if ($scope.isAvroAceEditable) { 213 | $scope.maxHeight = $scope.maxHeight - 64; 214 | toastFactory.showLongToast("You can now edit the schema"); 215 | $scope.aceBackgroundColor = "rgba(0, 128, 0, 0.04)"; 216 | } else { 217 | $scope.aceBackgroundColor = "white"; 218 | toastFactory.hideToast(); 219 | } 220 | }; 221 | 222 | /************************* md-table ***********************/ 223 | $scope.tableOptions = { 224 | rowSelection: false, 225 | multiSelect: false, 226 | autoSelect: false, 227 | decapitate: false, 228 | largeEditDialog: false, 229 | boundaryLinks: false, 230 | limitSelect: true, 231 | pageSelect: true 232 | }; 233 | 234 | $scope.query = { 235 | order: 'name', 236 | limit: 100, 237 | page: 1 238 | }; 239 | 240 | // This one is called each time - the user clicks on an md-table header (applies sorting) 241 | $scope.logOrder = function (a) { 242 | // $log.info("Ordering event " + a); 243 | sortSchema(a); 244 | }; 245 | 246 | function sortSchema(type) { 247 | var reverse = 1; 248 | if (type.indexOf('-') === 0) { 249 | // remove the - symbol 250 | type = type.substring(1, type.length); 251 | reverse = -1; 252 | } 253 | // $log.info(type + " " + reverse); 254 | $scope.schema = UtilsFactory.sortByKey($scope.schema, type, reverse); 255 | } 256 | 257 | function getScalaFiles(xx) { 258 | var scala = Avro4ScalaFactory.getScalaFiles(xx); 259 | $log.error("SCALA-> " + scala); 260 | } 261 | 262 | $scope.otherTabSelected = function () { 263 | $scope.hideEdit = true; 264 | }; 265 | 266 | /************************* md-table ***********************/ 267 | $scope.editor; 268 | 269 | // When the 'Ace' schema/view is loaded 270 | $scope.viewSchemaAceLoaded = function (_editor) { 271 | // $log.info("me"); 272 | $scope.editor = _editor; 273 | $scope.editor.$blockScrolling = Infinity; 274 | $scope.aceSchemaSession = _editor.getSession(); // we can get data on changes now 275 | $scope.editor.getSession().setUseWrapMode(true); 276 | $scope.editor.getSession().setBehavioursEnabled(false); 277 | 278 | 279 | var lines = $scope.aceString.split("\n").length; 280 | // TODO : getScalaFiles($scope.aceString); 281 | // Add one extra line for each command > 110 characters 282 | angular.forEach($scope.aceString.split("\n"), function (line) { 283 | lines = lines + Math.floor(line.length / 110); 284 | }); 285 | if (lines <= 1) { 286 | lines = 10; 287 | } 288 | // $log.warn("Lines loaded for curl create connector -> " + lines + "\n" + $scope.curlCommand); 289 | _editor.setOptions({ 290 | minLines: lines, 291 | maxLines: lines, 292 | highlightActiveLine: false 293 | }); 294 | // var _renderer = _editor.renderer; 295 | // _renderer.animatedScroll = false; 296 | }; 297 | 298 | // When the 'Ace' schema/view is CHANGED 299 | $scope.viewSchemaAceChanged = function (_editor) { 300 | $scope.editor = _editor; 301 | var aceString = $scope.aceSchemaSession.getDocument().getValue(); 302 | // $log.warn("LOADED ...."); 303 | // Highlight differences 304 | //TODO 305 | //$scope.aceSchemaSession.addMarker(new Range(2, 5, 4, 16), "ace_diff_new_line", "fullLine"); 306 | $scope.aceString = aceString; 307 | }; 308 | 309 | $scope.showTree = function (keyOrValue) { 310 | return !(angular.isNumber(keyOrValue) || angular.isString(keyOrValue) || (keyOrValue === null)); 311 | } 312 | 313 | $scope.askForConfirmToDelete = function (version){ 314 | $scope.versionToBeDeleted = version; 315 | $scope.showDeleteConfirmation = true; 316 | } 317 | 318 | $scope.deleteSchema = function (versionToBeDeleted) { 319 | $scope.showDeleteConfirmation =false; 320 | 321 | var subjectName = $routeParams.subject 322 | if(versionToBeDeleted) { 323 | SchemaRegistryFactory.deleteVersionOfSubject(subjectName, versionToBeDeleted).then(function(){ 324 | $rootScope.listChanges = true; 325 | toastFactory.showLongToast(subjectName + " version " + versionToBeDeleted + " deleted successfully"); 326 | getSchema() 327 | $location.path('/cluster/'+ $scope.cluster + '/schema/' + subjectName + '/version/latest') 328 | }) 329 | } 330 | else { 331 | SchemaRegistryFactory.deleteSubject(subjectName).then(function(){ 332 | $rootScope.listChanges = true; 333 | toastFactory.showLongToast(subjectName + "deleted successfully"); 334 | $location.path('/cluster/'+ $scope.cluster) 335 | 336 | }) 337 | 338 | } 339 | } 340 | 341 | }; 342 | 343 | SubjectsCtrl.$inject = ['$rootScope', '$scope', '$route', '$routeParams', '$log', '$location', '$mdDialog', 'SchemaRegistryFactory', 'UtilsFactory', 'toastFactory', 'Avro4ScalaFactory', 'env'] 344 | 345 | angularAPP.controller('SubjectsCtrl', SubjectsCtrl); //end of controller 346 | 347 | // Useful for browsing through different versions of a schema 348 | angularAPP.directive('clickLink', ['$location', function ($location) { 349 | return { 350 | link: function (scope, element, attrs) { 351 | element.on('click', function () { 352 | scope.$apply(function () { 353 | $location.path(attrs.clickLink); 354 | }); 355 | }); 356 | } 357 | } 358 | 359 | }]); 360 | -------------------------------------------------------------------------------- /src/assets/css/styles.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | height: 100%; 3 | margin: 0; 4 | padding: 0; 5 | background-color: #F5F5F5; 6 | font-size: 14px; 7 | font-family: sans-serif; 8 | -webkit-text-size-adjust: 100%; 9 | -ms-text-size-adjust: 100%; 10 | } 11 | 12 | a { 13 | color: #337ab7; 14 | text-decoration: none; 15 | } 16 | 17 | .ace_editor { 18 | height: 460px; 19 | } 20 | 21 | .ace_gutter { 22 | background: #f9f9f9 !important; 23 | } 24 | 25 | .searchSchemas { 26 | border: 0; 27 | margin: 10px auto; 28 | display: block; 29 | width: 100%; 30 | height: 36px; 31 | border-bottom: 1px solid #ddd; 32 | outline: none; 33 | text-indent: 15px; 34 | } 35 | 36 | .searchSchemas:focus, .searchSchemas:focused { 37 | outline: none; 38 | } 39 | 40 | header.header { 41 | background-color: #141414; 42 | min-height: 20px; 43 | color: #ebebeb; 44 | margin-bottom: 0; 45 | } 46 | 47 | json-tree .branch-preview { 48 | overflow: hidden; 49 | font-style: italic; 50 | max-width: 90%; 51 | height: 1.5em; 52 | opacity: .7; 53 | } 54 | 55 | .header-section { 56 | background-color: #4F6375; 57 | color: #ebebeb; 58 | border-bottom: 0 solid #2b3e50; 59 | padding: 4px 0; 60 | margin-bottom: 0; 61 | } 62 | 63 | .selectCluster { 64 | margin: 7px 10px; 65 | text-indent: 15px; 66 | padding: 0; 67 | line-height: 1.2; 68 | } 69 | 70 | .selectCluster md-select { 71 | background: white; 72 | } 73 | 74 | .selectCluster md-select md-option, 75 | .selectCluster md-select md-select-value { 76 | color: #333 77 | } 78 | 79 | .selectClusterLabel, 80 | .exportSchemas { 81 | color: rgb(204, 204, 204); 82 | font-size: 12px; 83 | line-height: 44px; 84 | padding-top: 0; 85 | margin: 0; 86 | } 87 | 88 | .exportSchemas { 89 | margin-right: 40px 90 | } 91 | 92 | md-input-container.md-default-theme label.md-required:after, md-input-container label.md-required:after { 93 | color: red; 94 | } 95 | 96 | .buttonGroup { 97 | top: 66px; 98 | text-align: right; 99 | right: 0; 100 | position: absolute; 101 | } 102 | 103 | .md-button.blue, 104 | .md-button.md-small.blue, 105 | .md-raised.md-primary.md-button.md-ink-ripple.blue { 106 | background-color: rgb(65, 191, 236); 107 | color: white; 108 | } 109 | 110 | .md-button.blue:hover, 111 | .md-button.md-small.blue:hover, 112 | .md-raised.md-primary.md-button.md-ink-ripple.blue:hover { 113 | background-color: rgb(60, 178, 216); 114 | } 115 | 116 | .md-button.md-small.green, 117 | .md-raised.md-primary.md-button.md-ink-ripple.green, 118 | .md-primary.md-button.green { 119 | background-color: rgb(139, 195, 74); 120 | color: white; 121 | } 122 | 123 | .md-button.md-small.green:hover, 124 | .md-raised.md-primary.md-button.md-ink-ripple.green:hover, 125 | .md-primary.md-button.green:hover { 126 | background-color: rgb(119, 175, 64); 127 | } 128 | 129 | .md-button.md-small.green[disabled], 130 | .md-raised.md-primary.md-button.md-ink-ripple.green[disabled], 131 | .md-primary.md-button.green[disabled], 132 | .md-button.blue[disabled], 133 | .md-button.md-small.blue[disabled], 134 | .md-raised.md-primary.md-button.md-ink-ripple.blue[disabled] { 135 | background-color: #ccc; 136 | } 137 | 138 | .md-button.md-small { 139 | width: 20px; 140 | height: 20px; 141 | line-height: 20px; 142 | min-height: 20px; 143 | vertical-align: top; 144 | font-size: 10px; 145 | padding: 0 0; 146 | margin: 0; 147 | } 148 | 149 | .exportSchemas:hover { 150 | color: rgb(230, 230, 230) 151 | } 152 | 153 | [ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak { 154 | display: none !important; 155 | } 156 | 157 | .container-fluid-centered { 158 | height: 100%; 159 | display: table; 160 | width: 100%; 161 | padding: 0; 162 | } 163 | 164 | a:link, header.a:visited, header.a:hover, header.a:active { 165 | text-decoration: none; 166 | } 167 | 168 | .row-fluid { 169 | height: 100%; 170 | display: table-cell; 171 | vertical-align: middle; 172 | } 173 | 174 | .textareacode { 175 | border-radius: 3px; 176 | outline: none; 177 | box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.075); 178 | border: 1px solid #ccc; 179 | width: 95%; 180 | font: 12px Consolas, Menlo, Courier, monospace; 181 | } 182 | 183 | .example { 184 | position: relative; 185 | margin: 15px 0 0; 186 | padding: 39px 19px 14px; 187 | background-color: #fff; 188 | border-radius: 4px 4px 0 0; 189 | border: 1px solid #ddd; 190 | z-index: 2; 191 | } 192 | 193 | .example1:after { 194 | content: "Register a Complex Schema"; 195 | } 196 | 197 | .example2:after { 198 | content: "Test Schema Compatibility"; 199 | } 200 | 201 | .example:after { 202 | position: absolute; 203 | top: 0; 204 | left: 0; 205 | padding: 2px 8px; 206 | font-size: 12px; 207 | font-weight: bold; 208 | background-color: #f5f5f5; 209 | color: #9da0a4; 210 | border-radius: 4px 0 4px 0; 211 | } 212 | 213 | .snippet { 214 | position: relative; 215 | overflow: visible; 216 | } 217 | 218 | pre { 219 | margin-top: 0; 220 | margin-bottom: 0; 221 | font: 12px Consolas, "Liberation Mono", Menlo, Courier, monospace; 222 | } 223 | 224 | .centering { 225 | float: none; 226 | margin: 0 auto; 227 | } 228 | 229 | .bs-callout-warning { 230 | border-left-color: #228415 !important; 231 | } 232 | 233 | .bs-callout { 234 | padding: 10px; 235 | margin: 10px 0; 236 | border: 1px solid #eee; 237 | border-left-width: 5px; 238 | border-radius: 3px; 239 | } 240 | 241 | /* Style The Dropdown Button */ 242 | .dropbtn { 243 | background-color: #4f6375; 244 | color: white; 245 | font-size: 16px; 246 | padding: 4px; 247 | border: none; 248 | cursor: pointer; 249 | min-width: 250px; 250 | } 251 | 252 | /* The container
- needed to position the dropdown content */ 253 | .dropdown { 254 | position: relative; 255 | display: inline-block; 256 | z-index: 999; 257 | } 258 | 259 | /* Dropdown Content (Hidden by Default) */ 260 | .dropdown-content { 261 | display: none; 262 | position: absolute; 263 | background-color: green; 264 | min-width: 250px; 265 | box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.2); 266 | } 267 | 268 | /* Links inside the dropdown */ 269 | .dropdown-content a { 270 | color: black; 271 | padding: 12px 16px; 272 | text-decoration: none; 273 | display: block; 274 | } 275 | 276 | /* Change color of dropdown links on hover */ 277 | .dropdown-content a:hover { 278 | background-color: #6e8294 279 | } 280 | 281 | /* Show the dropdown menu on hover */ 282 | .dropdown:hover .dropdown-content { 283 | display: block; 284 | background-color: #4f6375; 285 | } 286 | 287 | /* Change the background color of the dropdown button when the dropdown content is shown */ 288 | .dropdown:hover .dropbtn { 289 | background-color: #4f6375; 290 | } 291 | 292 | md-card { 293 | margin: 0; 294 | } 295 | 296 | md-toast.md-default-theme .md-toast-content, md-toast .md-toast-content { 297 | margin-top: 70px; 298 | margin-right: 25px; 299 | background-color: white; 300 | color: black; 301 | text-align: center; 302 | } 303 | 304 | md-chips.md-default-theme .md-chips, md-chips .md-chips { 305 | box-shadow: none; 306 | } 307 | 308 | .md-button.light-blue { 309 | background-color: rgba(74, 163, 223, 1); 310 | } 311 | 312 | /** Pagination **/ 313 | .label-primary { 314 | background-color: #286090; 315 | } 316 | 317 | /* Useful for the `New Subject` auto-complete field */ 318 | .md-whiteframe-1dp, .md-whiteframe-z1 { 319 | box-shadow: none; 320 | } 321 | 322 | /*.md-whiteframe-2dp {*/ 323 | /*box-shadow: 0 1px 5px 0 rgba(0, 0, 0, 0.2), 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 3px 1px -2px rgba(0, 0, 0, 0.12);*/ 324 | /*}*/ 325 | 326 | /*.md-whiteframe-2dp:focus {*/ 327 | /*box-shadow: 0 1px 5px 0 rgba(0, 0, 0, 0.4), 0 2px 2px 0 rgba(0, 0, 0, 0.28), 0 3px 1px -2px rgba(0, 0, 0, 0.24);*/ 328 | /*}*/ 329 | 330 | .md-button.light-blue { 331 | background-color: rgba(74, 163, 223, 1); 332 | } 333 | 334 | .md-button.md-fab { 335 | width: 42px; 336 | height: 42px; 337 | background-color: white; 338 | } 339 | 340 | a.md-button.md-default-theme.md-warn.md-raised, a.md-button.md-warn.md-raised, a.md-button.md-default-theme.md-warn.md-fab, a.md-button.md-warn.md-fab, .md-button.md-default-theme.md-warn.md-raised, .md-button.md-warn.md-raised, .md-button.md-default-theme.md-warn.md-fab, .md-button.md-warn.md-fab { 341 | color: rgb(255, 255, 255); 342 | background-color: rgba(169, 169, 169, 0.1); 343 | } 344 | 345 | /* Custom topics-ui CSS */ 346 | md-list-item.md-2-line, md-list-item.md-2-line > .md-no-style { 347 | min-height: 0; 348 | } 349 | 350 | .table > tbody > tr > td, .table > tbody > tr > th, .table > tfoot > tr > td, .table > tfoot > tr > th, .table > thead > tr > td, .table > thead > tr > th { 351 | padding-top: 10px; 352 | padding-bottom: 10px; 353 | vertical-align: middle; 354 | } 355 | 356 | md-card md-card-title { 357 | padding-bottom: 10px; 358 | } 359 | 360 | /** Kafka-Connect only **/ 361 | .ats-switch span.switch-right { 362 | color: #000; 363 | background: #f59800; 364 | } 365 | 366 | .md-tooltip ._md-content { 367 | height: auto; 368 | } 369 | 370 | /*Animated cogs https://codepen.io/marclloyd77/pen/tAlmd*/ 371 | #development_icon { 372 | width: 100px; 373 | margin: 0; 374 | -webkit-animation: pop 0.4s ease-in; 375 | } 376 | 377 | /*Animate cogs*/ 378 | #large-cog, #small-cog { 379 | -webkit-animation: spin 4s linear infinite; 380 | -webkit-transform-origin: 50% 50%; 381 | -webkit-animation-delay: 0.6s; 382 | } 383 | 384 | #small-cog { 385 | -webkit-animation: spinback 2s linear infinite; 386 | -webkit-animation-delay: 0.6s; 387 | } 388 | 389 | @-webkit-keyframes pop { 390 | 0% { 391 | -webkit-transform: scale(0); 392 | } 393 | 90% { 394 | -webkit-transform: scale(1.1); 395 | } 396 | 100% { 397 | -webkit-transform: scale(1); 398 | } 399 | } 400 | 401 | @-webkit-keyframes spin { 402 | 100% { 403 | -webkit-transform: rotate(360deg); 404 | } 405 | } 406 | 407 | @-webkit-keyframes spinback { 408 | 100% { 409 | -webkit-transform: rotate(-360deg); 410 | } 411 | } 412 | 413 | .ace_diff_new_line { 414 | background: rgba(0, 128, 0, 0.04); 415 | } 416 | 417 | .noselect { 418 | -webkit-touch-callout: none; 419 | -webkit-user-select: none; 420 | -khtml-user-select: none; 421 | -moz-user-select: none; 422 | -ms-user-select: none; 423 | user-select: none; 424 | } 425 | 426 | /* diff css */ 427 | .match { 428 | display: none; 429 | color: blue; 430 | } 431 | 432 | .ins { 433 | background: rgba(55, 255, 55, 0.15); 434 | } 435 | 436 | .del { 437 | background: rgba(243, 59, 59, 0.15); 438 | } 439 | 440 | /* Essential for dirPagination material design */ 441 | .pagination .md-button { 442 | min-width: 30px; 443 | margin: 5px 8px; 444 | } 445 | 446 | /* Essential for dirPagination material design */ 447 | .md-button { 448 | min-width: 70px; 449 | margin: 5px 8px; 450 | color: #333; 451 | } 452 | 453 | .selectedListItem { 454 | background-color: #DDDDDD; 455 | } 456 | 457 | /*Funny color*/ 458 | .md-fab:hover, .md-fab.md-focused { 459 | background-color: rgba(230, 100, 0, .9) !important; 460 | } 461 | 462 | md-list-item.md-2-line, md-list-item.md-2-line > .md-no-style { 463 | height: 48px; 464 | } 465 | 466 | md-content { 467 | background-color: white; 468 | } 469 | 470 | .md-button.md-default-theme.md-primary.md-raised.newschemabutton { 471 | margin-right: 4px; 472 | background-color: #448AFF; 473 | color: white; 474 | } 475 | 476 | md-list-item.md-2-line.shemaslistitem { 477 | width: 100%; 478 | } 479 | 480 | .md-2-line.shemaslistitem a .md-button.md-warn.md-raised.divlistitem { 481 | min-width: 100%; 482 | text-align: left; 483 | box-shadow: 0 0 0 0 rgba(0, 0, 0, .26) !important; 484 | } 485 | 486 | .md-raised.md-warn.md-button.versionbox { 487 | box-shadow: 0 0 0 0 rgba(0, 0, 0, .46); 488 | min-width: initial; 489 | font-size: 75%; 490 | line-height: 17px; 491 | min-height: 18px; 492 | text-transform: none; 493 | background-color: rgba(44, 152, 240, 0.3); 494 | color: black; 495 | text-align: left; 496 | float: right; 497 | padding: 3px 12px; 498 | margin-top: 10px; 499 | } 500 | 501 | .md-raised.md-warn.md-button.versionbox.moreversions { 502 | background: rgb(139, 195, 74); 503 | } 504 | 505 | .md-raised.md-warn.md-button.md-ink-ripple.editbutton, 506 | .md-raised.md-warn.md-button.md-ink-ripple.testbutton { 507 | padding-left: 20px; 508 | padding-right: 20px; 509 | background-color: #448AFF; 510 | color: white; 511 | } 512 | 513 | .md-default-theme md-input-container .md-errors-spacer { 514 | min-height: 0; 515 | } 516 | 517 | span.title { 518 | margin: 0; 519 | padding: 0; 520 | line-height: 44px; 521 | font-size: 14px; 522 | } 523 | 524 | md-tab-content md-content { 525 | overflow: auto 526 | } 527 | 528 | .flex-2 { 529 | width: 50%; 530 | float: left; 531 | } 532 | 533 | md-switch.md-default-theme.md-checked .md-thumb, md-switch.md-checked .md-thumb { 534 | background-color: rgb(65, 191, 236); 535 | } 536 | 537 | md-switch.md-default-theme.md-checked .md-bar, md-switch.md-checked .md-bar { 538 | background-color: rgba(65, 191, 236, 0.6); 539 | } 540 | 541 | .seperator:last-child { 542 | border-top: 2px dashed #aaa; 543 | margin: 40px 0 30px; 544 | } 545 | 546 | 547 | 548 | /* The Modal (background) */ 549 | .modal { 550 | display: none; /* Hidden by default */ 551 | position: fixed; /* Stay in place */ 552 | z-index: 1; /* Sit on top */ 553 | left: 0; 554 | top: 0; 555 | width: 100%; /* Full width */ 556 | height: 100%; /* Full height */ 557 | overflow: auto; /* Enable scroll if needed */ 558 | background-color: rgb(0,0,0); /* Fallback color */ 559 | background-color: rgba(0,0,0,0.4); /* Black w/ opacity */ 560 | z-index: 999999; 561 | } 562 | 563 | /* Modal Content/Box */ 564 | .modal-content { 565 | background-color: #fefefe; 566 | margin: 5% auto; /* 15% from the top and centered */ 567 | padding: 20px; 568 | border: 1px solid #888; 569 | width: 1200px; /* Could be more or less, depending on screen size */ 570 | } 571 | 572 | /* The Close Button */ 573 | .close { 574 | color: #aaa; 575 | float: right; 576 | font-size: 28px; 577 | font-weight: bold; 578 | } 579 | 580 | .close:hover, 581 | .close:focus { 582 | color: black; 583 | text-decoration: none; 584 | cursor: pointer; 585 | } 586 | 587 | .md-button.btn-danger { 588 | background:#d05653; 589 | color:white; 590 | } 591 | 592 | .md-button.btn-danger:hover { 593 | background:#a7110d; 594 | 595 | } -------------------------------------------------------------------------------- /src/schema-registry/new/new.controller.js: -------------------------------------------------------------------------------- 1 | var angular = require('angular'); 2 | var angularAPP = angular.module('angularAPP'); 3 | 4 | var NewSubjectCtrl = function ($scope, $route, $rootScope, $http, $log, $q, $location, UtilsFactory, SchemaRegistryFactory, toastFactory, env) { 5 | $log.debug("NewSubjectCtrl - initiating"); 6 | 7 | $scope.$on('$routeChangeSuccess', function () { 8 | $scope.cluster = env.getSelectedCluster().NAME;//$routeParams.cluster; 9 | }); 10 | 11 | $scope.noSubjectName = true; 12 | $rootScope.listChanges = false; 13 | toastFactory.hideToast(); 14 | 15 | $scope.showSimpleToast = function (message) { 16 | toastFactory.showSimpleToast(message) 17 | }; 18 | $scope.showSimpleToastToTop = function (message) { 19 | toastFactory.showSimpleToastToTop(message); 20 | }; 21 | 22 | $scope.hideToast = function () { 23 | toastFactory.hide(); 24 | }; 25 | 26 | $scope.$watch(function () { 27 | return $scope.text; 28 | }, function (a) { 29 | $scope.allowCreateOrEvolution = false; 30 | updateCurl(); 31 | }, true); 32 | 33 | $scope.$watch(function () { 34 | return $scope.newAvroString; 35 | }, function (a) { 36 | $scope.allowCreateOrEvolution = false; 37 | updateCurl(); 38 | }, true); 39 | 40 | 41 | /** 42 | * Create filter function for a query string 43 | */ 44 | function createFilterFor(query) { 45 | var lowercaseQuery = angular.lowercase(query); 46 | return function filterFn(state) { 47 | return (state.value.indexOf(lowercaseQuery) === 0); 48 | }; 49 | } 50 | 51 | /** 52 | * Possibilities 53 | * 1. no-subject-name -> User has not filled-in the subjectName 54 | * 2. not-json -> Schema is invalid Json 55 | * 3. new-schema -> Schema is Json + subject does not exist 56 | */ 57 | $scope.allowCreateOrEvolution = false; 58 | var validTypes = ["null", "double", "string", "record", "int", "float", "long", "array", "boolean", "enum", "map", "fixed", "bytes", "type"]; 59 | var primitiveTypes = ["null", "boolean", "int", "long", "float", "double", "bytes", "string"]; 60 | 61 | function testCompatibility(subject, newAvroString) { 62 | if (env.readonlyMode()) { 63 | var deferred = $q.defer(); 64 | $scope.showSimpleToastToTop("Creation is not allowed in readonly mode"); 65 | deferred.resolve("readonly"); 66 | return deferred.promise; 67 | } 68 | 69 | $scope.notValidType = false; 70 | 71 | if (newAvroString === "null") { 72 | if (primitiveTypes.indexOf(newAvroString) === -1) { 73 | $scope.wrongType = newAvroString; 74 | $scope.notValidType = true; 75 | } 76 | } else { 77 | var a; 78 | try { 79 | a = JSON.parse(newAvroString); 80 | console.log("It's probably object, so checking types", a) 81 | } catch (e) { 82 | if (typeof(newAvroString) === "string") { 83 | if (primitiveTypes.indexOf(newAvroString) === -1) { 84 | $scope.wrongType = newAvroString; 85 | $scope.notValidType = true; 86 | } 87 | } 88 | 89 | } 90 | } 91 | 92 | var flattenObject = function (ob) { 93 | var toReturn = {}; 94 | 95 | for (var i in ob) { 96 | if (!ob.hasOwnProperty(i)) continue; 97 | 98 | if ((typeof ob[i]) === 'object') { 99 | var flatObject = flattenObject(ob[i]); 100 | for (var x in flatObject) { 101 | if (!flatObject.hasOwnProperty(x)) continue; 102 | toReturn[i + '.' + x] = flatObject[x]; 103 | } 104 | 105 | } else { 106 | toReturn[i] = ob[i]; 107 | } 108 | } 109 | return toReturn; 110 | }; 111 | 112 | var obj = flattenObject(newAvroString); 113 | var typeKeysToCheck = Object.keys(obj) 114 | .reduce(function (typeKeys, key, idx) { 115 | // Check that this string has a type substring, if not, we don't have to validate 116 | if (key.indexOf('type') !== -1 && isPrimitiveTypeKey(key)) { 117 | typeKeys.push(key); 118 | } 119 | 120 | return typeKeys; 121 | }, []); 122 | 123 | var typeKeysToCheckLength = typeKeysToCheck.length; 124 | 125 | // Create for loop vars 126 | var i; 127 | var keyToCheck; 128 | 129 | /* 130 | * By iterating in a for loop, we can break out of an invalid key type found immediately. 131 | * That way the UI shows each wrong type one by one(if there are many) instead of just the last 132 | * one. 133 | */ 134 | for (i = 0; i < typeKeysToCheckLength; i++) { 135 | keyToCheck = typeKeysToCheck[i]; 136 | 137 | if (validTypes.indexOf(obj[keyToCheck]) < 0) { 138 | $scope.wrongType = obj[keyToCheck]; 139 | $scope.notValidType = true; 140 | 141 | break; 142 | } 143 | } 144 | 145 | function isKeyType(key) { 146 | return key === 'type'; 147 | } 148 | 149 | function checkLastTwoKeyParts(lastKeyPart, nextToLastKeyPart) { 150 | var isLastKeyPartNotANumber = isNaN(lastKeyPart); 151 | 152 | // If it is not a number, then make sure it's a type key 153 | if (isLastKeyPartNotANumber) { 154 | return isKeyType(lastKeyPart); 155 | } 156 | 157 | // If the last part was a number, is the next to last a type part? 158 | return isKeyType(nextToLastKeyPart); 159 | }; 160 | 161 | // Check if they key is actually a key that defines the primitive type 162 | function isPrimitiveTypeKey(key) { 163 | var keyToArray = key.split('.'); 164 | var keyToArrayLength = keyToArray.length; 165 | var lastKeyPart = keyToArray[keyToArrayLength - 1]; 166 | var nextToLastKeyPart = keyToArray[keyToArrayLength - 2]; 167 | 168 | return keyToArrayLength === 1 || checkLastTwoKeyParts(lastKeyPart, nextToLastKeyPart); 169 | } 170 | 171 | newAvroString = JSON.stringify(newAvroString); 172 | 173 | var deferred = $q.defer(); 174 | 175 | if ((subject === undefined) || subject.length === 0) { 176 | $scope.showSimpleToastToTop("Please fill in the subject name"); // (1.) 177 | $scope.aceBackgroundColor = "rgba(0, 128, 0, 0.04)"; 178 | deferred.resolve("no-subject-name"); 179 | } else { 180 | if ($scope.notValidType) { 181 | $scope.showSimpleToastToTop($scope.wrongType + " is not valid"); // (2.) 182 | $scope.aceBackgroundColor = "rgba(255, 255, 0, 0.10)"; 183 | deferred.resolve("not-valid-type") 184 | } else if (!UtilsFactory.IsJsonString(newAvroString)) { 185 | $scope.showSimpleToastToTop("This schema is not valid"); // (2.) 186 | $scope.aceBackgroundColor = "rgba(255, 255, 0, 0.10)"; 187 | deferred.resolve("not-json") 188 | } else { 189 | var latestKnownSubject = SchemaRegistryFactory.getLatestSubjectFromCache(subject); 190 | if (latestKnownSubject === undefined) { 191 | // (3.) 192 | $scope.createOrEvolve = "Create new schema"; 193 | $scope.showSimpleToast("This will be a new Subject"); 194 | $scope.allowCreateOrEvolution = true; 195 | $scope.aceBackgroundColor = "rgba(0, 128, 0, 0.04)"; 196 | $log.info('Valid schema'); 197 | deferred.resolve("new-schema") 198 | } else { 199 | SchemaRegistryFactory.testSchemaCompatibility($scope.text, $scope.newAvroString).then( 200 | function success(data) { 201 | $log.info("Success in testing schema compatibility " + data); 202 | // (4.) 203 | $scope.allowCreateOrEvolution = false; 204 | $scope.showSimpleToastToTop("Schema exists, please select a unique subject name"); 205 | $scope.aceBackgroundColor = "rgba(255, 255, 0, 0.10)"; 206 | deferred.resolve("non-compatible") 207 | }, 208 | function failure(data) { 209 | $scope.showSimpleToastToTop("Failure with - " + data); 210 | deferred.resolve("failure"); 211 | } 212 | ); 213 | } 214 | } 215 | } 216 | 217 | return deferred.promise; 218 | } 219 | 220 | /** 221 | * Update curl to reflect selected subject + schema 222 | */ 223 | function updateCurl() { 224 | //$log.debug("Updating curl commands accordingly"); 225 | var remoteSubject = "FILL_IN_SUBJECT"; 226 | if (($scope.text !== undefined) && $scope.text.length > 0) { 227 | remoteSubject = $scope.text; 228 | } 229 | if (JSON.stringify($scope.newAvroString)) { 230 | var curlPrefix = 'curl -vs --stderr - -XPOST -i -H "Content-Type: application/vnd.schemaregistry.v1+json" --data '; 231 | $scope.curlCommand = 232 | "\n" + 233 | "// Register new schema\n" + curlPrefix + 234 | "'" + '{"schema":"' + JSON.stringify($scope.newAvroString).replace(/\n/g, " ").replace(/\s\s+/g, ' ').replace(/"/g, "\\\"") + 235 | '"}' + "' " + env.SCHEMA_REGISTRY() + "/subjects/" + remoteSubject + "/versions"; 236 | } 237 | } 238 | 239 | /** 240 | * Private method to register-new-schema 241 | */ 242 | function registerNewSchemaPrivate(newSubject, newAvro) { 243 | 244 | var deferred = $q.defer(); 245 | SchemaRegistryFactory.registerNewSchema(newSubject, newAvro).then( 246 | function success(id) { 247 | $log.info("Success in registering new schema " + id); 248 | var schemaId = id; 249 | $scope.showSimpleToastToTop("Schema ID : " + id); 250 | $rootScope.listChanges = true; // trigger a cache re-load 251 | $location.path('/cluster/' + $scope.cluster + '/schema/' + newSubject + '/version/latest'); 252 | deferred.resolve(schemaId); 253 | }, 254 | function error(data, status) { 255 | $log.info("Error on schema registration : " + JSON.stringify(data)); 256 | var errorMessage = data.message; 257 | $scope.showSimpleToastToTop(errorMessage); 258 | if (status >= 400) { 259 | $log.debug("Schema registrations is not allowed " + status + " " + data); 260 | } else { 261 | $log.debug("Schema registration failure: " + JSON.stringify(data)); 262 | } 263 | deferred.reject(errorMessage); 264 | }); 265 | 266 | return deferred.promise; 267 | 268 | } 269 | 270 | $scope.testCompatibility = function () { 271 | return testCompatibility($scope.text, $scope.newAvroString); 272 | }; 273 | 274 | /** 275 | * How to responde to register new schema clicks 276 | */ 277 | $scope.registerNewSchema = function () { 278 | var subject = $scope.text; 279 | testCompatibility(subject, $scope.newAvroString).then( 280 | function success(response) { 281 | // no-subject-name | not-json | new-schema | compatible | non-compatible | failure | readonly 282 | switch (response) { 283 | case "no-subject-name": 284 | case "not-json": 285 | case "not-valid-type": 286 | case "failure": 287 | case "non-compatible": 288 | case "readonly": 289 | $log.debug("registerNewSchema - cannot do anything more with [ " + response + " ]"); 290 | break; 291 | case 'new-schema': 292 | var schemaString = ''; 293 | if (typeof $scope.newAvroString !== 'string') 294 | schemaString = JSON.stringify($scope.newAvroString); 295 | else 296 | schemaString = $scope.newAvroString; 297 | registerNewSchemaPrivate(subject, schemaString).then( 298 | function success(newSchemaId) { 299 | $log.info("New subject id after posting => " + newSchemaId); 300 | }, 301 | function failure(data) { 302 | $log.error("peiler2=>" + data); 303 | $scope.allowCreateOrEvolution = false; 304 | $scope.aceBackgroundColor = "rgba(255, 255, 0, 0.10)"; 305 | }); 306 | break; 307 | case 'compatible': 308 | $log.info("Compatibility [compatible]"); 309 | // TODO 310 | var latestKnownSubject = SchemaRegistryFactory.getLatestSubjectFromCache(subject); 311 | if (latestKnownSubject === undefined) { 312 | $log.error("This should never happen.") 313 | } else { 314 | $log.info("Existing schema id = " + latestKnownSubject.version); 315 | registerNewSchemaPrivate(subject, $scope.newAvroString).then( 316 | function success(newSchemaId) { 317 | $log.info("New subject id after posting => " + newSchemaId); 318 | if (latestKnownSubject.version === newSchemaId) { 319 | toastFactory.showSimpleToastToTop("The schema you posted was same to the existing one") 320 | } 321 | }, 322 | function failure(data) { 323 | $log.error("peiler=>" + data); 324 | $scope.allowCreateOrEvolution = false; 325 | $scope.aceBackgroundColor = "rgba(255, 255, 0, 0.10)"; 326 | }); 327 | break; 328 | } 329 | default: 330 | $log.warn("Should never come here " + response); 331 | } 332 | }, 333 | function failure(data) { 334 | if (data.error_code === 500) { 335 | $scope.aceBackgroundColor = "rgba(255, 255, 0, 0.10)"; 336 | toastFactory.showSimpleToastToTop("Not a valid avro"); 337 | } 338 | else { 339 | $log.error("Could not test compatibilitydasdas", data); 340 | } 341 | }); 342 | 343 | }; 344 | 345 | // $scope.createOrEvolve = "Create new schema"; 346 | // $scope.allowCreateOrEvolution = true; 347 | // $scope.aceBackgroundColor = "rgba(0, 128, 0, 0.04)"; 348 | 349 | 350 | // $http(postSchemaRegistration) 351 | // $http.get(env.SCHEMA_REGISTRY() + '/subjects/' + $scope.text + '/versions/latest') 352 | // .success(function (data) { 353 | // $log.info("Schema succesfully registered: " + JSON.stringify(data)); 354 | // $location.path('/subjects/' + data.subject + '/version/' + data.version); 355 | // }); 356 | // } 357 | 358 | // When the 'Ace' of the schema/new is loaded 359 | $scope.newSchemaAceLoaded = function (_editor) { 360 | $scope.editor = _editor; 361 | $scope.editor.$blockScrolling = Infinity; 362 | $scope.aceSchemaSession = _editor.getSession(); // we can get data on changes now 363 | var lines = $scope.newAvroString.split("\n").length; 364 | // TODO : getScalaFiles($scope.aceString); 365 | // Add one extra line for each command > 110 characters 366 | angular.forEach($scope.newAvroString.split("\n"), function (line) { 367 | lines = lines + Math.floor(line.length / 110); 368 | }); 369 | if (lines <= 1) { 370 | lines = 10; 371 | } 372 | _editor.setOptions({ 373 | minLines: lines + 1, 374 | maxLines: lines + 1, 375 | highlightActiveLine: false 376 | }); 377 | updateCurl(); 378 | }; 379 | 380 | // When the 'Ace' of the schema/new is CHANGED (!) 381 | $scope.newSchemaAceChanged = function (_editor) { 382 | $scope.editor = _editor; 383 | updateCurl(); 384 | 385 | }; 386 | 387 | // When the 'Ace' of the curl command is loaded 388 | $scope.curlCommandAceLoaded = function (_editor) { 389 | $scope.editor = _editor; 390 | $scope.editor.$blockScrolling = Infinity; 391 | }; 392 | 393 | 394 | $scope.newAvroString = 395 | angular.toJson( 396 | { 397 | "type": "record", 398 | "name": "evolution", 399 | "doc": "This is a sample Avro schema to get you started. Please edit", 400 | "namespace": "com.landoop", 401 | "fields": [{"name": "name", "type": "string"}, {"name": "number1", "type": "int"}, { 402 | "name": "number2", 403 | "type": "float" 404 | }] 405 | }, true); 406 | 407 | }; 408 | 409 | NewSubjectCtrl.$inject = ['$scope', '$route', '$rootScope', '$http', '$log', '$q', '$location', 'UtilsFactory', 'SchemaRegistryFactory', 'toastFactory', 'env'] 410 | 411 | angularAPP.controller('NewSubjectCtrl', NewSubjectCtrl); 412 | 413 | -------------------------------------------------------------------------------- /src/schema-registry/view/view.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 |
8 |

9 |
10 | 12 | 13 | 14 | 15 | 16 |
17 | 18 |
{{subjectObject.subjectName}}
19 |
20 | 21 |

23 | SCHEMA ID: {{subjectObject.id}} 24 |

25 |
26 |
27 |
28 |
29 |

30 | 31 |

32 |
33 |
{{subjectObject.subjectName}}
34 |
35 | 36 |

38 | SCHEMA ID: {{subjectObject.id}} 39 |

40 |
41 |
42 |
43 |

44 | 45 |
46 | 47 |
48 | 52 | version {{subjectObject.version}} 53 | 54 | 55 | 56 | 57 | version {{subjectObject.version}} 58 | 59 | 60 | 61 | 62 | 63 | version {{version}} 64 | 65 | 66 | 67 | 68 |
69 |
70 | 71 |
72 | 73 | 74 | 77 | 78 | EDIT 79 | 80 | 81 | 84 | 85 | CANCEL 86 | 87 | 88 | 89 | 90 | DELETE 91 | 92 | 93 | 94 | 95 | Delete Latest Version 96 | 97 | 98 | 99 | 100 | Delete Selected Version ({{subjectObject.version}}) 101 | 102 | 103 | 104 | 105 | Delete Subject 106 | 107 | 108 | 109 | 110 |
111 |
112 | 113 |
114 | Schema {{subjectObject.subjectName}} version: {{versionToBeDeleted}} will be deleted. 115 |
116 | 117 | 118 | DELETE 119 | 120 | 121 | 122 | CANCEL 123 | 124 |
125 | 126 |
127 | 128 | 129 | 130 | Schema 131 | 132 | 133 | 134 | 135 | 136 |
137 |
138 |
- Syntax Error
139 | 140 |
149 |
150 |
151 |
152 | 153 | 159 | VALIDATE 160 | 161 | 162 | 167 | 168 | EVOLVE SCHEMA 169 | 170 |
171 |
172 | 173 | 174 | 175 | Info 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 186 | 187 | 188 | type: {{subjectObject.Schema.type}} 189 | name: {{subjectObject.Schema.name}} 190 | namespace: {{subjectObject.Schema.namespace}} 191 | 192 | 193 | 194 |

{{subjectObject.Schema.doc}}

195 |
196 |
197 | 198 | 199 | 200 | 201 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 218 | 219 | 220 | 221 | 222 |
NameTypeDefaultDocumentation
{{s.name}} 215 | 216 |
{{s.type}}
217 |
{{s.default}}{{s.doc}}
223 |
224 |
225 | 226 | 227 | 228 | 230 | 231 | 232 | type: {{schemas.type}} 233 | name: {{schemas.name}} 234 | namespace: {{schemas.namespace}} 235 | 236 | 237 | 238 |

{{subjectObject.Schema.doc}}

239 |
240 |
241 | 242 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 260 | 261 | 262 | 263 | 264 |
NameTypeDefaultDocumentation
{{s.name}} 257 | 258 |
{{s.type}}
259 |
{{s.default}}{{s.doc}}
265 |
266 |
267 |
268 | 269 |
270 |
271 |
272 | 273 | 274 | Config 275 | 276 | 277 | 278 | 279 | 280 |
281 |

Current compatibility for {{subjectObject.subjectName}} : {{ existingValue }}
282 | Schema {{subjectObject.subjectName}} uses the global compatibility level [{{globalConfig}}]
283 | Change compatibility level to: 284 |

285 | 286 | NONE 287 | FULL 288 | FORWARD 289 | BACKWARD 290 | FULL TRANSITIVE 291 | FORWARD TRANSITIVE 292 | BACKWARD TRANSITIVE 293 | 294 | Update 295 |
296 |
297 | Successfully changed compatibility level to {{compatibilitySelect}} 298 |
299 |
300 |
301 |
302 | 303 | 304 | History 305 | 306 | 307 | 308 |
309 | 310 |
Version {{x.version}} (Schema ID: {{x.id}})
311 |

314 |             
315 |
Version 1 (Schema ID: {{completeSubjectHistory[0].id}})
317 |

322 |           
323 |
324 |
325 |
326 |
327 | 328 |
329 | -------------------------------------------------------------------------------- /src/factories/schema-registry-factory.js: -------------------------------------------------------------------------------- 1 | var angular = require('angular'); 2 | var angularAPP = angular.module('angularAPP'); 3 | 4 | /** 5 | * Schema-Registry angularJS Factory 6 | * 7 | * Landoop - version 0.9.x (May.2017) 8 | */ 9 | var SchemaRegistryFactory = function ($rootScope, $http, $location, $q, $log, UtilsFactory, env) { 10 | 11 | /** 12 | * Get subjects 13 | * @see http://docs.confluent.io/3.0.0/schema-registry/docs/api.html#get--subjects 14 | */ 15 | function getSubjects() { 16 | 17 | var url = env.SCHEMA_REGISTRY() + '/subjects/'; 18 | $log.debug(" curl -X GET " + url); 19 | var start = new Date().getTime(); 20 | 21 | var deferred = $q.defer(); 22 | $http.get(url) 23 | .then( 24 | function successCallback(response) { 25 | var allSubjectNames = response.data; 26 | $log.debug(" curl -X GET " + url + " => " + allSubjectNames.length + " registered subjects in [ " + ((new Date().getTime()) - start) + " ] msec"); 27 | deferred.resolve(allSubjectNames); 28 | }, 29 | function errorCallback(response) { 30 | deferred.reject("Failure with : " + response) 31 | }); 32 | 33 | return deferred.promise; 34 | } 35 | 36 | /** 37 | * Get subjects versions 38 | * @see http://docs.confluent.io/3.0.0/schema-registry/docs/api.html#get--subjects-(string- subject)-versions 39 | */ 40 | function getSubjectsVersions(subjectName) { 41 | 42 | var url = env.SCHEMA_REGISTRY() + '/subjects/' + subjectName + '/versions/'; 43 | $log.debug(" curl -X GET " + url); 44 | var start = new Date().getTime(); 45 | 46 | var deferred = $q.defer(); 47 | $http.get(url).then( 48 | function successCallback(response) { 49 | var allVersions = response.data; 50 | $log.debug(" curl -X GET " + url + " => " + JSON.stringify(allVersions) + " versions in [ " + (new Date().getTime() - start) + " ] msec"); 51 | deferred.resolve(allVersions); 52 | }, 53 | function errorCallback(response) { 54 | var msg = "Failure with : " + response + " " + JSON.stringify(response); 55 | $log.error("Error in getting subject versions : " + msg); 56 | deferred.reject(msg); 57 | }); 58 | 59 | return deferred.promise; 60 | 61 | } 62 | 63 | /** 64 | * Get a specific version of the schema registered under this subject 65 | * @see http://docs.confluent.io/3.0.0/schema-registry/docs/api.html#get--subjects-(string- subject)-versions-(versionId- version) 66 | */ 67 | function getSubjectAtVersion(subjectName, version) { 68 | 69 | var url = env.SCHEMA_REGISTRY() + '/subjects/' + subjectName + '/versions/' + version; 70 | $log.debug(" curl -X GET " + url); 71 | 72 | var deferred = $q.defer(); 73 | var start = new Date().getTime(); 74 | $http.get(url).then( 75 | function successCallback(response) { 76 | var subjectInformation = response.data; 77 | $log.debug(" curl -X GET " + url + " => [" + subjectName + "] subject " + JSON.stringify(subjectInformation).length + " bytes in [ " + (new Date().getTime() - start) + " ] msec"); 78 | deferred.resolve(subjectInformation); 79 | }, 80 | function errorCallback(response) { 81 | var msg = "Failure getting subject at version : " + response + " " + JSON.stringify(response); 82 | $log.error(msg); 83 | deferred.reject(msg); 84 | }); 85 | 86 | return deferred.promise; 87 | 88 | } 89 | 90 | function getAllSchemas(cache) { 91 | var i; 92 | var allSchemasCache = []; 93 | angular.forEach(cache, function (schema) { 94 | for (i = 1; i <= schema.version; i++) { 95 | getSubjectAtVersion(schema.subjectName, i).then(function (selectedSubject) { 96 | allSchemasCache.push(selectedSubject) 97 | //$rootScope.downloadFile += '\n echo >>>' + selectedSubject.subject +'.'+ selectedSubject.version + '.json <<< \n' + schema.schema + ' \n \n EOF'; 98 | }) 99 | } 100 | }); 101 | $rootScope.allSchemasCache = allSchemasCache; 102 | return allSchemasCache 103 | } 104 | 105 | /** 106 | * Register a new schema under the specified subject. If successfully registered, this returns the unique identifier of this schema in the registry. 107 | * @see http://docs.confluent.io/3.0.0/schema-registry/docs/api.html#post--subjects-(string- subject)-versions 108 | */ 109 | function postNewSubjectVersion(subjectName, newSchema) { 110 | 111 | var deferred = $q.defer(); 112 | $log.debug("Posting new version of subject [" + subjectName + "]"); 113 | 114 | var postSchemaRegistration = { 115 | method: 'POST', 116 | url: env.SCHEMA_REGISTRY() + '/subjects/' + subjectName + "/versions", 117 | data: '{"schema":"' + newSchema.replace(/\n/g, " ").replace(/\s\s+/g, ' ').replace(/"/g, "\\\"") + '"}' + "'", 118 | dataType: 'json', 119 | headers: {'Content-Type': 'application/json', 'Accept': 'application/json'} 120 | }; 121 | 122 | $http(postSchemaRegistration) 123 | .success(function (data) { 124 | //$log.info("Success in registering new schema " + JSON.stringify(data)); 125 | var schemaId = data.id; 126 | deferred.resolve(schemaId); 127 | }) 128 | .error(function (data, status) { 129 | $log.info("Error on schema registration : " + JSON.stringify(data)); 130 | var errorMessage = data.message; 131 | if (status >= 400) { 132 | $log.debug("Schema registrations is not allowed " + status + " " + data); 133 | } else { 134 | $log.debug("Schema registration failure: " + JSON.stringify(data)); 135 | } 136 | deferred.reject(data); 137 | }); 138 | 139 | return deferred.promise; 140 | 141 | } 142 | 143 | /** 144 | * Check if a schema has already been registered under the specified subject. If so, this returns the schema string 145 | * along with its globally unique identifier, its version under this subject and the subject name. 146 | * 147 | * @see http://docs.confluent.io/3.0.0/schema-registry/docs/api.html#post--subjects-(string- subject) 148 | */ 149 | function checkSchemaExists(subjectName, subjectInformation) { 150 | 151 | var deferred = $q.defer(); 152 | $log.debug("Checking if schema exists under this subject [" + subjectName + "]"); 153 | 154 | var postSchemaExists = { 155 | method: 'POST', 156 | url: env.SCHEMA_REGISTRY() + '/subjects/' + subjectName, 157 | data: '{"schema":"' + subjectInformation.replace(/\n/g, " ").replace(/\s\s+/g, ' ').replace(/"/g, "\\\"") + '"}' + "'", 158 | dataType: 'json', 159 | headers: {'Content-Type': 'application/json', 'Accept': 'application/json'} 160 | }; 161 | 162 | $http(postSchemaExists) 163 | .success(function (data) { 164 | var response = { 165 | id: data.id, 166 | version: data.version 167 | }; 168 | $log.info("Response : " + JSON.stringify(response)); 169 | deferred.resolve(response); 170 | }) 171 | .error(function (data, status) { 172 | $log.info("Error while checking if schema exists under a subject : " + JSON.stringify(data)); 173 | var errorMessage = data.message; 174 | if (status === 407) { 175 | $log.debug("Subject not found or schema not found - 407 - " + status + " " + data); 176 | } else { 177 | $log.debug("Some other failure: " + JSON.stringify(data)); 178 | } 179 | $defered.reject("Something") 180 | }); 181 | 182 | return deferred.promise; 183 | 184 | } 185 | 186 | /** 187 | * Test input schema against a particular version of a subject’s schema for compatibility. 188 | * @see http://docs.confluent.io/3.0.0/schema-registry/docs/api.html#post--compatibility-subjects-(string- subject)-versions-(versionId- version) 189 | */ 190 | function testSchemaCompatibility(subjectName, subjectInformation) { 191 | 192 | var deferred = $q.defer(); 193 | $log.debug(" Testing schema compatibility for [" + subjectName + "]"); 194 | 195 | var postCompatibility = { 196 | method: 'POST', 197 | url: env.SCHEMA_REGISTRY() + '/compatibility/subjects/' + subjectName + "/versions/latest", 198 | data: '{"schema":"' + subjectInformation.replace(/\n/g, " ").replace(/\s\s+/g, ' ').replace(/"/g, "\\\"") + '"}' + "'", 199 | dataType: 'json', 200 | headers: {'Content-Type': 'application/json', 'Accept': 'application/json'} 201 | }; 202 | 203 | $http(postCompatibility) 204 | .success(function (data) { 205 | $log.info("Success in testing schema compatibility " + JSON.stringify(data)); 206 | deferred.resolve(data.is_compatible) 207 | }) 208 | .error(function (data, status) { 209 | $log.warn("Error on check compatibility : " + JSON.stringify(data)); 210 | if (status === 404) { 211 | if (data.error_code === 40401) { 212 | $log.warn("40401 = Subject not found"); 213 | } 214 | $log.warn("[" + subjectName + "] is a non existing subject"); 215 | deferred.resolve("new"); // This will be a new subject (!) 216 | } else { 217 | $log.error("HTTP > 200 && < 400 (!) " + JSON.stringify(data)); 218 | } 219 | deferred.reject(data); 220 | }); 221 | 222 | return deferred.promise; 223 | 224 | } 225 | 226 | /** 227 | * Put global config (Test input schema against a particular version of a subject’s schema for compatibility. 228 | * @see http://docs.confluent.io/3.0.0/schema-registry/docs/api.html#put--config 229 | */ 230 | function putConfig(compatibilityLevel) { 231 | 232 | var deferred = $q.defer(); 233 | 234 | if (["NONE", "FULL", "FORWARD", "BACKWARD", "FULL_TRANSITIVE", "FORWARD_TRANSITIVE", "BACKWARD_TRANSITIVE"].indexOf(compatibilityLevel) !== -1) { 235 | 236 | var putConfig = { 237 | method: 'PUT', 238 | url: env.SCHEMA_REGISTRY() + '/config', 239 | data: '{"compatibility":"' + compatibilityLevel + '"}' + "'", 240 | dataType: 'json', 241 | headers: {'Content-Type': 'application/json', 'Accept': 'application/json'} 242 | }; 243 | 244 | $http(putConfig) 245 | .success(function (data) { 246 | $log.info("Success in changing global schema-registry compatibility " + JSON.stringify(data)); 247 | deferred.resolve(data.compatibility) 248 | }) 249 | .error(function (data, status) { 250 | $log.info("Error on changing global compatibility : " + JSON.stringify(data)); 251 | if (status === 422) { 252 | $log.warn("Invalid compatibility level " + JSON.stringify(status) + " " + JSON.stringify(data)); 253 | if (JSON.stringify(data).indexOf('50001') > -1) { 254 | $log.error(" Error in the backend data store - " + $scope.text); 255 | } else if (JSON.stringify(data).indexOf('50003') > -1) { 256 | $log.error("Error while forwarding the request to the master: " + JSON.stringify(data)); 257 | } 258 | } else { 259 | $log.debug("HTTP > 200 && < 400 (!) " + JSON.stringify(data)); 260 | } 261 | deferred.reject(data); 262 | }); 263 | 264 | } else { 265 | $log.warn("Compatibility level:" + compatibilityLevel + " is not supported"); 266 | deferred.reject(); 267 | } 268 | 269 | return deferred.promise; 270 | 271 | } 272 | 273 | /** 274 | * Get global compatibility-level config 275 | * @see http://docs.confluent.io/3.0.0/schema-registry/docs/api.html#get--config 276 | */ 277 | function getGlobalConfig() { 278 | 279 | var deferred = $q.defer(); 280 | var url = env.SCHEMA_REGISTRY() + '/config'; 281 | $log.debug(" curl -X GET " + url); 282 | var start = new Date().getTime(); 283 | $http.get(url) 284 | .success(function (data) { 285 | $log.debug(" curl -X GET " + url + " => in [ " + ((new Date().getTime()) - start) + "] msec"); 286 | deferred.resolve(data) 287 | }) 288 | .error(function (data, status) { 289 | deferred.reject("Get global config rejection : " + data + " " + status) 290 | }); 291 | 292 | return deferred.promise; 293 | 294 | } 295 | 296 | function getSubjectConfig(subjectName) { 297 | var deferred = $q.defer(); 298 | var url = env.SCHEMA_REGISTRY() + '/config/' + subjectName; 299 | $log.debug(" curl -X GET " + url); 300 | var start = new Date().getTime(); 301 | $http.get(url) 302 | .success(function (data) { 303 | $log.debug(" curl -X GET " + url + " => in [ " + ((new Date().getTime()) - start) + "] msec"); 304 | deferred.resolve(data) 305 | }) 306 | .error(function (data, status) { 307 | if (status === 404) { 308 | $log.warn('No compatibility level is set for ' + subjectName + '. Global compatibility level is applied'); 309 | } else 310 | deferred.reject("Get global config rejection : " + data + " " + status) 311 | }); 312 | return deferred.promise; 313 | 314 | } 315 | 316 | /** 317 | * Update compatibility level for the specified subject 318 | * @see http://docs.confluent.io/3.0.0/schema-registry/docs/api.html#put--config-(string- subject) 319 | */ 320 | function updateSubjectCompatibility(subjectName, newCompatibilityLevel) { 321 | 322 | var deferred = $q.defer(); 323 | 324 | if (["NONE", "FULL", "FORWARD", "BACKWARD", "FULL_TRANSITIVE", "FORWARD_TRANSITIVE", "BACKWARD_TRANSITIVE"].indexOf(newCompatibilityLevel) !== -1) { 325 | 326 | var putConfig = { 327 | method: 'PUT', 328 | url: env.SCHEMA_REGISTRY() + '/config/' + subjectName, 329 | data: '{"compatibility":"' + newCompatibilityLevel + '"}' + "'", 330 | dataType: 'json', 331 | headers: {'Content-Type': 'application/json', 'Accept': 'application/json'} 332 | }; 333 | 334 | $http(putConfig) 335 | .success(function (data) { 336 | $log.info("Success in changing subject [ " + subjectName + " ] compatibility " + JSON.stringify(data)); 337 | deferred.resolve(data.compatibility) 338 | }) 339 | .error(function (data, status) { 340 | $log.info("Error on changing compatibility : " + JSON.stringify(data)); 341 | if (status === 422) { 342 | $log.warn("Invalid compatibility level " + JSON.stringify(status) + " " + JSON.stringify(data)); 343 | if (JSON.stringify(data).indexOf('50001') > -1) { 344 | $log.error(" Error in the backend data store - " + $scope.text); 345 | } else if (JSON.stringify(data).indexOf('50003') > -1) { 346 | $log.error("Error while forwarding the request to the master: " + JSON.stringify(data)); 347 | } 348 | } else { 349 | $log.debug("HTTP > 200 && < 400 (!) " + JSON.stringify(data)); 350 | } 351 | deferred.reject(data); 352 | }); 353 | 354 | } else { 355 | $log.warn("Compatibility level:" + newCompatibilityLevel + " is not supported"); 356 | deferred.reject(); 357 | } 358 | 359 | return deferred.promise; 360 | 361 | } 362 | 363 | 364 | /** 365 | * Custom logic of Factory is implemented here. 366 | * 367 | * In a nut-shell `CACHE` is holding a cache of known subjects 368 | * Methods here are utilizing the cache - picking from it or updating 369 | * 370 | * Subjects are immutable in the schema-registry, thus downloading them 371 | * just once is enough ! 372 | */ 373 | 374 | var CACHE = []; // A cache of the latest subject 375 | 376 | /** 377 | * Gets from CACHE if exists - undefined otherwise 378 | */ 379 | function getFromCache(subjectName, subjectVersion) { 380 | var start = new Date().getTime(); 381 | var response = undefined; 382 | angular.forEach(CACHE, function (subject) { 383 | if (subject.subjectName === subjectName && subject.version === subjectVersion) { 384 | $log.debug(" [ " + subjectName + "/" + subjectVersion + " ] found in cache " + JSON.stringify(subject).length + " bytes in [ " + ((new Date().getTime()) - start) + " ] msec"); 385 | response = subject; 386 | } 387 | }); 388 | return response; 389 | } 390 | 391 | /** 392 | * GETs latest from CACHE or 'undefined' 393 | */ 394 | function getLatestFromCache(subjectName) { 395 | var subjectFromCache = undefined; 396 | for (var i = 1; i < 10000; i++) { 397 | var x = getFromCache(subjectName, i); 398 | if (x !== undefined) 399 | subjectFromCache = x; 400 | } 401 | return subjectFromCache; 402 | } 403 | 404 | 405 | /** 406 | * 407 | * Composite & Public Methods of this factory 408 | * 409 | */ 410 | return { 411 | 412 | // Proxy in function 413 | getGlobalConfig: function () { 414 | return getGlobalConfig(); 415 | }, 416 | 417 | getSubjectConfig: function (subjectName) { 418 | return getSubjectConfig(subjectName); 419 | }, 420 | 421 | putConfig: function (config) { 422 | return putConfig(config); 423 | }, 424 | updateSubjectCompatibility: function (subjectName, newCompatibilityLevel) { 425 | return updateSubjectCompatibility(subjectName, newCompatibilityLevel); 426 | }, 427 | 428 | // Proxy in function 429 | testSchemaCompatibility: function (subjectName, subjectInformation) { 430 | return testSchemaCompatibility(subjectName, subjectInformation); 431 | }, 432 | 433 | // Proxy in function 434 | registerNewSchema: function (subjectName, subjectInformation) { 435 | return postNewSubjectVersion(subjectName, subjectInformation); 436 | }, 437 | 438 | // Proxy in function 439 | getSubjectsVersions: function (subjectName) { 440 | return getSubjectsVersions(subjectName); 441 | }, 442 | 443 | // Proxy in function 444 | getLatestSubjectFromCache: function (subjectName) { 445 | return getLatestFromCache(subjectName); 446 | }, 447 | // Proxy in function 448 | getAllSchemas: function (schemas) { 449 | return getAllSchemas(schemas); 450 | }, 451 | 452 | deleteVersionOfSubject: function (subjectName, version) { 453 | var deferred = $q.defer(); 454 | 455 | var request = { 456 | method: 'DELETE', 457 | url: env.SCHEMA_REGISTRY() + '/subjects/' + subjectName + '/versions/' + version, 458 | dataType: 'json', 459 | headers: {'Content-Type': 'application/json', 'Accept': 'application/json, text/plain'} 460 | }; 461 | $http(request) 462 | .success(function (data) { 463 | $log.info("Success in deleting subject version" + subjectName + ", version" + version); 464 | var schemaId = data.id; 465 | deferred.resolve(schemaId); 466 | }) 467 | .error(function (data, status) { 468 | $log.info("Error on subject version deletion : ", data); 469 | deferred.reject(data); 470 | }); 471 | return deferred.promise; 472 | }, 473 | 474 | deleteSubject: function (subjectName) { 475 | 476 | var deferred = $q.defer(); 477 | 478 | var request = { 479 | method: 'DELETE', 480 | url: env.SCHEMA_REGISTRY() + '/subjects/' + subjectName, 481 | dataType: 'json', 482 | headers: {'Content-Type': 'application/json', 'Accept': 'application/json, text/plain'} 483 | }; 484 | $http(request) 485 | .success(function (data) { 486 | $log.info("Success in deleting schema" + subjectName); 487 | deferred.resolve(); 488 | }) 489 | .error(function (data, status) { 490 | $log.info("Error on schema deletion : ", data); 491 | deferred.reject(); 492 | }); 493 | return deferred.promise; 494 | }, 495 | 496 | /** 497 | * GETs all subject-names and then GETs the /versions/latest of each one 498 | * 499 | * Refreshes the CACHE object with latest subjects 500 | */ 501 | refreshLatestSubjectsCACHE: function () { 502 | 503 | var deferred = $q.defer(); 504 | var start = new Date().getTime(); 505 | 506 | // 1. Get all subject names 507 | getSubjects().then( 508 | function success(allSubjectNames) { 509 | // 2. Get full details of subject's final versions 510 | var urlFetchLatestCalls = []; 511 | angular.forEach(allSubjectNames, function (subject) { 512 | urlFetchLatestCalls.push($http.get(env.SCHEMA_REGISTRY() + '/subjects/' + subject + '/versions/latest')); 513 | }); 514 | $q.all(urlFetchLatestCalls).then(function (latestSchemas) { 515 | CACHE = []; // Clean up existing cache - to replace with new one 516 | angular.forEach(latestSchemas, function (result) { 517 | var data = result.data; 518 | var cacheData = { 519 | version: data.version, // version 520 | id: data.id, // id 521 | schema: data.schema, // schema - in String - schema i.e. {\"type\":\"record\",\"name\":\"User\",\"fields\":[{\"name\":\"name\",\"type\":\"string\"}]} 522 | Schema: JSON.parse(data.schema), // js type | name | doc | fields ... 523 | subjectName: data.subject 524 | }; 525 | CACHE.push(cacheData); 526 | }); 527 | $log.debug(" pipeline : get-latest-subjects-refresh-cache in [ " + (new Date().getTime() - start) + " ] msec"); 528 | $rootScope.showSpinner = false; 529 | $rootScope.Cache = CACHE; 530 | deferred.resolve(CACHE); 531 | }); 532 | }); 533 | 534 | return deferred.promise; 535 | 536 | }, 537 | /** 538 | * Get one subject at a particular version 539 | */ 540 | getSubjectAtVersion: function (subjectName, subjectVersion) { 541 | 542 | var deferred = $q.defer(); 543 | 544 | // If it's easier to fetch it from cache 545 | var subjectFromCache = getFromCache(subjectName, subjectVersion); 546 | if (subjectFromCache !== undefined) { 547 | deferred.resolve(subjectFromCache); 548 | } else { 549 | var start = new Date().getTime(); 550 | getSubjectAtVersion(subjectName, subjectVersion).then( 551 | function success(subjectInformation) { 552 | //cache it 553 | var subjectInformationWithMetadata = { 554 | version: subjectInformation.version, 555 | id: subjectInformation.id, 556 | schema: subjectInformation.schema, // this is text 557 | Schema: JSON.parse(subjectInformation.schema), // this is json 558 | subjectName: subjectInformation.subject 559 | }; 560 | $log.debug(" pipeline: " + subjectName + "/" + subjectVersion + " in [ " + (new Date().getTime() - start) + " ] msec"); 561 | deferred.resolve(subjectInformationWithMetadata); 562 | }, 563 | function errorCallback(response) { 564 | $log.error("Failure with : " + JSON.stringify(response)); 565 | }); 566 | } 567 | return deferred.promise; 568 | 569 | }, 570 | 571 | /** 572 | * GETs the entire subject's history, by 573 | * 574 | * i. Getting all version 575 | * ii. Fetching each version either from cache or from HTTP GET 576 | */ 577 | getSubjectHistory: function (subjectName) { 578 | 579 | var deferred = $q.defer(); 580 | 581 | $log.info("Getting subject [ " + subjectName + "] history"); 582 | var completeSubjectHistory = []; 583 | getSubjectsVersions(subjectName).then( 584 | function success(allVersions) { 585 | var urlCalls = []; 586 | angular.forEach(allVersions, function (version) { 587 | // If in cache 588 | var subjectFromCache = getFromCache(subjectName, version); 589 | if (subjectFromCache !== undefined) { 590 | completeSubjectHistory.push(subjectFromCache); 591 | } else { 592 | urlCalls.push($http.get(env.SCHEMA_REGISTRY() + '/subjects/' + subjectName + '/versions/' + version)); 593 | } 594 | }); 595 | // Get all missing versions and add them to cache 596 | $q.all(urlCalls).then(function (results) { 597 | angular.forEach(results, function (result) { 598 | completeSubjectHistory.push(result.data); 599 | }); 600 | deferred.resolve(completeSubjectHistory); 601 | }); 602 | }, 603 | function failure(data) { 604 | deferred.reject("pdata=>" + data); 605 | }); 606 | 607 | return deferred.promise; 608 | 609 | }, 610 | 611 | /** 612 | * Get the history in a diff format convenient for rendering a ui 613 | */ 614 | getSubjectHistoryDiff: function (subjectHistory) { 615 | var changelog = []; 616 | 617 | $log.info("Sorting by version.."); 618 | var sortedHistory = UtilsFactory.sortByVersion(subjectHistory); 619 | for (var i = 0; i < sortedHistory.length; i++) { 620 | var previous = ''; 621 | if (i > 0) 622 | previous = JSON.parse(sortedHistory[i - 1].schema); 623 | var changeDetected = { 624 | version: sortedHistory[i].version, 625 | id: sortedHistory[i].id, 626 | current: JSON.parse(sortedHistory[i].schema), 627 | previous: previous 628 | }; 629 | changelog.push(changeDetected); 630 | } 631 | 632 | return changelog; 633 | } 634 | } 635 | 636 | }; 637 | 638 | SchemaRegistryFactory.$inject = ['$rootScope', '$http', '$location', '$q', '$log', 'UtilsFactory', 'env']; 639 | 640 | angularAPP.factory('SchemaRegistryFactory', SchemaRegistryFactory); 641 | --------------------------------------------------------------------------------