├── .gitignore ├── .travis.yml ├── README.md ├── build.properties ├── build.sbt ├── build.sh ├── docker-compose.yml ├── kafkalot-storage └── src │ ├── main │ ├── resources │ │ ├── application.conf │ │ └── logback.xml │ └── scala │ │ └── kafkalot │ │ └── storage │ │ ├── Application.scala │ │ ├── Configuration.scala │ │ ├── api │ │ ├── ConnectorCommand.scala │ │ ├── StaticFileApi.scala │ │ └── StorageApi.scala │ │ ├── exception │ │ └── KafkalotStorageException.scala │ │ ├── kafka │ │ ├── ConnectorProxy.scala │ │ ├── ExportedConnector.scala │ │ └── PluginValidation.scala │ │ └── model │ │ ├── MongoUtil.scala │ │ ├── StorageConnector.scala │ │ └── StorageConnectorDao.scala │ └── test │ ├── resources │ └── local-db │ │ └── SampleConnectors.json │ └── scala │ └── kafkalot │ └── storage │ ├── TestSuite.scala │ ├── api │ └── KafkaConnectClientSpec.scala │ ├── kafka │ └── PluginValidationResultSpec.scala │ └── model │ └── StorageConnectorDaoSpec.scala ├── kafkalot-ui ├── .babelrc ├── .bowerrc ├── .editorconfig ├── .eslintrc ├── .gitignore ├── .npmrc ├── LICENSE ├── TODO.md ├── bower.json ├── config │ ├── development.config.js │ └── production.config.js ├── lib │ └── .gitkeep ├── package.json ├── src │ ├── actions │ │ ├── ConnectorActions.js │ │ └── index.js │ ├── components │ │ ├── Common │ │ │ ├── App.js │ │ │ ├── ClosableSnackbar │ │ │ │ ├── index.js │ │ │ │ └── style.js │ │ │ ├── Filter │ │ │ │ └── index.js │ │ │ ├── GlobalErrorPage.js │ │ │ ├── NavBar │ │ │ │ ├── index.js │ │ │ │ └── style.js │ │ │ ├── NotFoundPage.js │ │ │ ├── Paginator │ │ │ │ ├── index.js │ │ │ │ └── style.css │ │ │ └── Selector │ │ │ │ └── index.js │ │ └── ConnectorPage │ │ │ ├── ConnectorConfigEditor.js │ │ │ ├── ConnectorCreateEditor.js │ │ │ ├── ConnectorHeader │ │ │ ├── index.js │ │ │ └── style.js │ │ │ ├── ConnectorList │ │ │ ├── ConnectorListItem.js │ │ │ ├── ConnectorTask.js │ │ │ ├── ListItemColumn.js │ │ │ ├── index.js │ │ │ └── style.js │ │ │ └── RemoveDialog.js │ ├── constants │ │ ├── Config.js │ │ ├── ConnectorCommand.js │ │ ├── ConnectorState.js │ │ ├── EditorMode.js │ │ ├── Error.js │ │ ├── Page.js │ │ ├── Sorter.js │ │ ├── State.js │ │ └── Theme.js │ ├── containers │ │ ├── ConnectorPage │ │ │ ├── index.js │ │ │ └── style.js │ │ └── MainPage │ │ │ ├── index.js │ │ │ └── style.js │ ├── global.css │ ├── index.html │ ├── index.js │ ├── middlewares │ │ ├── Api.js │ │ ├── Handler.js │ │ ├── Saga.js │ │ ├── Url.js │ │ └── __tests__ │ │ │ └── Api.spec.js │ ├── reducers │ │ ├── ConnectorReducer │ │ │ ├── ClosableSnackbarState.js │ │ │ ├── ConfigEditorState.js │ │ │ ├── ConfigSchemaState.js │ │ │ ├── ConnectorActionType.js │ │ │ ├── ConnectorListState.js │ │ │ ├── CreateEditorState.js │ │ │ ├── PaginatorState.js │ │ │ ├── RemoveDialogState.js │ │ │ ├── Selector.js │ │ │ ├── StorageSelectorState.js │ │ │ └── index.js │ │ └── index.js │ ├── routes.js │ ├── store │ │ ├── configureStore.dev.js │ │ ├── configureStore.js │ │ └── configureStore.prod.js │ └── util │ │ ├── Logger.js │ │ └── SchemaUtil.js ├── tools │ ├── BuildBundle.js │ ├── BuildConfig.js │ ├── BuildHtml.js │ ├── BuildLogger.js │ ├── Clean.js │ ├── Config.js │ ├── DevServer.js │ └── ProdServer.js └── webpack.config.js ├── project ├── Dep.scala ├── NpmPlugin.scala └── plugins.sbt └── with-kafka.yml /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | kafkalot-storage/src/main/resources/public/ 3 | 4 | ### SBT ### 5 | # Simple Build Tool 6 | # http://www.scala-sbt.org/release/docs/Getting-Started/Directories.html#configuring-version-control 7 | 8 | target/ 9 | lib_managed/ 10 | src_managed/ 11 | project/boot/ 12 | .history 13 | .cache 14 | 15 | 16 | ### Scala ### 17 | *.class 18 | *.log 19 | 20 | # sbt specific 21 | .cache 22 | .history 23 | .lib/ 24 | dist/* 25 | target/ 26 | lib_managed/ 27 | src_managed/ 28 | project/boot/ 29 | project/plugins/project/ 30 | 31 | # Scala-IDE specific 32 | .scala_dependencies 33 | .worksheet 34 | 35 | 36 | ### Intellij ### 37 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 38 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 39 | 40 | # User-specific stuff: 41 | .idea/workspace.xml 42 | .idea/tasks.xml 43 | .idea/dictionaries 44 | .idea/vcs.xml 45 | .idea/jsLibraryMappings.xml 46 | 47 | # Sensitive or high-churn files: 48 | .idea/dataSources.ids 49 | .idea/dataSources.xml 50 | .idea/dataSources.local.xml 51 | .idea/sqlDataSources.xml 52 | .idea/dynamic.xml 53 | .idea/uiDesigner.xml 54 | 55 | # Gradle: 56 | .idea/gradle.xml 57 | .idea/libraries 58 | 59 | # Mongo Explorer plugin: 60 | .idea/mongoSettings.xml 61 | 62 | ## File-based project format: 63 | *.iws 64 | 65 | ## Plugin-specific files: 66 | 67 | # IntelliJ 68 | /out/ 69 | 70 | # mpeltonen/sbt-idea plugin 71 | .idea_modules/ 72 | 73 | # JIRA plugin 74 | atlassian-ide-plugin.xml 75 | 76 | # Crashlytics plugin (for Android Studio and IntelliJ) 77 | com_crashlytics_export_strings.xml 78 | crashlytics.properties 79 | crashlytics-build.properties 80 | fabric.properties 81 | 82 | ### Intellij Patch ### 83 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 84 | 85 | *.iml 86 | # modules.xml 87 | 88 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: scala 2 | scala: 3 | - 2.11.8 4 | sudo: false 5 | script: 6 | - sbt "project kafkalot-storage" "test" 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kafkalot 2 | 3 | [![Build Status](https://travis-ci.org/1ambda/kafka-connect-dashboard.svg?branch=master)](https://travis-ci.org/1ambda/kafka-connect-dashboard) [![Coverage Status](https://coveralls.io/repos/github/1ambda/kafka-connect-dashboard/badge.svg?branch=master)](https://coveralls.io/github/1ambda/kafka-connect-dashboard?branch=master) 4 | 5 | Centralize your [kafka-connect](http://kafka.apache.org/documentation.html#connect) instances 6 | 7 | - supports connect 0.10.0.0+ 8 | - shipped with dockerized images and the docker-compose file 9 | - compatibility with confluent platform 3.0.0 10 | - easy connector instance life-cycle management with fancy UI ([screenshots](https://github.com/1ambda/kafka-connect-dashboard/wiki/Screenshots)) 11 | - easy config validation, management using [JSON Schema](http://json-schema.org/), [JSONEditor](https://github.com/josdejong/jsoneditor) 12 | 13 |
14 | 15 | Future Plans 16 | 17 | - support [kafka-stream](http://kafka.apache.org/documentation.html#streams) 18 | - real-time metrics for connect and stream 19 | 20 |
21 | 22 | ## Demo 23 | 24 | ![Main](https://raw.githubusercontent.com/1ambda/kafka-connect-dashboard/screenshot/screenshots/main.png) 25 | 26 | See more [screenshots](https://github.com/1ambda/kafka-connect-dashboard/wiki/Screenshots) 27 | 28 |
29 | 30 | ## Usage 31 | 32 | Kafkalot consist of 2 sub-projects 33 | 34 | - *kafkalot-storage* (REST Server): persist configurations of connects and handling commands (start, validate, etc) 35 | - *kafkalot-ui* (SPA): provides view for managing connectors easily 36 | 37 | ### with Docker 38 | 39 | Set these env variables before launching compose 40 | 41 | > NOTE that a connect cluster should be in the same network otherwise kafkalot can't access 42 | 43 | - `KAFKALOT_STORAGE_CONNECTOR_CLUSTERHOST`: kafka connect cluster host 44 | - `KAFKALOT_STORAGE_CONNECTOR_CLUSTERPORT`: kafka connect cluster port 45 | 46 | ```shell 47 | $ wget https://raw.githubusercontent.com/1ambda/kafka-connect-dashboard/master/docker-compose.yml 48 | 49 | $ KAFKALOT_STORAGE_CONNECTOR_CLUSTERHOST=$CLUSTER_HOST \ 50 | KAFKALOT_STORAGE_CONNECTOR_CLUSTERPORT=$CLUSTER_PORT \ 51 | docker-compose up 52 | ``` 53 | 54 | If you do not have a connector cluster yet, use dockerized kafka and ZK 55 | 56 | ```shell 57 | $ wget https://raw.githubusercontent.com/1ambda/kafka-connect-dashboard/master/docker-compose.yml 58 | $ wget https://raw.githubusercontent.com/1ambda/kafka-connect-dashboard/master/with-kafka.yml 59 | 60 | # env variables are already configured in `with-kafka.yml` 61 | $ docker-compose -f docker-compose.yml -f with-kafka.yml up 62 | ``` 63 | 64 | See [docker-compose.yml](https://github.com/1ambda/kafka-connect-dashboard/blob/master/docker-compose.yml) and [with-kafka.yml](https://github.com/1ambda/kafka-connect-dashboard/blob/master/with-kafka.yml) 65 | 66 | ### without Docker (Not Recommended) 67 | 68 | See [Running kafkalot without Docker](https://github.com/1ambda/kafka-connect-dashboard/wiki/Running-without-Docker) 69 | -------------------------------------------------------------------------------- /build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=0.13.11 2 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | /** project setting */ 2 | 3 | val DockerNamespace = "1ambda" 4 | 5 | lazy val commonSettings = Seq( 6 | organization := "kafkalot", 7 | resolvers ++= Seq( 8 | Resolver.jcenterRepo, 9 | Resolver.sonatypeRepo("releases"), 10 | Resolver.sonatypeRepo("snapshots"), 11 | "twitter-repo" at "http://maven.twttr.com", 12 | Resolver.url("bintray-alpeb-sbt-plugins", url("http://dl.bintray.com/alpeb/sbt-plugins"))(Resolver.ivyStylePatterns) 13 | ) 14 | ) 15 | 16 | lazy val PROJECT_UI = Project("kafkalot-ui", file("kafkalot-ui")) 17 | .enablePlugins(NpmPlugin) 18 | .settings(commonSettings: _*) 19 | .settings( 20 | version := "0.0.1" 21 | , targetDirectory in npm := baseDirectory.value 22 | , npmTasks in npm := Seq( 23 | NpmTask("install"), 24 | NpmTask("run test"), 25 | NpmTask( 26 | "run build", 27 | Seq( 28 | NpmEnv("KAFKALOT_TITLE", "kafkalot"), 29 | NpmEnv("KAFKALOT_STORAGES", "[{\"name\":\"kafkalot-storage\",\"address\":\"\"}]") 30 | ) 31 | ) 32 | ) 33 | ) 34 | 35 | lazy val PROJECT_STORAGE = Project("kafkalot-storage", file("kafkalot-storage")) 36 | .enablePlugins(sbtdocker.DockerPlugin, JavaAppPackaging) 37 | .settings(commonSettings: _*) 38 | .settings( 39 | version := "0.0.1" 40 | , scalaVersion := "2.11.8" 41 | , libraryDependencies ++= Dep.STORAGE.value 42 | , mainClass in Compile := Some("kafkalot.storage.Application") 43 | , mappings in Universal += { (resourceDirectory in Compile).value / "application.conf" -> "conf/application.conf" } 44 | , bashScriptExtraDefines += """addJava "-Dconfig.file=${app_home}/../conf/application.conf"""" 45 | , mappings in Universal += { (resourceDirectory in Compile).value / "logback.xml" -> "conf/logback.xml" } 46 | , bashScriptExtraDefines += """addJava "-Dlogback.configurationFile=${app_home}/../conf/logback.xml"""" 47 | , topLevelDirectory := None 48 | , target in Universal := file(baseDirectory.value.getParent + "/dist/storage") 49 | , dockerfile in docker := { 50 | val appDir: File = stage.value 51 | val targetDir = "/app" 52 | 53 | new Dockerfile { 54 | from("java:8") 55 | copy(appDir, targetDir) 56 | entryPoint(s"$targetDir/bin/${executableScriptName.value}") 57 | expose(3003) 58 | } 59 | } 60 | , imageNames in docker := Seq( 61 | ImageName(s"${DockerNamespace}/${name.value}:latest"), 62 | ImageName( 63 | namespace = Some(DockerNamespace), 64 | repository = name.value, 65 | tag = Some("v" + version.value) 66 | ) 67 | ) 68 | , packageBin in Compile <<= (packageBin in Compile).dependsOn(npm in PROJECT_UI) 69 | ) 70 | 71 | 72 | cancelable in Global := true 73 | 74 | parallelExecution in Global := false 75 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # SET VAR 4 | PROG_MV=$(which mv) 5 | PROG_RM=$(which rm) 6 | PROG_MKDIR=$(which mkdir) 7 | 8 | PROG_UNZIP=$(which unzip) 9 | PROG_ZIP=$(which zip) 10 | 11 | PROG_SBT=$(which sbt) 12 | PROG_NPM=$(which npm) 13 | 14 | ROOT_DIR=$PWD 15 | DIST_DIR=$ROOT_DIR/dist 16 | DIST_STORAGE_DIR=$DIST_DIR/storage 17 | 18 | TAG="[KAFKALOT_SCRIPT]" 19 | 20 | # GET SCRIPT PARAM 21 | if [ "$#" -ne 1 ]; then 22 | echo -e "\n${TAG} Invalid Arguments. Usage: ./build.sh VERSION_NO\n" >&2; exit 1 23 | fi 24 | VERSION_NO=$1 25 | 26 | # REMOVE DIST DIR 27 | echo -e "${TAG} Initialize dist directory\n" 28 | $PROG_RM -rf $DIST_DIR 29 | $PROG_MKDIR $DIST_DIR 30 | 31 | # RUN BUILD: kafkalot-storage 32 | echo -e "${TAG} Building kafkalot-storage\n" 33 | cd $ROOT_DIR 34 | $PROG_SBT "project kafkalot-storage" "test" "universal:packageBin" 35 | 36 | cd $DIST_STORAGE_DIR 37 | $PROG_UNZIP *.zip # unzip 38 | $PROG_RM -rf tmp *.zip # remove tmp files 39 | 40 | # ZIP RESULT 41 | RELEASE_NAME=kafkalot-${VERSION_NO} 42 | echo -e "${TAG} Creating ${RELEASE_NAME}\n" 43 | cd $DIST_DIR 44 | 45 | $PROG_MKDIR $RELEASE_NAME 46 | $PROG_MV storage $RELEASE_NAME 47 | $PROG_ZIP -r $RELEASE_NAME.zip $RELEASE_NAME 48 | 49 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | mongo: 4 | image: mongo:3.2.0 5 | 6 | storage: 7 | image: 1ambda/kafkalot-storage 8 | links: 9 | - mongo 10 | ports: 11 | - "3003" 12 | environment: 13 | - KAFKALOT_STORAGE_CONNECTOR_CLUSTERHOST=${KAFKALOT_STORAGE_CONNECTOR_CLUSTERHOST} 14 | - KAFKALOT_STORAGE_CONNECTOR_CLUSTERPORT=${KAFKALOT_STORAGE_CONNECTOR_CLUSTERPORT} 15 | - KAFKALOT_STORAGE_APP_PORT=3003 16 | - KAFKALOT_STORAGE_MONGO_HOST=mongo 17 | - KAFKALOT_STORAGE_MONGO_PORT=27017 18 | - KAFKALOT_STORAGE_MONGO_DB=kafkalot-local 19 | -------------------------------------------------------------------------------- /kafkalot-storage/src/main/resources/application.conf: -------------------------------------------------------------------------------- 1 | 2 | kafkalot { 3 | storage { 4 | 5 | app { 6 | port = 3003 7 | port = ${?KAFKALOT_STORAGE_APP_PORT} 8 | } 9 | 10 | connector { 11 | clusterHost = "localhost" 12 | clusterHost = ${?KAFKALOT_STORAGE_CONNECTOR_CLUSTERHOST} 13 | clusterPort = "8083" 14 | clusterPort = ${?KAFKALOT_STORAGE_CONNECTOR_CLUSTERPORT} 15 | } 16 | 17 | mongo { 18 | host: "localhost" 19 | host= ${?KAFKALOT_STORAGE_MONGO_HOST} 20 | port = 27017 21 | port = ${?KAFKALOT_STORAGE_MONGO_PORT} 22 | db = "kafkalot-local" 23 | db = ${?KAFKALOT_STORAGE_MONGO_DB} 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /kafkalot-storage/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | true 5 | 6 | 7 | 8 | 9 | %d{HH:mm:ss.SSS} %-5level [%thread] %logger{36} - %msg%n 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /kafkalot-storage/src/main/scala/kafkalot/storage/Application.scala: -------------------------------------------------------------------------------- 1 | package kafkalot.storage 2 | 3 | import java.util.concurrent.atomic.AtomicBoolean 4 | 5 | import com.twitter.finagle.Http 6 | import com.twitter.finagle.http.filter.Cors 7 | import com.twitter.util.Await 8 | import com.typesafe.scalalogging.LazyLogging 9 | import io.finch._ 10 | import io.finch.circe._ 11 | import io.circe._ 12 | import io.circe.generic.auto._ 13 | import io.circe.parser._ 14 | import io.circe.syntax._ 15 | import io.circe.jawn._ 16 | import shapeless._ 17 | import kafkalot.storage.api._ 18 | import kafkalot.storage.model.MongoUtil 19 | 20 | object Application extends App with LazyLogging { 21 | private val shutdown = new AtomicBoolean(false) 22 | 23 | /** use `application/json` as Content-Type of error response */ 24 | implicit val encodeException: Encoder[Exception] = Encoder.instance(e => 25 | Json.obj( 26 | "error_type" -> e.getClass.getCanonicalName.asJson 27 | , "error_message" -> e.getMessage.asJson 28 | ) 29 | ) 30 | 31 | val corsFilter = new Cors.HttpFilter(Cors.Policy( 32 | allowsOrigin = { origin: String => Some(origin) } 33 | , allowsMethods = { method: String => Some(Seq("GET", "POST", "PUT", "DELETE")) } 34 | , allowsHeaders = { headers: Seq[String] => Some(headers) } 35 | , supportsCredentials = true 36 | )) 37 | 38 | val service = (StaticFileApi.api :+: StorageApi.api).toService 39 | 40 | def shutdownHook: Unit = { 41 | val wasShuttingDown = shutdown.getAndSet(true) 42 | 43 | if (!wasShuttingDown) { 44 | logger.info("Stopping kafkalot-storage...") 45 | 46 | service.close() 47 | MongoUtil.mongoClient.close() 48 | 49 | logger.info("Stopped kafkalot-storage") 50 | } 51 | } 52 | 53 | def start() = { 54 | logger.info("Starting kafkalot-storage...") 55 | sys.addShutdownHook(shutdownHook) 56 | 57 | Await.ready( 58 | Http.server.serve(s":${Configuration.app.port}" 59 | , corsFilter andThen service) 60 | ) 61 | } 62 | 63 | start() 64 | } 65 | 66 | -------------------------------------------------------------------------------- /kafkalot-storage/src/main/scala/kafkalot/storage/Configuration.scala: -------------------------------------------------------------------------------- 1 | package kafkalot.storage 2 | 3 | import com.typesafe.config.ConfigFactory 4 | 5 | case class MongoConfiguration(host: String, port: Int, db: String) 6 | case class ConnectorConfiguration(clusterHost: String, clusterPort: String) 7 | case class AppConfiguration(port: Int) 8 | 9 | object Configuration { 10 | import net.ceedubs.ficus.Ficus._ 11 | import net.ceedubs.ficus.readers.ArbitraryTypeReader._ 12 | 13 | /** validate config on startup time */ 14 | 15 | val connector = 16 | ConfigFactory.load().as[ConnectorConfiguration]("kafkalot.storage.connector") 17 | 18 | val mongo = 19 | ConfigFactory.load().as[MongoConfiguration]("kafkalot.storage.mongo") 20 | 21 | val app = 22 | ConfigFactory.load().as[AppConfiguration]("kafkalot.storage.app") 23 | } 24 | -------------------------------------------------------------------------------- /kafkalot-storage/src/main/scala/kafkalot/storage/api/ConnectorCommand.scala: -------------------------------------------------------------------------------- 1 | package kafkalot.storage.api 2 | 3 | case class ConnectorCommand(operation: String) 4 | case class ConnectorTaskCommand(operation: String) 5 | 6 | object ConnectorCommand { 7 | val OPERATION_START = "start" 8 | val OPERATION_STOP = "stop" 9 | val OPERATION_RESTART = "restart" 10 | val OPERATION_PAUSE = "pause" 11 | val OPERATION_RESUME = "resume" 12 | val OPERATION_ENABLE = "enable" 13 | val OPERATION_DISABLE = "disable" 14 | } 15 | 16 | object ConnectorTaskCommand { 17 | val OPERATION_RESTART = "restart" 18 | } -------------------------------------------------------------------------------- /kafkalot-storage/src/main/scala/kafkalot/storage/api/StaticFileApi.scala: -------------------------------------------------------------------------------- 1 | package kafkalot.storage.api 2 | 3 | import io.finch._ 4 | import com.twitter.io.{Buf, Reader} 5 | import com.typesafe.scalalogging.LazyLogging 6 | 7 | object StaticFileApi extends LazyLogging { 8 | 9 | val ContentTypeTextPlain = ("Content-Type", "text/plain") 10 | val ContentTypeTextHtml = ("Content-Type", "text/html") 11 | val ContentTypeTextCss = ("Content-Type", "text/css") 12 | 13 | val index: Endpoint[Buf] = get(/) { 14 | val indexResource = getClass.getResourceAsStream("/public/index.html") 15 | val indexStream = Reader.fromStream(indexResource) 16 | Ok(Reader.readAll(indexStream)).withHeader(ContentTypeTextHtml) 17 | } 18 | 19 | val bundle: Endpoint[Buf] = get("bundle.js") { 20 | val bundleResource = getClass.getResourceAsStream("/public/bundle.js") 21 | val bundleStream = Reader.fromStream(bundleResource) 22 | Ok(Reader.readAll(bundleStream)).withHeader(ContentTypeTextPlain) 23 | } 24 | 25 | /** TODO remove css files endpoint by bundling them into app.js or global css */ 26 | val fontAwesomeCss: Endpoint[Buf] = get("bower_components" :: "font-awesome" :: "css" :: "font-awesome.min.css") { 27 | val fontAwesomeResource = getClass.getResourceAsStream("/public/bower_components/font-awesome/css/font-awesome.min.css") 28 | val fontAwesomeCssStream = Reader.fromStream(fontAwesomeResource) 29 | Ok(Reader.readAll(fontAwesomeCssStream)).withHeader(ContentTypeTextCss) 30 | } 31 | 32 | /** TODO remove css files endpoint by bundling them into app.js or global css */ 33 | val materializeCss: Endpoint[Buf] = get("bower_components" :: "Materialize" :: "dist" :: "css" :: "materialize.min.css") { 34 | val materializeResource = getClass.getResourceAsStream("/public/bower_components/Materialize/dist/css/materialize.min.css") 35 | val materializeCssStream = Reader.fromStream(materializeResource) 36 | Ok(Reader.readAll(materializeCssStream)).withHeader(ContentTypeTextCss) 37 | } 38 | 39 | /** TODO remove css files endpoint by bundling them into app.js or global css */ 40 | val materializeCssFont: Endpoint[Buf] = get("bower_components" :: "Materialize" :: "dist" :: "fonts" :: "roboto" :: string) { 41 | font: String => 42 | val fontResource = getClass.getResourceAsStream(s"/public/bower_components/Materialize/dist/fonts/roboto/${font}") 43 | val fontStream = Reader.fromStream(fontResource) 44 | Ok(Reader.readAll(fontStream)).withHeader(ContentTypeTextCss) 45 | } 46 | 47 | val api = ( 48 | index 49 | :+: bundle 50 | :+: fontAwesomeCss 51 | :+: materializeCss 52 | :+: materializeCssFont 53 | ) handle { 54 | case e: Exception => 55 | logger.error("Unknown exception occurred while serving static files", e) 56 | InternalServerError(e) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /kafkalot-storage/src/main/scala/kafkalot/storage/api/StorageApi.scala: -------------------------------------------------------------------------------- 1 | package kafkalot.storage.api 2 | 3 | import com.twitter.util.Future 4 | import com.twitter.finagle.http 5 | import com.typesafe.scalalogging.LazyLogging 6 | import io.finch._ 7 | import io.finch.circe._ 8 | import io.circe._ 9 | import io.circe.generic.auto._ 10 | import io.circe.parser._ 11 | import io.circe.syntax._ 12 | import io.circe.jawn._ 13 | import shapeless._ 14 | import kafkalot.storage.exception._ 15 | import kafkalot.storage.kafka._ 16 | import kafkalot.storage.model._ 17 | 18 | object StorageApi extends LazyLogging { 19 | 20 | val API_VERSION = "v1" 21 | val RES_API = "api" 22 | val RES_CONNECTORS = "connectors" 23 | val RES_CONFIG = "config" 24 | val RES_COMMAND = "command" 25 | val RES_TASKS = "tasks" 26 | val RES_CONNECTOR_PLUGINS = "connector-plugins" 27 | val RES_VALIDATE = "validate" 28 | 29 | val handleGetConnectors: Endpoint[List[String]] = 30 | get(RES_API :: API_VERSION :: RES_CONNECTORS) mapOutputAsync { _ => 31 | StorageConnectorDao.getAll().map { scs: List[StorageConnector] => 32 | val names: List[String] = scs.map(_.name) 33 | Ok(names) 34 | } 35 | } 36 | 37 | val handleGetConnector: Endpoint[ExportedConnector] = 38 | get(RES_API :: API_VERSION :: RES_CONNECTORS :: string) mapOutputAsync { 39 | case connectorName => 40 | ExportedConnector.get(connectorName) map { Ok(_) } 41 | } 42 | 43 | val handleGetConnectorTasks: Endpoint[List[ConnectorTask]] = 44 | get(RES_API :: API_VERSION :: RES_CONNECTORS :: 45 | string :: RES_TASKS) mapOutputAsync { 46 | case connectorName => 47 | ExportedConnector.get(connectorName) map { ec => Ok(ec.tasks) } 48 | } 49 | 50 | val handleGetConnectorTask: Endpoint[ConnectorTask] = 51 | get(RES_API :: API_VERSION :: RES_CONNECTORS :: 52 | string :: RES_TASKS :: int) mapOutputAsync { 53 | case connectorName :: taskId :: HNil => 54 | ExportedConnector.get(connectorName) map { ec => 55 | if (taskId >= ec.tasks.length) { 56 | logger.error(s"Can't find task ${taskId} for ${ec.name}") 57 | throw new NoSuchConnectorTask(s"Can't find task ${taskId} for ${ec.name} (tasks.length: ${ec.tasks.length})") 58 | } 59 | else Ok(ec.tasks(taskId)) 60 | } 61 | } 62 | 63 | 64 | val handlePutConnectorConfig: Endpoint[ExportedConnector] = 65 | put(RES_API :: API_VERSION :: RES_CONNECTORS :: 66 | string :: RES_CONFIG :: body.as[JsonObject]) mapOutputAsync { 67 | 68 | case connectorName :: config :: HNil => 69 | val fEc: Future[ExportedConnector] = 70 | for { 71 | sc <- StorageConnector.get(connectorName) 72 | ec <- sc.updateConfig(config) 73 | } yield ec 74 | 75 | fEc map { Ok(_) } 76 | } 77 | 78 | val handlePostConnector: Endpoint[ExportedConnector] = 79 | post(RES_API :: API_VERSION :: RES_CONNECTORS :: body.as[RawConnector]) { (c: RawConnector) => 80 | StorageConnectorDao.insert(c.toInitialStorageConnector) map { inserted => 81 | Created(inserted.toStoppedExportedConnector) 82 | } 83 | } 84 | 85 | val handleDeleteConnector: Endpoint[Boolean] = 86 | delete(RES_API :: API_VERSION :: RES_CONNECTORS :: string) mapOutputAsync { 87 | 88 | case connectorName => 89 | ExportedConnector.get(connectorName).flatMap { ec => 90 | if (ec.state != ConnectorState.REGISTERED) { 91 | logger.error(s"Can't delete connector which is not in REGISTERED state (${connectorName})") 92 | Future { 93 | BadRequest(new InvalidStorageConnectorState(s"Can't delete connector which is not in REGISTERED state (${connectorName})")) 94 | } 95 | } else { 96 | StorageConnectorDao.delete(ec.name).map { Ok(_) } 97 | } 98 | } 99 | } 100 | 101 | val handlePostConnectorCommand: Endpoint[ExportedConnector] = 102 | post(RES_API :: API_VERSION :: RES_CONNECTORS :: 103 | string :: RES_COMMAND :: body.as[ConnectorCommand]) mapOutputAsync { 104 | 105 | case connectorName :: command :: HNil => 106 | StorageConnector.get(connectorName).flatMap { sc => 107 | sc.handleCommand(command) map { Ok(_) } 108 | } 109 | } 110 | 111 | val handlePostConnectorTaskCommand: Endpoint[ConnectorTask] = 112 | post(RES_API :: API_VERSION :: RES_CONNECTORS :: 113 | string :: RES_TASKS :: int :: RES_COMMAND :: body.as[ConnectorTaskCommand]) mapOutputAsync { 114 | 115 | case connectorName :: taskId :: command :: HNil => 116 | StorageConnector.get(connectorName) flatMap { sc => 117 | sc.handleTaskCommand(taskId, command).map { Ok(_) } 118 | } 119 | } 120 | 121 | val handleGetConnectorPlugins: Endpoint[Json] = 122 | get(RES_API :: API_VERSION :: RES_CONNECTOR_PLUGINS) mapOutputAsync { _ => 123 | ConnectorClientApi.getConnectorPlugins() map { Ok(_) } 124 | } 125 | 126 | val handleGetConnectorPluginsJSONSchema: Endpoint[JSONSchema] = 127 | get(RES_API :: API_VERSION :: RES_CONNECTOR_PLUGINS :: 128 | string :: "schema") mapOutputAsync { 129 | case connectorClass => 130 | ConnectorClientApi.getConnectorPluginJSONSchema(connectorClass) map { Ok(_) } 131 | } 132 | 133 | val handlePutConnectorConfigValidation: Endpoint[ConnectorConfigValidationResult] = 134 | put(RES_API :: API_VERSION :: RES_CONNECTOR_PLUGINS :: 135 | string :: RES_VALIDATE :: body.as[Json]) mapOutputAsync { 136 | case connectorClass :: config :: HNil => 137 | ConnectorClientApi.validateConnectorPlugin(connectorClass, config) map { Ok(_) } 138 | } 139 | 140 | val api = 141 | (handleGetConnectors 142 | :+: handleGetConnector 143 | :+: handleGetConnectorTasks 144 | :+: handleGetConnectorTask 145 | :+: handlePutConnectorConfig 146 | :+: handlePostConnector 147 | :+: handleDeleteConnector 148 | :+: handlePostConnectorCommand 149 | :+: handlePostConnectorTaskCommand 150 | :+: handleGetConnectorPlugins 151 | :+: handlePutConnectorConfigValidation 152 | :+: handleGetConnectorPluginsJSONSchema 153 | ) handle { /** global exception handler */ 154 | 155 | case e: NoSuchConnectorInStorage => 156 | logger.error(s"Cannot found connector in storage") 157 | NotFound(e) 158 | case e: CannotCreateDuplicatedConnector => 159 | logger.error(s"Can't create duplicated connector") 160 | Conflict(e) 161 | case e: ConnectorPluginValidationFailed => 162 | logger.error(s"Invalid Connector Config ${e.message}") 163 | BadRequest(e) 164 | case e: ConnectorPluginNotFoundException => 165 | logger.error(s"Requested connector class does not exist (${e.message})") 166 | BadRequest(e) 167 | 168 | case e: KafkalotException => 169 | logger.error(e.getMessage) 170 | BadRequest(e) 171 | 172 | case e: ConnectClusterException => 173 | logger.error(e.getMessage) 174 | InternalServerError(e) 175 | 176 | case e: Exception => 177 | logger.error(s"Unknown exception occurred: ${e.getMessage}", e) 178 | InternalServerError(e) 179 | } 180 | 181 | } 182 | -------------------------------------------------------------------------------- /kafkalot-storage/src/main/scala/kafkalot/storage/exception/KafkalotStorageException.scala: -------------------------------------------------------------------------------- 1 | package kafkalot.storage.exception 2 | 3 | sealed trait KafkalotException extends Exception { def message: String } 4 | sealed trait ConnectClusterException extends Exception { def message: String } 5 | 6 | case class RawConnectorHasNoConnectorClassField(message: String) 7 | extends Exception(message) with KafkalotException 8 | 9 | /** client exception: 400 */ 10 | case class NoSuchConnectorInStorage(message: String) 11 | extends Exception(message) with KafkalotException 12 | case class NoSuchConnectorTask(message: String) 13 | extends Exception(message) with KafkalotException 14 | case class InvalidStorageConnectorState(message: String) 15 | extends Exception(message) with KafkalotException 16 | case class InvalidStorageConnectorCommand(message: String) 17 | extends Exception(message) with KafkalotException 18 | case class InvalidStorageConnectorTaskCommand(message: String) 19 | extends Exception(message) with KafkalotException 20 | case class CannotCreateDuplicatedConnector(message: String) 21 | extends Exception(message) with KafkalotException 22 | case class TaskIdDoesNotExist(message: String) 23 | extends Exception(message) with KafkalotException 24 | case class ConnectorPluginNotFoundException(message: String) 25 | extends Exception(message) with KafkalotException 26 | case class ConnectorPluginValidationFailed(message: String) 27 | extends Exception(message) with KafkalotException 28 | 29 | /** connector cluster exception : 500 */ 30 | 31 | case class FailedToDeleteConnectorFromCluster(message: String) 32 | extends Exception(message) with ConnectClusterException 33 | case class FailedToGetConnectorsFromCluster(message: String) 34 | extends Exception(message) with ConnectClusterException 35 | case class FailedToGetConnectorFromCluster(message: String) 36 | extends Exception(message) with ConnectClusterException 37 | case class FailedToUpdateConnectorConfig(message: String) 38 | extends Exception(message) with ConnectClusterException 39 | case class FailedToStartConnector(message: String) 40 | extends Exception(message) with ConnectClusterException 41 | case class FailedToStopConnector(message: String) 42 | extends Exception(message) with ConnectClusterException 43 | case class FailedToRestartConnector(message: String) 44 | extends Exception(message) with ConnectClusterException 45 | case class FailedToRestartConnectorTask(message: String) 46 | extends Exception(message) with ConnectClusterException 47 | case class FailedToPauseConnector(message: String) 48 | extends Exception(message) with ConnectClusterException 49 | case class FailedToResumeConnector(message: String) 50 | extends Exception(message) with ConnectClusterException 51 | case class FailedToGetConnectorPlugins(message: String) 52 | extends Exception(message) with ConnectClusterException 53 | 54 | 55 | -------------------------------------------------------------------------------- /kafkalot-storage/src/main/scala/kafkalot/storage/kafka/ExportedConnector.scala: -------------------------------------------------------------------------------- 1 | package kafkalot.storage.kafka 2 | 3 | import com.twitter.util.Future 4 | import io.circe._ 5 | import io.circe.generic.auto._ 6 | import io.circe.jawn._ 7 | import io.circe.syntax._ 8 | 9 | import kafkalot.storage.model.{StorageConnectorDao, StorageConnectorMeta} 10 | 11 | case class ExportedConnector(name: String, 12 | state: String, 13 | config: JsonObject, 14 | tasks: List[ConnectorTask], 15 | _meta: Option[StorageConnectorMeta]) 16 | 17 | object ExportedConnector { 18 | def get(connectorName: String): Future[ExportedConnector] = { 19 | for { 20 | sc <- StorageConnectorDao.get(connectorName) 21 | ec <- sc.toExportedConnector 22 | } yield ec 23 | } 24 | } 25 | 26 | -------------------------------------------------------------------------------- /kafkalot-storage/src/main/scala/kafkalot/storage/kafka/PluginValidation.scala: -------------------------------------------------------------------------------- 1 | package kafkalot.storage.kafka 2 | 3 | import io.circe._ 4 | import io.circe.generic.auto._ 5 | import io.circe.parser._ 6 | import io.circe.syntax._ 7 | import io.circe.jawn._ 8 | 9 | /** Raw response from the connector cluster for connector config validation */ 10 | case class PluginValidation(name: String, 11 | error_count: Int, 12 | groups: List[String], 13 | configs: List[ValidationConfig]) { 14 | 15 | def toJSONSchema(): JSONSchema = { 16 | var properties: JsonObject = JsonObject.empty 17 | 18 | /** extract `required` */ 19 | val required: List[String] = configs 20 | .filter(c => c.definition.required) 21 | .map(c => c.definition.name) 22 | 23 | /** extract `properties` */ 24 | configs.foreach { c => 25 | /** every type in PluginValidation should be converted to `string` in JSONSchema */ 26 | val typeField = JsonObject.singleton("type", "string".asJson).asJson 27 | properties = properties.add(c.definition.name, typeField) 28 | } 29 | 30 | JSONSchema(name, name, properties, required) 31 | } 32 | 33 | def toValidationResult(): ConnectorConfigValidationResult = { 34 | val errorMessages = configs.foldLeft(List[String]()) { (acc, c) => 35 | if (c.value.errors.size == 0) acc 36 | else acc ++ c.value.errors 37 | } 38 | 39 | ConnectorConfigValidationResult(name, error_count, errorMessages) 40 | } 41 | } 42 | 43 | case class ValidationConfigDefinition(name: String, 44 | `type`: String, 45 | required: Boolean) 46 | case class ValidationConfigValue(name: String, 47 | value: Option[String], /** `value` field might be null */ 48 | errors: List[String]) 49 | case class ValidationConfig(definition: ValidationConfigDefinition, 50 | value: ValidationConfigValue) 51 | 52 | 53 | /** JSONSchema used in config validation in client when opening dialogs */ 54 | case class JSONSchema(title: String, 55 | description: String, 56 | properties: JsonObject, 57 | required: List[String], 58 | $schema: String = "http://json-schema.org/draft-04/schema#", 59 | `type`: String = "object" /** type is reserved keyword in Scala */) 60 | 61 | 62 | /** ValidationResult used in config validation in client using `validate` button */ 63 | case class ConnectorConfigValidationResult(name: String, 64 | error_count: Int, 65 | error_messages: List[String]) 66 | 67 | -------------------------------------------------------------------------------- /kafkalot-storage/src/main/scala/kafkalot/storage/model/MongoUtil.scala: -------------------------------------------------------------------------------- 1 | package kafkalot.storage.model 2 | 3 | import kafkalot.storage.Configuration 4 | 5 | 6 | object MongoUtil { 7 | import com.mongodb.casbah.Imports._ 8 | 9 | val MONGO_HOST = Configuration.mongo.host 10 | val MONGO_PORT = Configuration.mongo.port 11 | val MONGO_DB_NAME = Configuration.mongo.db 12 | 13 | lazy val mongoClient = MongoClient(MONGO_HOST, MONGO_PORT) 14 | lazy val mongoDb = mongoClient(MONGO_DB_NAME) 15 | } 16 | -------------------------------------------------------------------------------- /kafkalot-storage/src/main/scala/kafkalot/storage/model/StorageConnector.scala: -------------------------------------------------------------------------------- 1 | package kafkalot.storage.model 2 | 3 | import io.circe._ 4 | import io.circe.generic.auto._ 5 | import io.circe.jawn._ 6 | import io.circe.syntax._ 7 | import cats.data.Xor 8 | import com.twitter.util.Future 9 | import kafkalot.storage.api.{ConnectorCommand, ConnectorTaskCommand} 10 | import kafkalot.storage.exception.{FailedToGetConnectorsFromCluster, InvalidStorageConnectorCommand, InvalidStorageConnectorState, InvalidStorageConnectorTaskCommand} 11 | import kafkalot.storage.kafka._ 12 | 13 | case class StorageConnectorMeta(enabled: Boolean, 14 | tags: List[String]) 15 | 16 | case class StorageConnector(name: String, 17 | config: JsonObject, 18 | _meta: StorageConnectorMeta) { 19 | 20 | def toPersistedStorageConnector: PersistedStorageConnector = { 21 | PersistedStorageConnector(name, config.asJson.noSpaces, _meta) 22 | } 23 | 24 | def toStoppedExportedConnector: ExportedConnector = { 25 | val state = if (_meta.enabled) ConnectorState.REGISTERED else ConnectorState.DISABLED 26 | ExportedConnector(name, state, config, Nil, Some(_meta)) 27 | } 28 | 29 | def toExportedConnector: Future[ExportedConnector] = { 30 | ConnectorClientApi.getConnector(name) map { ec => 31 | ec.copy(_meta = Some(_meta)) /** set _meta to running connector */ 32 | } rescue { 33 | // TODO: case e: FailedToGetConnectorsFromCluster => Future { toStoppedExportedConnector } 34 | case e: Exception => Future { toStoppedExportedConnector } 35 | } 36 | } 37 | 38 | def toRawConnector: RawConnector = { 39 | RawConnector(name, config) 40 | } 41 | 42 | def updateConfig(config: JsonObject): Future[ExportedConnector] = { 43 | if (!_meta.enabled) { 44 | Future.exception(new InvalidStorageConnectorState(s"Cannot update disabled connector config (${name})")) 45 | } else { 46 | ConnectorClientApi.getConnector(name) map { ec => 47 | throw new InvalidStorageConnectorState(s"Cannot update running connector config (${name})") 48 | } rescue { 49 | case e: InvalidStorageConnectorState => 50 | Future.exception(e) 51 | case _ => StorageConnectorDao.update(this.copy(config = config)).map { 52 | _.toStoppedExportedConnector 53 | } 54 | } /** rescue */ 55 | } /** else */ 56 | } 57 | 58 | def handleCommand(command: ConnectorCommand): Future[ExportedConnector] = { 59 | command.operation match { 60 | case ConnectorCommand.OPERATION_START => 61 | ConnectorClientApi.start(toRawConnector) flatMap { _ => toExportedConnector } 62 | 63 | case ConnectorCommand.OPERATION_STOP => 64 | ConnectorClientApi.stop(toRawConnector) map { _ => toStoppedExportedConnector } 65 | 66 | case ConnectorCommand.OPERATION_RESTART => 67 | ConnectorClientApi.restart(toRawConnector) flatMap { _ => toExportedConnector } 68 | 69 | case ConnectorCommand.OPERATION_PAUSE => 70 | ConnectorClientApi.pause(toRawConnector) flatMap { _ => toExportedConnector } 71 | 72 | case ConnectorCommand.OPERATION_RESUME => 73 | ConnectorClientApi.resume(toRawConnector) flatMap { _ => toExportedConnector } 74 | 75 | case ConnectorCommand.OPERATION_ENABLE => 76 | val updatedMeta = _meta.copy(enabled = true) 77 | val updated = this.copy(_meta = updatedMeta) 78 | StorageConnectorDao.update(updated) map { persisted => 79 | persisted.toStoppedExportedConnector 80 | } 81 | 82 | case ConnectorCommand.OPERATION_DISABLE => 83 | val updatedMeta = _meta.copy(enabled = false) 84 | val updated = this.copy(_meta = updatedMeta) 85 | StorageConnectorDao.update(updated) map { persisted => 86 | persisted.toStoppedExportedConnector 87 | } 88 | case _ => 89 | Future.exception( 90 | new InvalidStorageConnectorCommand( 91 | s"Invalid storage connector command ${command.operation}" 92 | ) 93 | ) 94 | } 95 | } 96 | 97 | def handleTaskCommand(taskId: Int, command: ConnectorTaskCommand): Future[ConnectorTask] = { 98 | command.operation match { 99 | case ConnectorTaskCommand.OPERATION_RESTART => 100 | ConnectorClientApi.restartTask(toRawConnector, taskId) 101 | 102 | case _ => 103 | Future.exception( 104 | new InvalidStorageConnectorTaskCommand( 105 | s"Invalid storage task connector command ${command.operation}" 106 | ) 107 | ) 108 | } 109 | } 110 | 111 | } 112 | 113 | object StorageConnector { 114 | val FIELD_KEY_META = "_meta" 115 | val FIELD_KEY_NAME = "name" 116 | 117 | def get(connectorName: String): Future[StorageConnector] = { 118 | StorageConnectorDao.get(connectorName) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /kafkalot-storage/src/main/scala/kafkalot/storage/model/StorageConnectorDao.scala: -------------------------------------------------------------------------------- 1 | package kafkalot.storage.model 2 | 3 | import io.circe._ 4 | import io.circe.generic.auto._ 5 | import io.circe.jawn._ 6 | import io.circe.syntax._ 7 | import com.twitter.util.Future 8 | import com.novus.salat._ 9 | import com.novus.salat.global._ 10 | import com.mongodb.casbah.Imports._ 11 | import com.typesafe.scalalogging.LazyLogging 12 | import kafkalot.storage.exception.{CannotCreateDuplicatedConnector, NoSuchConnectorInStorage} 13 | 14 | /** 15 | * StorageConnector including the stringified `config` field 16 | * 17 | * since MongoDB doesn't allow to save BSON fields including `.` in field key (e.g `tasks.max`) 18 | * so that we have to stringify StroageConnector.config before saving it 19 | **/ 20 | case class PersistedStorageConnector(name: String, 21 | config: String, 22 | _meta: StorageConnectorMeta) { 23 | 24 | def toStorageConnector: StorageConnector = { 25 | val jsonConfig = decode[JsonObject](config).valueOr(throw _) 26 | StorageConnector(name, jsonConfig, _meta) 27 | } 28 | } 29 | 30 | object StorageConnectorDao extends LazyLogging { 31 | import MongoUtil._ 32 | 33 | val COLLECTION_NAME_CONNECTOR = "connector" 34 | 35 | lazy val collection = mongoDb(COLLECTION_NAME_CONNECTOR) 36 | 37 | def createSelectQuery(sc: StorageConnector): MongoDBObject = 38 | createSelectQuery(sc.name) 39 | 40 | def createSelectQuery(name: String): MongoDBObject = 41 | MongoDBObject(StorageConnector.FIELD_KEY_NAME -> name) 42 | 43 | 44 | def convertStorageConnectorToDBObject(sc: StorageConnector): MongoDBObject = { 45 | val pc = sc.toPersistedStorageConnector 46 | grater[PersistedStorageConnector].asDBObject(pc) 47 | } 48 | 49 | def convertDBObjectToStorageConnector(dbo: DBObject): StorageConnector = { 50 | val pc = grater[PersistedStorageConnector].asObject(dbo) 51 | pc.toStorageConnector 52 | } 53 | 54 | def insert(sc: StorageConnector): Future[StorageConnector] = { 55 | Future { 56 | val query = createSelectQuery(sc.name) 57 | collection.findOne(query) map { convertDBObjectToStorageConnector(_) } 58 | } map { scOption: Option[StorageConnector] => 59 | scOption match { 60 | case Some(_) => 61 | throw new CannotCreateDuplicatedConnector(s"Cannot create duplicated connector (s${sc.name})") 62 | case None => 63 | val dbo = convertStorageConnectorToDBObject(sc) 64 | collection.insert(dbo) 65 | sc 66 | } 67 | } 68 | } 69 | 70 | def update(sc: StorageConnector): Future[StorageConnector] = { 71 | Future { 72 | val query = createSelectQuery(sc.name) 73 | collection.findOne(query) map { convertDBObjectToStorageConnector(_) } 74 | } map { scOption: Option[StorageConnector] => 75 | scOption match { 76 | case Some(_) => 77 | val query = createSelectQuery(sc) 78 | val dbo = convertStorageConnectorToDBObject(sc) 79 | collection.update(query, dbo, upsert = true) 80 | sc 81 | case None => 82 | throw new NoSuchConnectorInStorage(s"Cannot update non-exist connector in storage (${sc.name})") 83 | } 84 | } 85 | } 86 | 87 | def delete(connectorName: String): Future[Boolean] = { 88 | Future { 89 | val query = createSelectQuery(connectorName) 90 | val result = collection.remove(query) 91 | 92 | if (result.getN < 1) 93 | throw new NoSuchConnectorInStorage(s"Cannot delete non-exist connector in storage (${connectorName})") 94 | else true 95 | } 96 | } 97 | 98 | def get(name: String): Future[StorageConnector] = { 99 | Future { 100 | val query = createSelectQuery(name) 101 | collection.findOne(query) map { convertDBObjectToStorageConnector(_) } 102 | } map { optSc: Option[StorageConnector] => 103 | optSc match { 104 | case None => 105 | logger.error("Can't get a connector which does not exist in storage") 106 | throw new NoSuchConnectorInStorage("Can't get a connector which does not exist in storage") 107 | case Some(sc) => sc 108 | } 109 | } 110 | } 111 | 112 | def getAll(): Future[List[StorageConnector]] = { 113 | Future { 114 | collection.find().toList map { convertDBObjectToStorageConnector(_) } 115 | } 116 | } 117 | 118 | } 119 | -------------------------------------------------------------------------------- /kafkalot-storage/src/test/resources/local-db/SampleConnectors.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "kafka-connect-console-sink-0", 4 | "config": "{ \"connector.class\": \"io.github.lambda.ConsoleSinkConnector\", \"tasks.max\": \"4\", \"topics\": \"test-p4-1\", \"name\": \"kafka-connect-console-sink-0\", \"id\": \"console-connector-0-A\" }", 5 | "_meta": { 6 | "enabled": true, 7 | "tags": ["console"] 8 | } 9 | }, 10 | { 11 | "name": "akka-stream-connector-sink-0", 12 | "config": "{ \"connector.class\": \"io.github.lambda.ConsoleSinkConnector\", \"tasks.max\": \"2\", \"topics\": \"test-p4-1\", \"name\": \"akka-stream-connector-sink-0\", \"id\": \"akka-stream-connector-sink-0-A\" }", 13 | "_meta": { 14 | "enabled": true, 15 | "tags": ["akka-stream"] 16 | } 17 | }, 18 | { 19 | "name": "spark-stream-connector-sink-0", 20 | "config": "{ \"connector.class\": \"io.github.lambda.ConsoleSinkConnector\", \"tasks.max\": \"1\", \"topics\": \"test-p4-1\", \"name\": \"spark-stream-connector-sink-0\", \"id\": \"spark-stream-connector-sink-0-A\" }", 21 | "_meta": { 22 | "enabled": false, 23 | "tags": ["spark-stream"] 24 | } 25 | } 26 | ] -------------------------------------------------------------------------------- /kafkalot-storage/src/test/scala/kafkalot/storage/TestSuite.scala: -------------------------------------------------------------------------------- 1 | package kafkalot.storage 2 | 3 | import org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach, FunSuite, Matchers} 4 | 5 | trait TestSuite 6 | extends FunSuite with Matchers with BeforeAndAfterEach with BeforeAndAfterAll 7 | -------------------------------------------------------------------------------- /kafkalot-storage/src/test/scala/kafkalot/storage/api/KafkaConnectClientSpec.scala: -------------------------------------------------------------------------------- 1 | package kafkalot.storage.api 2 | 3 | import java.net.URL 4 | 5 | import io.circe._ 6 | import io.circe.generic.auto._ 7 | import io.circe.jawn._ 8 | import io.circe.syntax._ 9 | import kafkalot.storage.TestSuite 10 | import kafkalot.storage.kafka.ConnectorClientApi 11 | 12 | 13 | class KafkaConnectClientSpec extends TestSuite { 14 | 15 | import ConnectorClientApi._ 16 | 17 | test("example") { 18 | 19 | val startConfig = 20 | """ 21 | { 22 | "name": "kafka-connect-console-sink-143", 23 | "config": { 24 | "connector.class": "io.github.lambda.ConsoleSinkConnector", 25 | "tasks.max": "4", 26 | "topics": "test-p4-1", 27 | "name": "kafka-connect-console-sink-143", 28 | "id": "console-connector-id-2" 29 | } 30 | } 31 | """.stripMargin 32 | 33 | val config: String = """ 34 | { 35 | "connector.class": "io.github.lambda.ConsoleSinkConnector", 36 | "tasks.max": "4", 37 | "topics": "test-p4-1", 38 | "name": "kafka-connect-console-sink-143", 39 | "id": "console-connector-id-2" 40 | } 41 | """ 42 | 43 | val jsonConfig: JsonObject = parse(config).getOrElse(Json.Null).asObject.get 44 | val startJsonConfig = parse(startConfig).getOrElse(Json.Null) 45 | val name = "kafka-connect-console-sink-143" 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /kafkalot-storage/src/test/scala/kafkalot/storage/kafka/PluginValidationResultSpec.scala: -------------------------------------------------------------------------------- 1 | package kafkalot.storage.kafka 2 | 3 | import io.circe.JsonObject 4 | import io.circe.generic.auto._ 5 | import io.circe.syntax._ 6 | import io.circe.jawn._ 7 | import kafkalot.storage.TestSuite 8 | 9 | class PluginValidationResultSpec extends TestSuite { 10 | import PluginValidationResultFixture._ 11 | 12 | test("PluginValidation should be parsed") { 13 | val v = decode[PluginValidation](validationResult1).valueOr(throw _) 14 | v.name shouldBe "io.github.lambda.ConsoleSinkConnector" 15 | } 16 | 17 | test("toJSONSchema should return a valid JSONSchema") { 18 | val v: PluginValidation = decode[PluginValidation](validationResult1).valueOr(throw _) 19 | 20 | val schema: JSONSchema = v.toJSONSchema() 21 | 22 | /** validate `required` */ 23 | schema.required should contain only ("connector.class", "name", "id") 24 | 25 | /** validate `properties` */ 26 | schema.properties.fields should contain only ("connector.class", "name", "id", "topics", "tasks.max") 27 | } 28 | 29 | test("toJSONSchema should return the expected JSONSchema") { 30 | val v = decode[PluginValidation](validationResult1).valueOr(throw _) 31 | 32 | val expected: JsonObject = decode[JsonObject](expectedJSONSchema1).valueOr(throw _) 33 | v.toJSONSchema().asJson.asObject.get shouldBe expected 34 | } 35 | 36 | test( 37 | """toValidationResult should return 38 | |the expected ConnectorConfigValidationResult 39 | |which contains error_messages""".stripMargin) { 40 | 41 | val v = decode[PluginValidation](validationResult1).valueOr(throw _) 42 | val configValidationResult = v.toValidationResult() 43 | 44 | configValidationResult.error_count shouldBe 3 45 | configValidationResult.error_messages should contain only ( 46 | "Missing required configuration \"connector.class\" which has no default value." 47 | , "Missing required configuration \"name\" which has no default value." 48 | , "Missing required configuration \"id\" which has no default value." 49 | ) 50 | 51 | } 52 | } 53 | 54 | object PluginValidationResultFixture { 55 | val validationResult1 = /** validation result: error case */ 56 | """ 57 | |{ 58 | | "name":"io.github.lambda.ConsoleSinkConnector", 59 | | "error_count":3, 60 | | "groups":[ 61 | | "Common" 62 | | ], 63 | | "configs":[ 64 | | { 65 | | "definition":{ 66 | | "name":"connector.class", 67 | | "type":"STRING", 68 | | "required":true, 69 | | "default_value":"", 70 | | "importance":"HIGH", 71 | | "documentation":"Name or alias of the class for this connector. Must be a subclass of org.apache.kafka.connect.connector.Connector. If the connector is org.apache.kafka.connect.file.FileStreamSinkConnector, you can either specify this full name, or use \"FileStreamSink\" or \"FileStreamSinkConnector\" to make the configuration a bit shorter", 72 | | "group":"Common", 73 | | "width":"LONG", 74 | | "display_name":"Connector class", 75 | | "dependents":[ 76 | | 77 | | ], 78 | | "order":2 79 | | }, 80 | | "value":{ 81 | | "name":"connector.class", 82 | | "value":null, 83 | | "recommended_values":[ 84 | | 85 | | ], 86 | | "errors":[ 87 | | "Missing required configuration \"connector.class\" which has no default value." 88 | | ], 89 | | "visible":true 90 | | } 91 | | }, 92 | | { 93 | | "definition":{ 94 | | "name":"name", 95 | | "type":"STRING", 96 | | "required":true, 97 | | "default_value":"", 98 | | "importance":"HIGH", 99 | | "documentation":"Globally unique name to use for this connector.", 100 | | "group":"Common", 101 | | "width":"MEDIUM", 102 | | "display_name":"Connector name", 103 | | "dependents":[ 104 | | 105 | | ], 106 | | "order":1 107 | | }, 108 | | "value":{ 109 | | "name":"name", 110 | | "value":null, 111 | | "recommended_values":[ 112 | | 113 | | ], 114 | | "errors":[ 115 | | "Missing required configuration \"name\" which has no default value." 116 | | ], 117 | | "visible":true 118 | | } 119 | | }, 120 | | { 121 | | "definition":{ 122 | | "name":"id", 123 | | "type":"STRING", 124 | | "required":true, 125 | | "default_value":"", 126 | | "importance":"HIGH", 127 | | "documentation":"Connector ID", 128 | | "group":null, 129 | | "width":"NONE", 130 | | "display_name":"id", 131 | | "dependents":[ 132 | | 133 | | ], 134 | | "order":-1 135 | | }, 136 | | "value":{ 137 | | "name":"id", 138 | | "value":null, 139 | | "recommended_values":[ 140 | | 141 | | ], 142 | | "errors":[ 143 | | "Missing required configuration \"id\" which has no default value." 144 | | ], 145 | | "visible":true 146 | | } 147 | | }, 148 | | { 149 | | "definition":{ 150 | | "name":"tasks.max", 151 | | "type":"INT", 152 | | "required":false, 153 | | "default_value":"1", 154 | | "importance":"HIGH", 155 | | "documentation":"Maximum number of tasks to use for this connector.", 156 | | "group":"Common", 157 | | "width":"SHORT", 158 | | "display_name":"Tasks max", 159 | | "dependents":[ 160 | | 161 | | ], 162 | | "order":3 163 | | }, 164 | | "value":{ 165 | | "name":"tasks.max", 166 | | "value":"1", 167 | | "recommended_values":[ 168 | | 169 | | ], 170 | | "errors":[ 171 | | 172 | | ], 173 | | "visible":true 174 | | } 175 | | }, 176 | | { 177 | | "definition":{ 178 | | "name":"topics", 179 | | "type":"LIST", 180 | | "required":false, 181 | | "default_value":"", 182 | | "importance":"HIGH", 183 | | "documentation":"", 184 | | "group":"Common", 185 | | "width":"LONG", 186 | | "display_name":"Topics", 187 | | "dependents":[ 188 | | 189 | | ], 190 | | "order":4 191 | | }, 192 | | "value":{ 193 | | "name":"topics", 194 | | "value":"", 195 | | "recommended_values":[ 196 | | 197 | | ], 198 | | "errors":[ 199 | | 200 | | ], 201 | | "visible":true 202 | | } 203 | | } 204 | | ] 205 | |} 206 | """.stripMargin 207 | 208 | val expectedJSONSchema1 = 209 | """ 210 | |{ 211 | | "$schema": "http://json-schema.org/draft-04/schema#", 212 | | "title": "io.github.lambda.ConsoleSinkConnector", 213 | | "description": "io.github.lambda.ConsoleSinkConnector", 214 | | "type": "object", 215 | | "properties": { 216 | | "connector.class": { 217 | | "type": "string" 218 | | }, 219 | | "name": { 220 | | "type": "string" 221 | | }, 222 | | "id": { 223 | | "type": "string" 224 | | }, 225 | | "tasks.max": { 226 | | "type": "string" 227 | | }, 228 | | "topics": { 229 | | "type": "string" 230 | | } 231 | | }, 232 | | "required": ["connector.class", "name", "id"] 233 | |} 234 | """.stripMargin 235 | 236 | 237 | } 238 | -------------------------------------------------------------------------------- /kafkalot-storage/src/test/scala/kafkalot/storage/model/StorageConnectorDaoSpec.scala: -------------------------------------------------------------------------------- 1 | package kafkalot.storage.model 2 | 3 | import com.twitter.util.Await 4 | import io.circe._ 5 | import io.circe.generic.auto._ 6 | import io.circe.jawn._ 7 | import io.circe.syntax._ 8 | import kafkalot.storage.TestSuite 9 | 10 | class StorageConnectorDaoSpec extends TestSuite { 11 | test("insert should put StorageConnector") { 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /kafkalot-ui/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | ["transform-runtime", { 4 | "polyfill": true, 5 | "regenerator": true 6 | }] 7 | ], 8 | "presets": ["es2015", "react", "stage-1"], 9 | "env": { 10 | "development": { 11 | "plugins": [ 12 | ["react-transform", { 13 | "transforms": [{ 14 | "transform": "react-transform-hmr", 15 | "imports": ["react"], 16 | "locals": ["module"] 17 | }, { 18 | "transform": "react-transform-catch-errors", 19 | "imports": ["react", "redbox-react"] 20 | }] 21 | }] 22 | ] 23 | }, 24 | "test": { 25 | "plugins": [ 26 | ] 27 | }, 28 | "production": { 29 | "plugins": [ 30 | ] 31 | }, 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /kafkalot-ui/.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "lib/bower_components" 3 | } 4 | -------------------------------------------------------------------------------- /kafkalot-ui/.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /kafkalot-ui/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": "eslint:recommended", 4 | "plugins": [ 5 | "react" 6 | ], 7 | "env": { 8 | "es6": true, 9 | "browser": true, 10 | "node": true, 11 | "jquery": true, 12 | "mocha": true 13 | }, 14 | "ecmaFeatures": { 15 | "jsx": true, 16 | "modules": true 17 | }, 18 | "rules": { 19 | "comma-dangle": [1, "always"], 20 | "quotes": [1, "single"], 21 | "no-console": 1, 22 | "no-debugger": 1, 23 | "no-var": 1, 24 | "semi": [1, "never"], 25 | "no-trailing-spaces": 0, 26 | "eol-last": 0, 27 | "no-unused-vars": 0, 28 | "no-underscore-dangle": 0, 29 | "no-alert": 0, 30 | "no-lone-blocks": 0, 31 | "jsx-quotes": [1, "prefer-double"], 32 | "react/display-name": [ 1, {"ignoreTranspilerName": false }], 33 | "react/forbid-prop-types": [1, {"forbid": ["any"]}], 34 | "react/jsx-boolean-value": 1, 35 | "react/jsx-closing-bracket-location": 0, 36 | "react/jsx-curly-spacing": 1, 37 | "react/jsx-indent-props": 0, 38 | "react/jsx-key": 1, 39 | "react/jsx-max-props-per-line": 0, 40 | "react/jsx-no-bind": 0, 41 | "react/jsx-no-duplicate-props": 1, 42 | "react/jsx-no-literals": 0, 43 | "react/jsx-no-undef": 1, 44 | "react/jsx-pascal-case": 1, 45 | "react/jsx-sort-prop-types": 0, 46 | "react/jsx-sort-props": 0, 47 | "react/jsx-uses-react": 1, 48 | "react/jsx-uses-vars": 1, 49 | "react/no-danger": 1, 50 | "react/no-did-mount-set-state": 1, 51 | "react/no-did-update-set-state": 1, 52 | "react/no-direct-mutation-state": 1, 53 | "react/no-multi-comp": 0, 54 | "react/no-set-state": 0, 55 | "react/no-unknown-property": 1, 56 | "react/prefer-es6-class": 1, 57 | "react/prop-types": 1, 58 | "react/react-in-jsx-scope": 1, 59 | "react/self-closing-comp": 1, 60 | "react/sort-comp": 1, 61 | "react/jsx-wrap-multilines": 1 62 | }, 63 | "globals": { 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /kafkalot-ui/.gitignore: -------------------------------------------------------------------------------- 1 | # resource, config 2 | db.json 3 | 4 | # Bower 5 | lib/bower_components/ 6 | 7 | # Logs 8 | logs 9 | *.log 10 | 11 | # Runtime data 12 | pids 13 | *.pid 14 | *.seed 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 23 | .grunt 24 | 25 | # node-waf configuration 26 | .lock-wscript 27 | 28 | # Compiled binary addons (http://nodejs.org/api/addons.html) 29 | build/Release 30 | 31 | # Dependency directory 32 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 33 | node_modules 34 | 35 | #dist folder 36 | dist 37 | 38 | #Webstorm metadata 39 | .idea 40 | 41 | # Mac files 42 | .DS_Store 43 | 44 | !.gitkeep 45 | -------------------------------------------------------------------------------- /kafkalot-ui/.npmrc: -------------------------------------------------------------------------------- 1 | save-exact=true 2 | -------------------------------------------------------------------------------- /kafkalot-ui/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Cory House 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /kafkalot-ui/TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | - 400 4 | - 500 5 | - last updated at 6 | - name validation (saga middleware) 7 | - auth 8 | - config dialog theme 9 | 10 | # DONE 11 | 12 | - main page 13 | - search area 14 | - create new dialog 15 | -------------------------------------------------------------------------------- /kafkalot-ui/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kafkalot", 3 | "description": "dashboard for kafka-connect", 4 | "main": "", 5 | "authors": [ 6 | "1ambda@gmail.com" 7 | ], 8 | "license": "Apache-2.0", 9 | "keywords": [ 10 | "kafka", "connect", "dashboard", "kafkalot" 11 | ], 12 | "homepage": "https://github.com/1ambda/kafka-connect-dashboard", 13 | "moduleType": [], 14 | "ignore": [ 15 | "**/.*", 16 | "node_modules", 17 | "bower_components", 18 | "test", 19 | "tests" 20 | ], 21 | "dependencies": { 22 | "Materialize": "materialize#^0.97.5", 23 | "font-awesome": "fontawesome#^4.5.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /kafkalot-ui/config/development.config.js: -------------------------------------------------------------------------------- 1 | export const defaultStorages = [ 2 | { name: 'kafka-storage', address: 'http://localhost:3003', }, 3 | ] 4 | 5 | /** exposed variables, should be stringified if it is string */ 6 | export const STORAGES = JSON.stringify(defaultStorages) 7 | export const TITLE = JSON.stringify('Kafkalot') 8 | -------------------------------------------------------------------------------- /kafkalot-ui/config/production.config.js: -------------------------------------------------------------------------------- 1 | const envStorages = process.env.KAFKALOT_STORAGES 2 | const envTitle = process.env.KAFKALOT_TITLE 3 | const envPaginatorItemCount = process.env.KAFKALOT_PAGINATOR_ITEM_COUNT 4 | 5 | /** exposed variables, should be stringified if it is string */ 6 | export const STORAGES = (envStorages === void 0) ? 7 | JSON.stringify([ 8 | { name: 'kafkalot-storage', address: '', }, 9 | ]) : envStorages /** envContainer is already stringified */ 10 | 11 | export const TITLE = (envTitle === void 0) ? 12 | JSON.stringify('Kafkalot') : JSON.stringify(envTitle) 13 | -------------------------------------------------------------------------------- /kafkalot-ui/lib/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1ambda/kafka-connect-dashboard/54d2b929e162dfa030a542c423f932a0c6959aa1/kafkalot-ui/lib/.gitkeep -------------------------------------------------------------------------------- /kafkalot-ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "kafkalot", 3 | "version": "0.0.1", 4 | "description": "dashboard for kafka-connect", 5 | "scripts": { 6 | "postinstall": "bower cache clean && bower install", 7 | "open:dev": "babel-node tools/DevServer.js", 8 | "open:prod": "babel-node tools/ProdServer.js", 9 | "start:client": "cross-env NODE_ENV=development npm-run-all --parallel test:watch open:dev", 10 | "lint:src": "eslint src", 11 | "lint": "npm run lint:src", 12 | "clean": "babel-node tools/Clean.js", 13 | "build:html": "babel-node tools/BuildHtml.js", 14 | "build:js": "cross-env NODE_ENV=production babel-node tools/BuildBundle.js", 15 | "prebuild": "npm run test && npm run clean && npm run build:html", 16 | "build": "npm run build:js", 17 | "test": "cross-env NODE_ENV=test mocha --reporter progress --compilers js:babel-core/register --recursive \"./src/**/*.spec.js\" --require ignore-styles", 18 | "test:watch": "npm run test -- --watch", 19 | "coverage:open": "cross-env NODE_ENV=test istanbul cover node_modules/.bin/_mocha -- -u exports --compilers js:babel-register --recursive \"./src/**/*.spec.js\" --require ignore-styles && open ./coverage/lcov-report/index.html", 20 | "coverage:coverall": "cross-env NODE_ENV=test istanbul cover node_modules/.bin/_mocha -- -u exports --compilers js:babel-register --recursive \"./src/**/*.spec.js\" --require ignore-styles --report lcovonly -- && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage" 21 | }, 22 | "author": "1ambda@gmail.com", 23 | "license": "Apache-2.0", 24 | "dependencies": { 25 | "connect-history-api-fallback": "1.3.0", 26 | "debug": "2.2.0", 27 | "deep-equal": "1.0.1", 28 | "isomorphic-fetch": "2.2.1", 29 | "jsoneditor": "5.5.7", 30 | "keycode": "2.1.4", 31 | "material-ui": "0.15.4", 32 | "react": "15.3.0", 33 | "react-dom": "15.3.0", 34 | "react-redux": "4.4.5", 35 | "react-router": "2.6.1", 36 | "react-router-redux": "4.0.5", 37 | "react-tap-event-plugin": "1.0.0", 38 | "redux": "3.5.2", 39 | "redux-saga": "0.11.0" 40 | }, 41 | "devDependencies": { 42 | "autoprefixer": "6.4.0", 43 | "babel-cli": "6.11.4", 44 | "babel-core": "6.13.2", 45 | "babel-eslint": "6.1.2", 46 | "babel-loader": "6.2.5", 47 | "babel-plugin-react-display-name": "2.0.0", 48 | "babel-plugin-react-transform": "2.0.2", 49 | "babel-plugin-transform-runtime": "6.12.0", 50 | "babel-preset-es2015": "6.13.2", 51 | "babel-preset-react": "6.11.1", 52 | "babel-preset-stage-1": "6.13.0", 53 | "bower": "1.7.9", 54 | "browser-sync": "2.14.0", 55 | "chai": "3.5.0", 56 | "cheerio": "0.20.0", 57 | "colors": "1.1.2", 58 | "coveralls": "2.11.12", 59 | "cross-env": "2.0.0", 60 | "css-loader": "0.23.1", 61 | "eslint": "3.3.1", 62 | "eslint-loader": "1.5.0", 63 | "eslint-plugin-react": "6.1.2", 64 | "extract-text-webpack-plugin": "1.0.1", 65 | "file-loader": "0.9.0", 66 | "fs-extra": "0.30.0", 67 | "ignore-styles": "4.0.0", 68 | "istanbul": "^1.0.0-alpha.2", 69 | "main-bower-files": "2.13.1", 70 | "mkdirp": "0.5.1", 71 | "mocha": "3.0.2", 72 | "mocha-lcov-reporter": "1.2.0", 73 | "moment": "2.14.1", 74 | "npm-run-all": "3.0.0", 75 | "postcss-cssnext": "2.7.0", 76 | "postcss-import": "8.1.2", 77 | "postcss-loader": "0.10.1", 78 | "postcss-reporter": "1.4.1", 79 | "postcss-url": "5.1.2", 80 | "prettyjson": "1.1.3", 81 | "react-paginate": "2.1.3", 82 | "react-transform-catch-errors": "1.0.2", 83 | "react-transform-hmr": "1.0.4", 84 | "redbox-react": "1.3.0", 85 | "redux-actions": "0.11.0", 86 | "rimraf": "2.5.4", 87 | "style-loader": "0.13.1", 88 | "url-loader": "0.5.7", 89 | "watch": "0.19.2", 90 | "webpack": "1.13.2", 91 | "webpack-dev-middleware": "1.6.1", 92 | "webpack-hot-middleware": "2.12.2", 93 | "yargs": "5.0.0" 94 | }, 95 | "keywords:": [ 96 | "kafka", 97 | "connect", 98 | "dastboard", 99 | "kafkalot" 100 | ], 101 | "repository": { 102 | "type": "git", 103 | "url": "https://github.com/1ambda/kafka-connect-dashboard" 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /kafkalot-ui/src/actions/ConnectorActions.js: -------------------------------------------------------------------------------- 1 | import { createAction, } from 'redux-actions' 2 | 3 | import { Action as PaginatorAction, } from '../reducers/ConnectorReducer/PaginatorState' 4 | import { Action as ConfigEditorAction, } from '../reducers/ConnectorReducer/ConfigEditorState' 5 | import { Action as CreateEditorAction, } from '../reducers/ConnectorReducer/CreateEditorState' 6 | import { Action as RemoveDialogAction, } from '../reducers/ConnectorReducer/RemoveDialogState' 7 | import { Action as ClosableSnackbarAction, } from '../reducers/ConnectorReducer/ClosableSnackbarState' 8 | import { Action as ConnectorAction, } from '../reducers/ConnectorReducer/ConnectorListState' 9 | import { Action as SagaAction, } from '../middlewares/Saga' 10 | 11 | export default Object.assign({}, { 12 | /** Component Actions */ 13 | ...ConfigEditorAction, 14 | ...CreateEditorAction, 15 | ...RemoveDialogAction, 16 | ...ClosableSnackbarAction, 17 | ...ConnectorAction, 18 | ...PaginatorAction, 19 | }, { 20 | /** API Actions */ 21 | ...SagaAction, 22 | } 23 | ) 24 | -------------------------------------------------------------------------------- /kafkalot-ui/src/actions/index.js: -------------------------------------------------------------------------------- 1 | 2 | import ConnectorActions from './ConnectorActions' 3 | import { ROOT, } from '../constants/State' 4 | 5 | export default { 6 | [ROOT.CONNECTOR]: ConnectorActions, 7 | } 8 | 9 | -------------------------------------------------------------------------------- /kafkalot-ui/src/components/Common/App.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, } from 'react' 2 | 3 | import getMuiTheme from 'material-ui/styles/getMuiTheme' 4 | import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider' 5 | import NavBar from './NavBar' 6 | 7 | class App extends React.Component { 8 | static propTypes = { 9 | children: PropTypes.element, 10 | } 11 | 12 | render() { 13 | return ( 14 | 15 |
16 | 17 |
18 |
19 |
20 | {this.props.children} 21 |
22 |
23 |
24 |
25 | ) 26 | } 27 | } 28 | 29 | export default App 30 | -------------------------------------------------------------------------------- /kafkalot-ui/src/components/Common/ClosableSnackbar/index.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, } from 'react' 2 | import Snackbar from 'material-ui/Snackbar' 3 | 4 | import * as style from './style' 5 | 6 | export const CLOSABLE_SNACKBAR_MODE = { 7 | OPEN: 'OPEN', 8 | CLOSE: 'CLOSE', 9 | } 10 | 11 | export default class ClosableSnackbar extends React.Component { 12 | static propTypes = { 13 | snackbarMode: PropTypes.string.isRequired, 14 | message: PropTypes.node.isRequired, 15 | closeSnackbar: PropTypes.func.isRequired, 16 | } 17 | 18 | constructor(props) { 19 | super(props) 20 | } 21 | 22 | render() { 23 | const { snackbarMode, message, closeSnackbar, } = this.props 24 | 25 | return ( 26 | 35 | ) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /kafkalot-ui/src/components/Common/ClosableSnackbar/style.js: -------------------------------------------------------------------------------- 1 | import { ClosableSnackbar, } from '../../../constants/Theme' 2 | 3 | export const snackbar = { 4 | fontWeight: 300, 5 | } 6 | 7 | export const body = { 8 | backgroundColor: ClosableSnackbar.backgroundColor, 9 | } 10 | 11 | -------------------------------------------------------------------------------- /kafkalot-ui/src/components/Common/Filter/index.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, } from 'react' 2 | import TextField from 'material-ui/TextField' 3 | import keycode from 'keycode' 4 | 5 | export default class Filter extends React.Component { 6 | 7 | static propTypes = { 8 | style: PropTypes.object, 9 | handler: PropTypes.func.isRequired, 10 | floatingLabel: PropTypes.string.isRequired, 11 | } 12 | 13 | constructor(props) { 14 | super(props) 15 | 16 | this.handleFilterChange = this.handleFilterChange.bind(this) 17 | } 18 | 19 | handleFilterChange(event) { 20 | const { handler, } = this.props 21 | const filterKeyword = event.target.value.trim() 22 | 23 | if (handler && (keycode(event) === 'enter')) handler(filterKeyword) 24 | } 25 | 26 | render() { 27 | const { floatingLabel, style, } = this.props 28 | 29 | return ( 30 | 33 | ) 34 | } 35 | } 36 | 37 | -------------------------------------------------------------------------------- /kafkalot-ui/src/components/Common/GlobalErrorPage.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, } from 'react' 2 | import { Link, } from 'react-router' 3 | 4 | 5 | 6 | const style = { 7 | title: { 8 | fontWeight: 300, 9 | fontSize: 25, 10 | }, 11 | } 12 | 13 | export default class GlobalErrorPage extends React.Component { 14 | static propTypes = { 15 | title: PropTypes.string.isRequired, 16 | message: PropTypes.string.isRequired, 17 | } 18 | 19 | render() { 20 | const { title, message, } = this.props 21 | 22 | return ( 23 |
24 |

{title}

25 |

{message}

26 | Go back to homepage 27 |
28 | ) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /kafkalot-ui/src/components/Common/NavBar/index.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, } from 'react' 2 | import { Link, IndexLink, } from 'react-router' 3 | 4 | import FlatButton from 'material-ui/FlatButton' 5 | import {Toolbar, ToolbarGroup, ToolbarTitle,} from 'material-ui/Toolbar' 6 | 7 | 8 | import IconButton from 'material-ui/IconButton' 9 | import IconMenu from 'material-ui/IconMenu' 10 | import MoreVertIcon from 'material-ui/svg-icons/navigation/more-vert' 11 | import MenuItem from 'material-ui/MenuItem' 12 | 13 | import * as Page from '../../../constants/Page' 14 | import * as style from './style.js' 15 | import * as CONFIG from '../../../constants/Config' 16 | 17 | export default class NavBar extends React.Component { 18 | 19 | render() { 20 | return ( 21 | 22 | 23 | {CONFIG.TITLE}} 24 | style={style.title} /> 25 | {Page.ConnectorPageTitle}} 26 | style={style.linkButton} /> 27 | 28 | 29 | } > 32 | 33 | 34 | 35 | 36 | 37 | ) 38 | } 39 | } 40 | 41 | 42 | -------------------------------------------------------------------------------- /kafkalot-ui/src/components/Common/NavBar/style.js: -------------------------------------------------------------------------------- 1 | import * as Theme from '../../../constants/Theme' 2 | 3 | export const navbar = { 4 | backgroundColor: Theme.NavBarColors.backgroundColor, 5 | boxShadow: '0 1px 6px rgba(0,0,0,0.12), 0 1px 4px rgba(0,0,0,0.12)', 6 | } 7 | 8 | export const text = { 9 | color: Theme.NavBarColors.textColor, 10 | } 11 | 12 | export const icon = { 13 | fill: Theme.NavBarColors.textColor, 14 | } 15 | 16 | export const title = { 17 | marginLeft: 20, 18 | marginRight: 20, 19 | fontWeight: 200, 20 | } 21 | 22 | export const linkButton = { 23 | marginLeft: 0, 24 | marginRight: 0, 25 | fontWeight: 300, 26 | } 27 | 28 | export const iconMenu= { 29 | marginTop: 3, 30 | } 31 | 32 | export const iconMenuItem = { 33 | fontWeight: 300, 34 | } 35 | 36 | -------------------------------------------------------------------------------- /kafkalot-ui/src/components/Common/NotFoundPage.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link, } from 'react-router' 3 | 4 | export default class NotFoundPage extends React.Component { 5 | render() { 6 | return ( 7 |
8 |

9 | 404 Page Not Found 10 |

11 | Go back to homepage 12 |
13 | ) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /kafkalot-ui/src/components/Common/Paginator/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactPaginate from 'react-paginate' 3 | 4 | /** since react-paginate support css style only, we need a stylesheet */ 5 | import * as paginatorStyle from './style.css' 6 | 7 | const styles = { 8 | container: { 9 | 10 | }, 11 | } 12 | 13 | export default class Paginator extends React.Component { 14 | 15 | static propTypes = { 16 | handler: React.PropTypes.func.isRequired, 17 | totalItemCount: React.PropTypes.number.isRequired, 18 | currentPageOffset: React.PropTypes.number.isRequired, 19 | itemCountPerPage: React.PropTypes.number.isRequired, 20 | } 21 | 22 | constructor(props) { 23 | super(props) 24 | 25 | this.handlePageChange = this.handlePageChange.bind(this) 26 | } 27 | 28 | handlePageChange(data) { 29 | const { handler, } = this.props 30 | 31 | if (data.selected || Number.isInteger(data.selected) || handler ) { 32 | handler(data.selected) /** return currentPageOffset (zero-based offset) */ 33 | } 34 | } 35 | 36 | render() { 37 | const { totalItemCount, currentPageOffset, itemCountPerPage, } = this.props 38 | const totalPageCount = Math.ceil(totalItemCount / itemCountPerPage) 39 | 40 | return( 41 | ...} 44 | forceSelected={currentPageOffset} 45 | pageNum={totalPageCount} 46 | marginPagesDisplayed={1} 47 | pageRangeDisplayed={3} 48 | clickCallback={this.handlePageChange} 49 | containerClassName={"pagination"} 50 | subContainerClassName={"pages pagination"} 51 | activeClassName={paginatorStyle.activeLabel} /> 52 | 53 | ) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /kafkalot-ui/src/components/Common/Paginator/style.css: -------------------------------------------------------------------------------- 1 | .activeLabel { 2 | font-weight: 400; 3 | } 4 | -------------------------------------------------------------------------------- /kafkalot-ui/src/components/Common/Selector/index.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, } from 'react' 2 | import SelectField from 'material-ui/SelectField' 3 | import MenuItem from 'material-ui/MenuItem' 4 | 5 | export default class Selector extends React.Component { 6 | 7 | static propTypes = { 8 | handler: PropTypes.func.isRequired, 9 | strategies: PropTypes.array.isRequired, 10 | currentStrategy: PropTypes.string.isRequired, 11 | style: PropTypes.object, 12 | labelStyle: PropTypes.object, 13 | floatingLabel: PropTypes.string, 14 | floatingLabelStyle: PropTypes.object, 15 | } 16 | 17 | constructor(props) { 18 | super(props) 19 | 20 | this.handleSorterChange = this.handleSorterChange.bind(this) 21 | } 22 | 23 | handleSorterChange(event, selectedIndex) { 24 | const { handler, strategies, } = this.props 25 | 26 | if (handler) handler(strategies[selectedIndex]) 27 | } 28 | 29 | render() { 30 | const { 31 | style, strategies, currentStrategy, 32 | labelStyle, floatingLabel, floatingLabelStyle, 33 | } = this.props 34 | 35 | const strategyElems = strategies.reduce((acc, s) => { 36 | acc.push() 37 | return acc 38 | }, []) 39 | 40 | return ( 41 | 47 | {strategyElems} 48 | 49 | ) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /kafkalot-ui/src/components/ConnectorPage/ConnectorConfigEditor.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, } from 'react' 2 | 3 | import 'jsoneditor/dist/jsoneditor.min.css' 4 | import JSONEditor from 'jsoneditor/dist/jsoneditor.min.js' 5 | 6 | import Chip from 'material-ui/Chip' 7 | import FlatButton from 'material-ui/FlatButton' 8 | import Dialog from 'material-ui/Dialog' 9 | 10 | import { ConnectorEditor as EditorTheme, } from '../../constants/Theme' 11 | import { JsonEditorMode, } from '../../constants/EditorMode' 12 | import { ConnectorProperty, } from '../../reducers/ConnectorReducer/ConnectorListState' 13 | import { Payload as ConfigSchemaPayload, } from '../../reducers/ConnectorReducer/ConfigSchemaState' 14 | 15 | const dialogStyle = { 16 | editorContainer: { paddingTop: 20, }, 17 | editor: { height: 450, }, 18 | button : { marginRight: 15, marginBottom: 15, }, 19 | buttonLabel : { fontWeight: 300, }, 20 | validateButtonLabel: { fontWeight: 300, color: EditorTheme.validationButton, }, 21 | disabledValidateButtonLabel: { fontWeight: 300, color: EditorTheme.disabledValidationButton, }, 22 | title: { float: 'left', fontWeight: 300, fontSize: 20, }, 23 | titleChipLabel: { fontWeight: 300, fontSize: 13, }, 24 | titleChip: { float: 'left', marginLeft: 10, }, 25 | } 26 | 27 | const ELEM_ID_EDITOR_DIALOG = 'config-editor' 28 | 29 | export function getDefaultEditorMode (readonly) { 30 | return (readonly) ? JsonEditorMode.CODE: JsonEditorMode.CODE 31 | } 32 | 33 | export function getAvailableEditorModes (readonly) { 34 | return (readonly) ? [JsonEditorMode.VIEW, JsonEditorMode.CODE, ] : 35 | [JsonEditorMode.TREE, JsonEditorMode.CODE,] 36 | } 37 | 38 | export function isEditorJSONChanged(initial, updated) { 39 | return !(JSON.stringify(initial) === JSON.stringify(updated)) 40 | } 41 | 42 | export default class ConnectorConfigEditor extends React.Component { 43 | static propTypes = { 44 | close: PropTypes.func.isRequired, 45 | update: PropTypes.func.isRequired, 46 | validateConnectorConfig: PropTypes.func.isRequired, 47 | 48 | name: PropTypes.string.isRequired, 49 | config: PropTypes.object.isRequired, 50 | configSchema: PropTypes.object, /** optional */ 51 | errorMessages: PropTypes.array.isRequired, 52 | readonly: PropTypes.bool.isRequired, 53 | } 54 | 55 | constructor(props) { 56 | super(props) 57 | 58 | /** 59 | * to avoid re-drawing the whole page whenever JSON is updated, 60 | * EditorDialog manages editor as it's state 61 | */ 62 | this.state = { editor: null, isJSONChanged: false, } 63 | 64 | this.handleValidateConfig = this.handleValidateConfig.bind(this) 65 | this.handleClose = this.handleClose.bind(this) 66 | this.handleUpdate = this.handleUpdate.bind(this) 67 | this.handleEditorError = this.handleEditorError.bind(this) 68 | this.handleEditorContentChange = this.handleEditorContentChange.bind(this) 69 | } 70 | 71 | /** component life-cycle */ 72 | componentDidMount() { 73 | const { config, configSchema, readonly, } = this.props 74 | const initialJSON = config 75 | 76 | const defaultMode = getDefaultEditorMode(readonly) 77 | const availableModes = getAvailableEditorModes(readonly) 78 | 79 | const onChangeHandler = this.handleEditorContentChange 80 | const onErrorHandler = this.handleEditorError 81 | 82 | const options = { 83 | search: false, // TODO: fix search width 84 | mode: defaultMode, 85 | schema: configSchema, 86 | modes: availableModes, 87 | onChange: onChangeHandler, 88 | onError: onErrorHandler, 89 | } 90 | 91 | /** external library which does not be managed by React */ 92 | const editor = new JSONEditor(document.getElementById(ELEM_ID_EDITOR_DIALOG), options, initialJSON) 93 | 94 | if (defaultMode !== JsonEditorMode.CODE) editor.expandAll() 95 | 96 | this.setState({ editor, }) // eslint-disable-line react/no-did-mount-set-state,react/no-set-state 97 | } 98 | 99 | /** component life-cycle */ 100 | componentWillReceiveProps(nextProps) { 101 | const { config: currentJSON, } = this.props 102 | const { config: nextJSON, configSchema, } = nextProps 103 | const { editor, } = this.state 104 | 105 | /** if JSON is not changed, then disable `UPDATE` button */ 106 | this.setState({ isJSONChanged: isEditorJSONChanged(currentJSON, nextJSON), }) // eslint-disable-line react/no-set-state 107 | if (editor) editor.setSchema(configSchema) 108 | } 109 | 110 | getEditorJSONValue() { 111 | const { editor, } = this.state 112 | return editor.get() 113 | } 114 | 115 | handleEditorContentChange() { 116 | const { config: prevJSON, } = this.props 117 | 118 | const updatedJSON = this.getEditorJSONValue() 119 | this.setState({ isJSONChanged: isEditorJSONChanged(prevJSON, updatedJSON), }) // eslint-disable-line react/no-set-state 120 | } 121 | 122 | handleEditorError(err) { 123 | /*eslint no-console: ["error", { allow: ["warn", "error"] }] */ 124 | console.error(`JSONEditor: ${err}`) /** TODO 500 page */ 125 | } 126 | 127 | handleValidateConfig() { 128 | const { validateConnectorConfig, } = this.props 129 | const { editor, } = this.state 130 | 131 | const connectorConfig = editor.get() 132 | 133 | const connectorClass = connectorConfig['connector.class'] 134 | 135 | validateConnectorConfig({ 136 | [ConfigSchemaPayload.CONNECTOR_CLASS]: connectorClass, 137 | [ConfigSchemaPayload.CONNECTOR_CONFIG]: connectorConfig, 138 | }) 139 | } 140 | 141 | handleUpdate() { 142 | const { update, name, } = this.props 143 | const { isJSONChanged, } = this.state 144 | 145 | if (isJSONChanged) { 146 | update({ 147 | [ConnectorProperty.NAME]: name, 148 | [ConnectorProperty.CONFIG]: this.getEditorJSONValue(), 149 | }) 150 | } 151 | } 152 | 153 | handleClose() { 154 | const { close, } = this.props 155 | close() 156 | } 157 | 158 | createDialogTitle() { 159 | const { name, configSchema, errorMessages, } = this.props 160 | 161 | const emptySchema = configSchema === void 0 162 | const noError = errorMessages.length === 0 163 | 164 | const schemaChipLabel = (emptySchema) ? 'INVALID CLASS' : 'SCHEMA VALIDATION' 165 | const schemaChipLabelColor = (emptySchema) ? 166 | EditorTheme.titleNoSchemaChipLabelColor : EditorTheme.titleSchemaChipLabelColor 167 | const schemaChipBgColor = (emptySchema) ? 168 | EditorTheme.titleNoSchemaChipBgColor : EditorTheme.titleSchemaChipBgColor 169 | 170 | const errorChipLabel = (noError) ? 'NO ERROR' : `ERROR (${errorMessages.length})` 171 | const errorChipLabelColor = (noError) ? 172 | EditorTheme.titleNoErrorChipLabelColor : EditorTheme.titleErrorChipLabelColor 173 | const errorChipBgColor = (noError) ? 174 | EditorTheme.titleNoErrorChipBgColor : EditorTheme.titleErrorChipBgColor 175 | 176 | const errorChip = (emptySchema) ? null : ( 177 | 181 | {errorChipLabel} 182 | 183 | ) 184 | 185 | return ( 186 |
187 | {name} 188 | 192 | {schemaChipLabel} 193 | 194 | {errorChip} 195 |
196 |
197 | ) 198 | } 199 | 200 | render() { 201 | const { configSchema, readonly, } = this.props 202 | const { isJSONChanged, } = this.state 203 | 204 | const validationButtonDisabled = (configSchema === void 0) 205 | const validationButtonLabelStyle = (configSchema === void 0) ? 206 | dialogStyle.disabledValidateButtonLabel : dialogStyle.validateButtonLabel 207 | 208 | let buttons = [ 209 | , 215 | , 220 | , 224 | ] 225 | 226 | return ( 227 | 233 |
234 |
235 |
236 |
237 | ) 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /kafkalot-ui/src/components/ConnectorPage/ConnectorCreateEditor.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, } from 'react' 2 | 3 | import JSONEditor from 'jsoneditor/dist/jsoneditor.min.js' 4 | 5 | import FlatButton from 'material-ui/FlatButton' 6 | import Dialog from 'material-ui/Dialog' 7 | import SelectField from 'material-ui/SelectField' 8 | import TextField from 'material-ui/TextField' 9 | import MenuItem from 'material-ui/MenuItem' 10 | import Chip from 'material-ui/Chip' 11 | 12 | import { JsonEditorMode, } from '../../constants/EditorMode' 13 | import { Payload as ConfigSchemaPayload, } from '../../reducers/ConnectorReducer/ConfigSchemaState' 14 | import { Payload as CreateEditorPayload, } from '../../reducers/ConnectorReducer/CreateEditorState' 15 | import { ConnectorEditor as EditorTheme, } from '../../constants/Theme' 16 | import * as Logger from '../../util/Logger' 17 | import * as SchemaUtil from '../../util/SchemaUtil' 18 | 19 | const dialogStyle = { 20 | textFieldContainer: { marginTop: 5, }, 21 | selectFieldContainer: {}, 22 | editorContainer: { paddingTop: 5, }, 23 | editor: { height: 350, }, 24 | button : { marginRight: 15, marginBottom: 15, }, 25 | buttonLabel : { fontWeight: 300, }, 26 | validateButtonLabel: { fontWeight: 300, color: EditorTheme.validationButton, }, 27 | disabledValidateButtonLabel: { fontWeight: 300, color: EditorTheme.disabledValidationButton, }, 28 | title: { float: 'left', fontWeight: 300, fontSize: 20, }, 29 | titleChipLabel: { fontWeight: 300, fontSize: 13, }, 30 | titleChip: { float: 'left', marginLeft: 10, }, 31 | } 32 | 33 | const ELEM_ID = 'create-editor' 34 | 35 | export function getDefaultEditorMode() { 36 | return JsonEditorMode.CODE 37 | } 38 | 39 | export function getAvailableEditorModes() { 40 | return [ 41 | JsonEditorMode.CODE, JsonEditorMode.VIEW, JsonEditorMode.TREE, 42 | ] 43 | } 44 | 45 | export default class ConnectorCreateEditor extends React.Component { 46 | static propTypes = { 47 | close: PropTypes.func.isRequired, 48 | create: PropTypes.func.isRequired, 49 | changeSelectedConnectorClass: PropTypes.func.isRequired, 50 | validateConnectorConfig: PropTypes.func.isRequired, 51 | 52 | availableConnectors: PropTypes.array.isRequired, 53 | selectedConnectorClass: PropTypes.string, 54 | configSchema: PropTypes.object, /** optional */ 55 | errorMessages: PropTypes.array.isRequired, 56 | } 57 | 58 | constructor(props) { 59 | super(props) 60 | 61 | /** 62 | * to avoid re-drawing the whole page whenever JSON is updated, 63 | * EditorDialog manages editor as it's state 64 | */ 65 | this.state = { 66 | editor: null, 67 | isValidConnectorClass: false, 68 | connectorName: '', 69 | } 70 | 71 | this.handleEditorError = this.handleEditorError.bind(this) 72 | this.handleCreate = this.handleCreate.bind(this) 73 | this.handleClose = this.handleClose.bind(this) 74 | this.handleClose = this.handleClose.bind(this) 75 | this.handleValidateConfig = this.handleValidateConfig.bind(this) 76 | this.handleEditorContentChange = this.handleEditorContentChange.bind(this) 77 | this.handleSelectFieldChange = this.handleSelectFieldChange.bind(this) 78 | this.handleTextFieldChange = this.handleTextFieldChange.bind(this) 79 | } 80 | 81 | /** component life-cycle */ 82 | componentDidMount() { 83 | const { configSchema, } = this.props 84 | 85 | const defaultMode = getDefaultEditorMode() 86 | const availableModes = getAvailableEditorModes() 87 | const onChangeHandler = this.handleEditorContentChange 88 | const onErrorHandler = this.handleEditorError 89 | 90 | const options = { 91 | search: false, // TODO: fix search width 92 | schema: configSchema, 93 | mode: defaultMode, 94 | modes: availableModes, 95 | onChange: onChangeHandler, 96 | onError: onErrorHandler, 97 | } 98 | 99 | /** external library which does not be managed by React */ 100 | const editor = new JSONEditor(document.getElementById(ELEM_ID), options) 101 | editor.set(SchemaUtil.InitialConnectorConfig) 102 | 103 | if (defaultMode !== JsonEditorMode.CODE) editor.expandAll() 104 | 105 | this.setState({ editor, }) // eslint-disable-line react/no-did-mount-set-state,react/no-set-state 106 | } 107 | 108 | componentWillReceiveProps(nextProps) { 109 | const { configSchema, } = nextProps 110 | const { editor, } = this.state 111 | 112 | if (editor) editor.setSchema(configSchema) 113 | } 114 | 115 | handleEditorError(err) { 116 | /*eslint no-console: ["error", { allow: ["warn", "error"] }] */ 117 | console.error(`JSONEditor: ${err}`) /** TODO 500 page */ 118 | } 119 | 120 | handleEditorContentChange() { /** do nothing */ } 121 | 122 | handleClose() { 123 | const { close, } = this.props 124 | close() 125 | } 126 | 127 | handleCreate() { 128 | const { create, } = this.props 129 | const { editor, connectorName, } = this.state 130 | const configToBeCreated = editor.get() 131 | 132 | create({ 133 | [CreateEditorPayload.CONNECTOR_CONFIG]: configToBeCreated, 134 | [CreateEditorPayload.CONNECTOR_NAME]: connectorName, 135 | }) 136 | } 137 | 138 | handleTextFieldChange(event) { 139 | this.setState({ connectorName: event.target.value, }) 140 | } 141 | 142 | handleSelectFieldChange(event, key, value) { 143 | const { changeSelectedConnectorClass, } = this.props 144 | const { editor, } = this.state 145 | 146 | const connectorConfig = editor.get() 147 | 148 | if (connectorConfig && connectorConfig['connector.class']) { 149 | 150 | connectorConfig['connector.class'] = value 151 | editor.set(connectorConfig) 152 | } 153 | 154 | changeSelectedConnectorClass({ 155 | [CreateEditorPayload.SELECTED_CONNECTOR_CLASS]: value, 156 | }) 157 | } 158 | 159 | handleValidateConfig() { 160 | const { validateConnectorConfig, } = this.props 161 | const { editor, } = this.state 162 | 163 | const connectorConfig = editor.get() 164 | const connectorClass = connectorConfig['connector.class'] 165 | 166 | validateConnectorConfig({ 167 | [ConfigSchemaPayload.CONNECTOR_CLASS]: connectorClass, 168 | [ConfigSchemaPayload.CONNECTOR_CONFIG]: connectorConfig, 169 | }) 170 | } 171 | 172 | createTextFieldForConnectorName() { 173 | const { connectorName, } = this.state 174 | 175 | let errorText = '' 176 | 177 | if (connectorName === '') errorText = 'required' 178 | 179 | return () 186 | } 187 | 188 | createSelectFieldForConnectorClass() { 189 | const { availableConnectors, selectedConnectorClass, } = this.props 190 | 191 | const menuItems = availableConnectors.reduce((acc, c) => { 192 | return acc.concat([ 193 | , 194 | ]) 195 | }, []) 196 | 197 | return ( 198 | 204 | {menuItems} 205 | 206 | ) 207 | } 208 | 209 | createDialogTitle() { 210 | const { configSchema, errorMessages, } = this.props 211 | 212 | const emptySchema = configSchema === void 0 213 | const noError = errorMessages.length === 0 214 | 215 | const schemaChipLabel = (emptySchema) ? 'INVALID CLASS' : 'SCHEMA VALIDATION' 216 | const schemaChipLabelColor = (emptySchema) ? 217 | EditorTheme.titleNoSchemaChipLabelColor : EditorTheme.titleSchemaChipLabelColor 218 | const schemaChipBgColor = (emptySchema) ? 219 | EditorTheme.titleNoSchemaChipBgColor : EditorTheme.titleSchemaChipBgColor 220 | 221 | const errorChipLabel = (noError) ? 'NO ERROR' : `ERROR (${errorMessages.length})` 222 | const errorChipLabelColor = (noError) ? 223 | EditorTheme.titleNoErrorChipLabelColor : EditorTheme.titleErrorChipLabelColor 224 | const errorChipBgColor = (noError) ? 225 | EditorTheme.titleNoErrorChipBgColor : EditorTheme.titleErrorChipBgColor 226 | 227 | const errorChip = (emptySchema) ? null : ( 228 | 232 | {errorChipLabel} 233 | 234 | ) 235 | 236 | return ( 237 |
238 | Create New Connector 239 | 243 | {schemaChipLabel} 244 | 245 | {errorChip} 246 |
247 |
248 | ) 249 | } 250 | 251 | render() { 252 | const { configSchema, } = this.props 253 | 254 | const validationButtonDisabled = (configSchema === void 0) 255 | const validationButtonLabelStyle = (configSchema === void 0) ? 256 | dialogStyle.disabledValidateButtonLabel : dialogStyle.validateButtonLabel 257 | 258 | const buttons = [ 259 | , 265 | , 270 | , 274 | ] 275 | 276 | return ( 277 | 283 |
284 | {this.createTextFieldForConnectorName()} 285 |
286 |
287 | {this.createSelectFieldForConnectorClass()} 288 |
289 |
290 |
291 |
292 |
293 | ) 294 | } 295 | } 296 | -------------------------------------------------------------------------------- /kafkalot-ui/src/components/ConnectorPage/ConnectorHeader/index.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, } from 'react' 2 | import RaisedButton from 'material-ui/RaisedButton' 3 | import {Popover, PopoverAnimationVertical,} from 'material-ui/Popover' 4 | import SelectField from 'material-ui/SelectField' 5 | import MenuItem from 'material-ui/MenuItem' 6 | 7 | import { Payload as ConnectorListPayload, ConnectorProperty, } from '../../../reducers/ConnectorReducer/ConnectorListState' 8 | import { Property as PaginatorProperty, } from '../../../reducers/ConnectorReducer/PaginatorState' 9 | import { AvailableSorters, } from '../../../constants/Sorter' 10 | import * as Page from '../../../constants/Page' 11 | 12 | import { ConnectorCommand, } from '../../../constants/ConnectorCommand' 13 | 14 | import Filter from '../../Common/Filter' 15 | import Selector from '../../Common/Selector' 16 | import * as style from './style' 17 | 18 | export default class ConnectorHeader extends React.Component { 19 | static propTypes = { 20 | connectors: PropTypes.array.isRequired, 21 | sorter: PropTypes.string.isRequired, 22 | filterKeyword: PropTypes.string.isRequired, 23 | itemCountPerPage: PropTypes.number.isRequired, 24 | availableItemCountsPerPage: PropTypes.array.isRequired, 25 | 26 | changeSorter: PropTypes.func.isRequired, 27 | changeFilterKeyword: PropTypes.func.isRequired, 28 | changePageItemCount: PropTypes.func.isRequired, 29 | 30 | startConnector: PropTypes.func.isRequired, 31 | stopConnector: PropTypes.func.isRequired, 32 | restartConnector: PropTypes.func.isRequired, 33 | pauseConnector: PropTypes.func.isRequired, 34 | resumeConnector: PropTypes.func.isRequired, 35 | openCreateEditor: PropTypes.func.isRequired, 36 | openRemoveDialog: PropTypes.func.isRequired, 37 | } 38 | 39 | constructor(props) { 40 | super(props) 41 | 42 | this.state = { 43 | currentCommand: ConnectorCommand.START, 44 | } 45 | 46 | this.handleFilterChange = this.handleFilterChange.bind(this) 47 | this.handleSorterChange = this.handleSorterChange.bind(this) 48 | this.handlePageItemCountChange = this.handlePageItemCountChange.bind(this) 49 | this.handleCreate = this.handleCreate.bind(this) 50 | 51 | this.handleCommandChange = this.handleCommandChange.bind(this) 52 | this.handleCommandExecute = this.handleCommandExecute.bind(this) 53 | } 54 | 55 | handleCommandChange(event, key, payload) { 56 | this.setState({ currentCommand: payload,}) // eslint-disable-line react/no-set-state 57 | } 58 | 59 | handleCommandExecute() { 60 | const { 61 | startConnector, 62 | stopConnector, 63 | restartConnector, 64 | pauseConnector, 65 | resumeConnector, 66 | } = this.props 67 | const { currentCommand, } = this.state 68 | 69 | switch (currentCommand) { 70 | case ConnectorCommand.START: startConnector(); break 71 | case ConnectorCommand.STOP: stopConnector(); break 72 | case ConnectorCommand.RESTART: restartConnector(); break 73 | case ConnectorCommand.PAUSE: pauseConnector(); break 74 | case ConnectorCommand.RESUME: resumeConnector(); break 75 | default: 76 | } 77 | } 78 | 79 | handleCreate() { 80 | const { openCreateEditor, } = this.props 81 | openCreateEditor() 82 | } 83 | 84 | handleFilterChange(filterKeyword) { 85 | const { changeFilterKeyword, } = this.props 86 | const payload = { [ConnectorListPayload.FILTER_KEYWORD]: filterKeyword, } 87 | changeFilterKeyword(payload) 88 | } 89 | 90 | handleSorterChange(strategy) { 91 | const { changeSorter, } = this.props 92 | 93 | changeSorter({ [ConnectorListPayload.SORTER]: strategy, }) 94 | } 95 | 96 | handlePageItemCountChange(event, key, payload) { 97 | const { changePageItemCount, } = this.props 98 | 99 | changePageItemCount({ [PaginatorProperty.ITEM_COUNT_PER_PAGE]: payload, }) 100 | } 101 | 102 | createCommandButtonsDOM() { 103 | const { openRemoveDialog, } = this.props 104 | const { currentCommand, } = this.state 105 | 106 | return ( 107 |
108 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 122 | 123 | 126 | 127 | 130 | 131 |
132 | 133 |
134 | ) 135 | } 136 | 137 | createSelectorDOM() { 138 | const { 139 | sorter, filterKeyword, connectors, 140 | itemCountPerPage, availableItemCountsPerPage, 141 | } = this.props 142 | 143 | const checkedConnectorCount = connectors.filter(c => c[ConnectorProperty.CHECKED]).length 144 | 145 | const filterLabel = (filterKeyword !== '') ? 146 | `Filter: ${filterKeyword} (${checkedConnectorCount} selected)` : 147 | `Insert filter (${checkedConnectorCount} selected)` 148 | 149 | const itemCountPerPageMenuItems = availableItemCountsPerPage.reduce((acc, i) => { 150 | return acc.concat([ 151 | , 152 | ]) 153 | }, []) 154 | 155 | return ( 156 |
157 | 160 | 161 | 167 | {itemCountPerPageMenuItems} 168 | 169 | 170 | 177 | 178 |
179 |
180 | ) 181 | } 182 | 183 | render() { 184 | /** filter, paginator count selector, sorter, */ 185 | const selectorDOM = this.createSelectorDOM() 186 | 187 | /** command buttons */ 188 | const commandButtonsDOM = this.createCommandButtonsDOM() 189 | 190 | return ( 191 |
192 |
{Page.ConnectorPageTitle}
193 | {selectorDOM} 194 | {commandButtonsDOM} 195 |
196 | ) 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /kafkalot-ui/src/components/ConnectorPage/ConnectorHeader/style.js: -------------------------------------------------------------------------------- 1 | const CommandButtonLeftMargin = 15 2 | 3 | export const CommandButton = { 4 | RightButton: { marginLeft: CommandButtonLeftMargin, float: 'right', }, 5 | ExecuteButton: { marginLeft: CommandButtonLeftMargin, }, 6 | ExecuteButtonColor: '#546e7a', 7 | ButtonLabel: { fontWeight: 300, color: '#f5f5f5', }, 8 | Container: { fontSize: 15, fontWeight: 300, marginTop: 30, }, 9 | Selector: { width: 100, top: 5, }, 10 | } 11 | 12 | export const Selector = { 13 | ConnectorSelector: { float: 'right', width: 100, marginRight: 15, }, 14 | PageItemCountSelector: { float: 'right', width: 100, marginRight: 25, }, 15 | 16 | SelectorLabel: { fontWeight: 300, }, 17 | SelectorFloatingLabel: { color: 'red', }, 18 | FilterInput: { fontWeight: 300, fontSize: 14, }, 19 | } 20 | 21 | export const title = { fontSize: 30, fontWeight: 100, marginTop: 10, } 22 | 23 | -------------------------------------------------------------------------------- /kafkalot-ui/src/components/ConnectorPage/ConnectorList/ConnectorListItem.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, } from 'react' 2 | 3 | import Divider from 'material-ui/Divider' 4 | import CircularProgress from 'material-ui/CircularProgress' 5 | import FlatButton from 'material-ui/FlatButton' 6 | import Checkbox from 'material-ui/Checkbox' 7 | import { grey400, darkBlack, lightBlack, } from 'material-ui/styles/colors' 8 | import { List, ListItem, } from 'material-ui/List' 9 | 10 | import { 11 | Payload as ConfigEditorPayload, 12 | } from '../../../reducers/ConnectorReducer/ConfigEditorState' 13 | 14 | import { 15 | isRunningTask, ConnectorProperty, 16 | isRunningState, isUnassignedState, 17 | isPausedState, isFailedState, 18 | isRegisteredState, isDisabledState, isWorkingState, 19 | } from '../../../reducers/ConnectorReducer/ConnectorListState' 20 | 21 | import * as style from './style' 22 | import * as Theme from '../../../constants/Theme' 23 | import { ConnectorTask, } from './ConnectorTask' 24 | import { ListItemColumn, ListItemLastColumn, } from './ListItemColumn' 25 | 26 | export class ConnectorListItem extends React.Component { 27 | 28 | static propTypes = { 29 | tasks: PropTypes.array.isRequired, 30 | name: PropTypes.string.isRequired, 31 | state: PropTypes.string.isRequired, 32 | uptime: PropTypes.string.isRequired, 33 | tags: PropTypes.array.isRequired, 34 | checked: PropTypes.bool.isRequired, 35 | 36 | setConnectorChecked: PropTypes.func.isRequired, 37 | fetchConnector: PropTypes.func.isRequired, 38 | openConfigEditor: PropTypes.func.isRequired, 39 | disableConnector: PropTypes.func.isRequired, 40 | enableConnector: PropTypes.func.isRequired, 41 | restartTask: PropTypes.func.isRequired, 42 | } 43 | 44 | constructor(props) { 45 | super(props) 46 | 47 | this.handleConfigButtonClick = this.handleConfigButtonClick.bind(this) 48 | this.handleCheckboxClick = this.handleCheckboxClick.bind(this) 49 | this.handleEnableButtonClick = this.handleEnableButtonClick.bind(this) 50 | } 51 | 52 | handleCheckboxClick() { 53 | const { name, checked, setConnectorChecked, } = this.props 54 | 55 | setConnectorChecked({ 56 | [ConnectorProperty.NAME]: name, 57 | [ConnectorProperty.CHECKED]: !checked, 58 | }) 59 | } 60 | 61 | handleConfigButtonClick() { 62 | const { name, openConfigEditor, } = this.props 63 | 64 | openConfigEditor({ 65 | [ConfigEditorPayload.NAME]: name, 66 | }) 67 | } 68 | 69 | handleEnableButtonClick() { 70 | const { name, state, enableConnector, disableConnector, } = this.props 71 | 72 | /** branch enable/disable button using `state` */ 73 | if (isRegisteredState(state)) { 74 | disableConnector({ [ConnectorProperty.NAME]: name, }) 75 | } else if (isDisabledState(state)) { 76 | enableConnector({ [ConnectorProperty.NAME]: name, }) 77 | } 78 | } 79 | 80 | 81 | createTaskText() { 82 | const { state: connectorState, tasks, } = this.props 83 | 84 | if (!isWorkingState(connectorState)) return '' 85 | else if (tasks.length === 0) return '' 86 | else { 87 | const maxTaskCount = tasks.length 88 | const runningTaskCount = tasks.reduce((acc, task) => { 89 | if (isRunningTask(task)) return acc + 1 90 | else return acc 91 | }, 0) 92 | return `${runningTaskCount} / ${maxTaskCount}` 93 | } 94 | } 95 | 96 | createEnableButton() { 97 | const { state, } = this.props 98 | 99 | const enableButtonActive = !isWorkingState(state) 100 | const enableButtonText = (isRegisteredState(state)) ? 'DISABLE' : 101 | (isDisabledState(state)) ? 'ENABLE' : '' 102 | 103 | const enableButtonDOM = (enableButtonActive) ? ( 104 | 109 | ) : null 110 | 111 | return enableButtonDOM 112 | } 113 | 114 | createStateIcon(connectorState) { 115 | const { state, } = this.props 116 | const workingOnConnector = (isWorkingState(state)) 117 | 118 | let color = Theme.ConnectorStateColor.Disabled 119 | 120 | if (isRunningState(connectorState)) 121 | color = Theme.ConnectorStateColor.Running 122 | else if (isUnassignedState(connectorState)) 123 | color = Theme.ConnectorStateColor.Unassigned 124 | else if (isPausedState(connectorState)) 125 | color = Theme.ConnectorStateColor.Paused 126 | else if (isFailedState(connectorState)) 127 | color = Theme.ConnectorStateColor.Failed 128 | else if (isRegisteredState(connectorState)) 129 | color = Theme.ConnectorStateColor.Registered 130 | else 131 | color = Theme.ConnectorStateColor.Disabled 132 | 133 | if (workingOnConnector) { 134 | return () 135 | } else { 136 | return () 137 | } 138 | } 139 | 140 | render() { 141 | const { name, state, checked, tasks, uptime, restartTask, } = this.props 142 | const taskElems = (!tasks) ? [] : 143 | tasks.reduce((elems, task) => { 144 | const elem = () 147 | const divider = 148 | return elems.concat([ divider, elem, ]) 149 | }, []) 150 | 151 | return ( 152 | 155 | 156 | 157 | 158 | {this.createStateIcon(state)} 159 | {state} 160 | {this.createTaskText()} 161 | {name} 162 | {uptime} 163 | 164 | {this.createEnableButton()} 165 | 170 | 171 | 172 | 173 | ) 174 | } 175 | } 176 | 177 | -------------------------------------------------------------------------------- /kafkalot-ui/src/components/ConnectorPage/ConnectorList/ConnectorTask.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, } from 'react' 2 | 3 | import CircularProgress from 'material-ui/CircularProgress' 4 | import FlatButton from 'material-ui/FlatButton' 5 | import Popover from 'material-ui/Popover' 6 | import TextField from 'material-ui/TextField' 7 | import { grey400, darkBlack, lightBlack, } from 'material-ui/styles/colors' 8 | import { List, ListItem, } from 'material-ui/List' 9 | 10 | import { ListItemColumn, ListItemLastColumn, } from './ListItemColumn' 11 | 12 | import { 13 | isRunningState, isUnassignedState, isPausedState, 14 | isFailedState, isRegisteredState, 15 | ConnectorProperty, ConnectorTaskProperty, 16 | } from '../../../reducers/ConnectorReducer/ConnectorListState' 17 | 18 | import * as style from './style' 19 | import * as Theme from '../../../constants/Theme' 20 | 21 | export class ConnectorTask extends React.Component { 22 | static propTypes = { 23 | connectorName: PropTypes.string.isRequired, 24 | id: PropTypes.number.isRequired, 25 | worker_id: PropTypes.string.isRequired, 26 | state: PropTypes.string.isRequired, 27 | trace: PropTypes.string, /** optional */ 28 | 29 | restartTask: PropTypes.func.isRequired, 30 | } 31 | 32 | static createStateIcon(taskState) { 33 | const isRunning = isRunningState(taskState) 34 | const mode = (isRunning) ? 'indeterminate' : 'determinate' 35 | const value = (isRunning) ? 0 : 100 36 | let color = Theme.ConnectorStateColor.Disabled 37 | 38 | if (isRunningState(taskState)) 39 | color = Theme.ConnectorStateColor.Running 40 | else if (isUnassignedState(taskState)) 41 | color = Theme.ConnectorStateColor.Unassigned 42 | else if (isPausedState(taskState)) 43 | color = Theme.ConnectorStateColor.Paused 44 | else if (isFailedState(taskState)) 45 | color = Theme.ConnectorStateColor.Failed 46 | else if (isRegisteredState(taskState)) 47 | color = Theme.ConnectorStateColor.Registered 48 | else 49 | color = Theme.ConnectorStateColor.Disabled 50 | 51 | return ( 52 | 54 | ) 55 | } 56 | 57 | constructor(props) { 58 | super(props) 59 | 60 | this.state = { tracePopoverOpened: false, } 61 | 62 | this.handleRestartButtonClicked = this.handleRestartButtonClicked.bind(this) 63 | this.handleTraceButtonClicked = this.handleTraceButtonClicked.bind(this) 64 | this.handleTracePopoverClose = this.handleTracePopoverClose.bind(this) 65 | } 66 | 67 | handleRestartButtonClicked() { 68 | const { connectorName, id, restartTask, } = this.props 69 | 70 | restartTask({ 71 | [ConnectorProperty.NAME]: connectorName, 72 | [ConnectorTaskProperty.ID]: id, 73 | }) 74 | } 75 | 76 | handleTraceButtonClicked(event) { 77 | event.preventDefault() 78 | 79 | this.setState({ 80 | tracePopoverOpened: true, 81 | tracePopoverOpenedAnchorEl: event.currentTarget, 82 | }) 83 | } 84 | 85 | handleTracePopoverClose() { 86 | this.setState({ tracePopoverOpened: false, }) 87 | } 88 | 89 | render() { 90 | const { id, worker_id, state, trace, } = this.props 91 | const { tracePopoverOpened, tracePopoverOpenedAnchorEl, } = this.state 92 | 93 | const stateIcon = ConnectorTask.createStateIcon(state) 94 | const traceButtonActive = (trace && trace.length > 0) 95 | 96 | return ( 97 | 98 | {id} 99 | {stateIcon} 100 | {state} 101 | {worker_id} 102 | 103 | 106 | 110 | 117 |
118 | 119 |
120 |
121 |
122 | 123 |
124 | ) 125 | } 126 | } -------------------------------------------------------------------------------- /kafkalot-ui/src/components/ConnectorPage/ConnectorList/ListItemColumn.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, } from 'react' 2 | 3 | export class ListItemLastColumn extends React.Component { 4 | render() { 5 | return (
) 6 | } 7 | } 8 | 9 | export class ListItemColumn extends React.Component { 10 | static propTypes = { 11 | style: PropTypes.object, 12 | children: PropTypes.node, 13 | } 14 | 15 | static defaultStyle = { 16 | display: 'inline-block', 17 | verticalAlign: 'middle', 18 | fontWeight: 300, 19 | fontSize: 15, 20 | } 21 | 22 | render() { 23 | const { style, children, } = this.props 24 | const mergedStyle = (style) ? 25 | Object.assign({}, ListItemColumn.defaultStyle, style) : ListItemColumn.defaultStyle 26 | 27 | return ( 28 | {children} 29 | ) 30 | } 31 | } -------------------------------------------------------------------------------- /kafkalot-ui/src/components/ConnectorPage/ConnectorList/index.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, } from 'react' 2 | 3 | import Checkbox from 'material-ui/Checkbox' 4 | import Divider from 'material-ui/Divider' 5 | import Paper from 'material-ui/Paper' 6 | import { List, ListItem, } from 'material-ui/List' 7 | 8 | import { ListItemColumn, } from './ListItemColumn' 9 | import { ConnectorListItem, } from './ConnectorListItem' 10 | import { ConnectorListProperty, } from '../../../reducers/ConnectorReducer/ConnectorListState' 11 | import { Property as PaginatorProperty, } from '../../../reducers/ConnectorReducer/PaginatorState' 12 | 13 | import * as style from './style' 14 | 15 | export default class ConnectorList extends React.Component { 16 | static propTypes = { 17 | connectors: PropTypes.array.isRequired, 18 | itemOffset: PropTypes.number.isRequired, 19 | itemCountPerPage: PropTypes.number.isRequired, 20 | 21 | actions: PropTypes.object.isRequired, 22 | tableHeaderChecked: PropTypes.bool.isRequired, 23 | toggleCurrentPageCheckboxes: PropTypes.func.isRequired, 24 | } 25 | 26 | constructor(props) { 27 | super(props) 28 | 29 | this.handleHeaderCheckboxClick = this.handleHeaderCheckboxClick.bind(this) 30 | } 31 | 32 | handleHeaderCheckboxClick() { 33 | const { 34 | toggleCurrentPageCheckboxes, tableHeaderChecked, 35 | itemCountPerPage, itemOffset, 36 | } = this.props 37 | 38 | toggleCurrentPageCheckboxes({ 39 | [ConnectorListProperty.TABLE_HEADER_CHECKED]: !tableHeaderChecked, /** invert */ 40 | [PaginatorProperty.ITEM_OFFSET]: itemOffset, 41 | [PaginatorProperty.ITEM_COUNT_PER_PAGE]: itemCountPerPage, 42 | }) 43 | } 44 | 45 | createTableHeader() { 46 | const { tableHeaderChecked, } = this.props 47 | 48 | return( 49 | 50 | 51 | 52 | 54 | 55 | 56 | STATUS 57 | TASKS 58 | NAME 59 | UPTIME 60 | 61 | 62 | ) 63 | } 64 | 65 | createTableBody() { 66 | const { connectors, actions, } = this.props 67 | 68 | const items = [] 69 | 70 | for (let i = 0; i < connectors.length; i++) { 71 | const connector = connectors[i] 72 | const item = ( 73 | ) 81 | 82 | items.push(item) 83 | 84 | if (i < connectors.length - 1) 85 | items.push() 86 | } 87 | 88 | return( 89 | 90 | {items} 91 | 92 | ) 93 | } 94 | 95 | render() { 96 | return ( 97 | 98 | {this.createTableHeader()} 99 | 100 | {this.createTableBody()} 101 | 102 | ) 103 | } 104 | } 105 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /kafkalot-ui/src/components/ConnectorPage/ConnectorList/style.js: -------------------------------------------------------------------------------- 1 | import * as Theme from '../../../constants/Theme' 2 | 3 | export const list = { marginTop: 20, } 4 | export const TableContainer = { marginTop: 30, marginBottom: 50, } 5 | export const HeaderContainer = { background: Theme.ConnectorList.Header, } 6 | export const BodyContainer = { paddingTop: 0, paddingBottom: 0, } 7 | 8 | const HeaderFontWeight = 400 9 | 10 | export const ItemHeaderColumn = { 11 | container: { padding: '10px 10px 10px 10px', }, 12 | checkbox: { marginLeft: 15, }, 13 | stateIcon: { width: 50, }, 14 | stateText: { marginLeft: 15, width: 120, fontWeight: HeaderFontWeight, }, 15 | taskText: { width: 120, fontWeight: HeaderFontWeight, }, 16 | name: { width: 280, fontWeight: HeaderFontWeight, }, 17 | uptime: { width: 150, fontWeight: HeaderFontWeight, }, 18 | } 19 | 20 | const CommandButtonMarginLeft = 10 21 | 22 | export const ItemBodyColumn = { 23 | container: { padding: '3px 13px 3px 13px', }, 24 | checkbox: { marginLeft: 15, }, 25 | stateIcon: { width: ItemHeaderColumn.stateIcon.width, textAlign: 'center', }, 26 | stateText: { marginLeft: ItemHeaderColumn.stateText.marginLeft, width: ItemHeaderColumn.stateText.width, }, 27 | taskText: { width: ItemHeaderColumn.taskText.width, }, 28 | name: { width: ItemHeaderColumn.name.width, }, 29 | uptime: { width: ItemHeaderColumn.uptime.width, }, 30 | commandButtons: { marginTop: 8, marginRight: 35, float: 'right', }, 31 | commandButtonLabel: { fontWeight: 300, }, 32 | commandButton: { marginLeft: CommandButtonMarginLeft, }, 33 | } 34 | 35 | export const TaskColumn = { 36 | container: { marginLeft: 55, padding: '2px 10px 2px 10px', }, 37 | taskId: { width: 50, }, 38 | stateIcon: { width: 50, textAlign: 'center', }, 39 | stateText: { marginLeft: 20, width: 120, }, 40 | workerId: { marginLeft: 30, width: 150, }, 41 | commandButtons: { marginTop: 8, marginRight: 35, float: 'right', }, 42 | traceButton: { 43 | marginLeft: CommandButtonMarginLeft, 44 | color: Theme.ConnectorTaskItem.TraceButton, 45 | }, 46 | } 47 | 48 | export const TaskTracePopover = { 49 | contentContainer: { padding: 30, }, 50 | } 51 | 52 | -------------------------------------------------------------------------------- /kafkalot-ui/src/components/ConnectorPage/RemoveDialog.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, } from 'react' 2 | 3 | import FlatButton from 'material-ui/FlatButton' 4 | import Dialog from 'material-ui/Dialog' 5 | import {List, ListItem, } from 'material-ui/List' 6 | 7 | import { Property as RemoveDialogProperty, } from '../../reducers/ConnectorReducer/RemoveDialogState' 8 | 9 | const dialogStyle = { 10 | title: { fontWeight: 300, fontSize: 18, padding: '50px 0px 0px 20px', }, 11 | list: { marginTop: 30, marginBottom: 30, }, 12 | button: { marginRight: 15, marginBottom: 15, }, 13 | buttonLabel: { fontWeight: 300, }, 14 | } 15 | 16 | export default class RemoveDialog extends React.Component { 17 | static propTypes = { 18 | connectorNames: PropTypes.array.isRequired, 19 | closeRemoveDialog: PropTypes.func.isRequired, 20 | removeConnector: PropTypes.func.isRequired, 21 | } 22 | 23 | constructor(props) { 24 | super(props) 25 | 26 | this.handleRemove = this.handleRemove.bind(this) 27 | } 28 | 29 | handleRemove() { 30 | const { connectorNames, removeConnector, } = this.props 31 | 32 | removeConnector({ 33 | [RemoveDialogProperty.CONNECTOR_NAMES]: connectorNames, 34 | }) 35 | } 36 | 37 | createTitle() { 38 | const { connectorNames, } = this.props 39 | 40 | const connectorCountToBeRemoved = connectorNames.length 41 | const postfix = (connectorCountToBeRemoved < 2) ? '' : 's' 42 | const titleText = `Remove ${connectorCountToBeRemoved} connector${postfix}` 43 | 44 | return ( 45 |
{titleText}
46 | ) 47 | } 48 | 49 | createConnectorList() { 50 | const { connectorNames, } = this.props 51 | 52 | const items = [] 53 | for (let i = 0; i < connectorNames.length; i ++) { 54 | items.push() 55 | } 56 | 57 | return ( {items} ) 58 | } 59 | 60 | render() { 61 | const { closeRemoveDialog, } = this.props 62 | 63 | const buttons = [ 64 | , 68 | , 72 | ] 73 | 74 | return ( 75 | 81 | {this.createConnectorList()} 82 | 83 | ) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /kafkalot-ui/src/constants/Config.js: -------------------------------------------------------------------------------- 1 | /** injected by webpack, see also `tools/config.js` */ 2 | 3 | export const ENV_DEV = process.env.ENV_DEV || '' 4 | export const ENV_PROD = process.env.ENV_PROD || '' 5 | export const NODE_ENV = process.env.NODE_ENV || '' 6 | 7 | const envStorages = process.env.STORAGES 8 | export const STORAGES = (envStorages === void 0) ? [] : envStorages 9 | export const TITLE = process.env.TITLE || '' 10 | 11 | -------------------------------------------------------------------------------- /kafkalot-ui/src/constants/ConnectorCommand.js: -------------------------------------------------------------------------------- 1 | export const ConnectorCommand = { 2 | START: 'START', 3 | STOP: 'STOP', 4 | RESTART: 'RESTART', 5 | PAUSE: 'PAUSE', 6 | RESUME: 'RESUME', 7 | } 8 | -------------------------------------------------------------------------------- /kafkalot-ui/src/constants/ConnectorState.js: -------------------------------------------------------------------------------- 1 | export const ConnectorState = { 2 | RUNNING: 'RUNNING', 3 | UNASSIGNED: 'UNASSIGNED', 4 | PAUSED: 'PAUSED', 5 | FAILED: 'FAILED', 6 | REGISTERED: 'REGISTERED', 7 | DISABLED: 'DISABLED', 8 | } 9 | -------------------------------------------------------------------------------- /kafkalot-ui/src/constants/EditorMode.js: -------------------------------------------------------------------------------- 1 | export const JsonEditorMode = { 2 | TREE: 'tree', VIEW: 'view', CODE: 'code', 3 | } 4 | -------------------------------------------------------------------------------- /kafkalot-ui/src/constants/Error.js: -------------------------------------------------------------------------------- 1 | export const Code = { 2 | DUPLICATE_NAME: 'DUPLICATE_NAME', 3 | EMPTY_NAME: 'EMPTY_NAME', 4 | EMPTY_CONFIG: 'EMPTY_CONFIG', 5 | CANNOT_CHANGE_NAME: 'CANNOT_CHANGE_NAME', 6 | FOUND_DUPLICATED_CONNECTORS: 'FOUND_DUPLICATED_CONNECTORS', 7 | NO_SUCH_CONNECTOR: 'NO_SUCH_CONNECTOR', 8 | CANNOT_UPDATE_NON_REGISTERED_CONNECTOR: 'CANNOT_UPDATE_NON_REGISTERED_CONNECTOR', 9 | 10 | CANNOT_OPEN_REMOVE_DIALOG: 'CANNOT_OPEN_REMOVE_DIALOG', 11 | CONNECTOR_COMMAND_FAILED: 'CONNECTOR_COMMAND_FAILED', 12 | CANNOT_REMOVE_CONNECTOR: 'CANNOT_REMOVE_CONNECTOR', 13 | NO_SELECTED_CONNECTORS: 'NO_SELECTED_CONNECTORS', 14 | 15 | INVALID_TASKS: 'INVALID_TASKS', 16 | 17 | CANNOT_FETCH_ALL_CONNECTORS: 'CANNOT_FETCH_ALL_CONNECTORS', 18 | CANNOT_REMOVE_ALL_SELECTED_CONNECTORS: 'CANNOT_REMOVE_ALL_SELECTED_CONNECTORS', 19 | CANNOT_CONVERT_CONNECT_CONFIG_TYPE: 'CANNOT_CONVERT_CONNECT_CONFIG_TYPE', 20 | CANNOT_CONVERT_CONNECT_VALIDATION_RESULT: 'CANNOT_CONVERT_CONNECT_VALIDATION_RESULT', 21 | CANNOT_FIND_CONNECTOR_PLUGINS: 'CANNOT_FIND_CONNECTOR_PLUGINS', 22 | CONNECTOR_CONFIG_HAS_NO_CONNECTOR_CLASS_FIELD: 'CONNECTOR_CONFIG_HAS_NO_CONNECTOR_CLASS_FIELD', 23 | } 24 | -------------------------------------------------------------------------------- /kafkalot-ui/src/constants/Page.js: -------------------------------------------------------------------------------- 1 | 2 | export const MainPageRouting = 'kafkalot' 3 | export const ConnectorPageRouting = 'connectors' 4 | export const ConnectorPageTitle = 'Connectors' 5 | -------------------------------------------------------------------------------- /kafkalot-ui/src/constants/Sorter.js: -------------------------------------------------------------------------------- 1 | import { ConnectorState, } from './ConnectorState' 2 | 3 | export const SorterType = { 4 | CHECKED: 'CHECKED', 5 | UNCHECKED: 'UNCHECKED', 6 | } 7 | 8 | export const AvailableSorters = [ 9 | ConnectorState.RUNNING, 10 | ConnectorState.UNASSIGNED, 11 | ConnectorState.PAUSED, 12 | ConnectorState.FAILED, 13 | ConnectorState.REGISTERED, 14 | ConnectorState.DISABLED, 15 | SorterType.CHECKED, 16 | SorterType.UNCHECKED, 17 | ] 18 | -------------------------------------------------------------------------------- /kafkalot-ui/src/constants/State.js: -------------------------------------------------------------------------------- 1 | export const ROOT = { 2 | CONNECTOR: 'connector', 3 | ROUTING: 'routing', 4 | } 5 | 6 | export const CONNECTOR = { 7 | CONNECTOR_LIST: 'connectorList', 8 | PAGINATOR: 'paginator', 9 | FILTER: 'filterKeyword', 10 | CONFIRM_DIALOG: 'confirmDialog', 11 | SORTER: 'sorter', 12 | STORAGE_SELECTOR: 'storageSelector', 13 | 14 | CONFIG_SCHEMA: 'configSchema', 15 | CONFIG_EDITOR: 'configEditor', 16 | CREATE_EDITOR: 'createEditor', 17 | REMOVE_DIALOG: 'removeDialog', 18 | SNACKBAR: 'snackbar', 19 | } 20 | -------------------------------------------------------------------------------- /kafkalot-ui/src/constants/Theme.js: -------------------------------------------------------------------------------- 1 | export const ThemeDefaultColor = '#546e7a' 2 | export const White = '#f5f5f5' 3 | 4 | export const NavBarColors = { 5 | backgroundColor: ThemeDefaultColor, 6 | textColor: White, 7 | iconMenuColor: White, 8 | } 9 | 10 | export const ClosableSnackbar = { 11 | backgroundColor: ThemeDefaultColor, 12 | } 13 | 14 | export const ConnectorList = { 15 | Header: White, 16 | } 17 | 18 | export const ConnectorTaskItem = { 19 | TraceButton: '#607d8b', 20 | } 21 | 22 | export const ConnectorStateColor = { 23 | Running: '#00bcd4', 24 | Failed: '#e53935', 25 | Paused: '#ffc107', 26 | Unassigned: '#ce93d8', 27 | Registered: '#4db6ac', 28 | Disabled: '#b0bec5', 29 | } 30 | 31 | export const ConnectorEditor = { 32 | validationButton: ThemeDefaultColor, 33 | disabledValidationButton: 'rgba(0, 0, 0, 0.298039)', 34 | titleSchemaChipBgColor: '#039be5', /** light-blue darken-1 */ 35 | titleSchemaChipLabelColor: White, 36 | titleNoSchemaChipBgColor: '#f4511e', /** deep-orange darken-1 */ 37 | titleNoSchemaChipLabelColor: White, 38 | titleErrorChipBgColor: '#e53935', /** #e53935 red darken-1 */ 39 | titleErrorChipLabelColor: White, 40 | titleNoErrorChipBgColor: '#546e7a', /** blue-grey darken-1 */ 41 | titleNoErrorChipLabelColor: White, 42 | } 43 | 44 | -------------------------------------------------------------------------------- /kafkalot-ui/src/containers/ConnectorPage/index.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, } from 'react' 2 | import { Link, } from 'react-router' 3 | import { connect, } from 'react-redux' 4 | import { bindActionCreators, } from 'redux' 5 | 6 | import ConnectorList from '../../components/ConnectorPage/ConnectorList' 7 | import ConnectorHeader from '../../components/ConnectorPage/ConnectorHeader' 8 | import Paginator from '../../components/Common/Paginator' 9 | import RemoveDialog from '../../components/ConnectorPage/RemoveDialog' 10 | import { Property as PaginatorProperty, } from '../../reducers/ConnectorReducer/PaginatorState' 11 | import Snackbar from '../../components/Common/ClosableSnackbar' 12 | 13 | import { ConnectorProperty, ConnectorListProperty, } from '../../reducers/ConnectorReducer/ConnectorListState' 14 | 15 | import ConfigEditor from '../../components/ConnectorPage/ConnectorConfigEditor' 16 | import CreateEditor from '../../components/ConnectorPage/ConnectorCreateEditor' 17 | 18 | import { ROOT, CONNECTOR, } from '../../constants/State' 19 | import Actions from '../../actions' 20 | import * as style from './style' 21 | 22 | class ConnectorPage extends React.Component { 23 | static propTypes = { 24 | actions: PropTypes.object.isRequired, 25 | connectors: PropTypes.array.isRequired, 26 | paginator: PropTypes.object.isRequired, 27 | filterKeyword: PropTypes.string.isRequired, 28 | sorter: PropTypes.string.isRequired, 29 | tableHeaderChecked: PropTypes.bool.isRequired, 30 | 31 | configSchema: PropTypes.object.isRequired, 32 | configEditor: PropTypes.object.isRequired, 33 | createEditor: PropTypes.object.isRequired, 34 | removeDialog: PropTypes.object.isRequired, 35 | snackbar: PropTypes.object.isRequired, 36 | } 37 | 38 | constructor(props) { 39 | super(props) 40 | 41 | this.handlePageOffsetChange = this.handlePageOffsetChange.bind(this) 42 | } 43 | 44 | handlePageOffsetChange(newPageOffset) { 45 | const { actions, } = this.props 46 | const { changePageOffset, } = actions 47 | 48 | changePageOffset({ [PaginatorProperty.PAGE_OFFSET]: newPageOffset, }) 49 | } 50 | 51 | createSnackbarAndDialogs() { 52 | const { 53 | actions, configSchema, configEditor, createEditor, removeDialog, snackbar, 54 | } = this.props 55 | 56 | /** 3. draw dialogs, snackbar */ 57 | const configEditorDOM = (configEditor.opened) ? 58 | () : null 62 | 63 | const createEditorDOM = (createEditor.opened) ? 64 | () : null 69 | 70 | const removeDialogDOM = (removeDialog.opened) ? 71 | () : null 74 | 75 | const snackbarDOM = () 76 | 77 | return ( 78 |
79 | {configEditorDOM} 80 | {createEditorDOM} 81 | {removeDialogDOM} 82 | {snackbarDOM} 83 |
84 | ) 85 | } 86 | 87 | render() { 88 | const { 89 | actions, connectors, paginator, filterKeyword, sorter, tableHeaderChecked, 90 | } = this.props 91 | 92 | const { 93 | itemCountPerPage, pageOffset, itemOffset, 94 | } = paginator 95 | 96 | /** 1. filter connectors */ 97 | const filtered = connectors.filter(connector => { 98 | if (filterKeyword === '') return true 99 | else if (connector[ConnectorProperty.CHECKED]) /** do not exclude checked connector to support incremental search */ 100 | return true 101 | else { 102 | const searchArea = JSON.stringify(connector) 103 | return (searchArea.includes(filterKeyword)) 104 | } 105 | }) 106 | 107 | /** 2. select connectors to be curated */ 108 | const sliced = filtered.slice(itemOffset, itemOffset + itemCountPerPage) 109 | 110 | return ( 111 |
112 | 129 | 130 | 136 | 137 |
138 | 143 |
144 | {this.createSnackbarAndDialogs()} 145 |
146 | ) 147 | } 148 | } 149 | 150 | function mapStateToProps(state) { 151 | return { 152 | connectors: state[ROOT.CONNECTOR][CONNECTOR.CONNECTOR_LIST][ConnectorListProperty.CONNECTORS], 153 | filterKeyword: state[ROOT.CONNECTOR][CONNECTOR.CONNECTOR_LIST][ConnectorListProperty.FILTER_KEYWORD], 154 | sorter: state[ROOT.CONNECTOR][CONNECTOR.CONNECTOR_LIST][ConnectorListProperty.SORTER], 155 | tableHeaderChecked: state[ROOT.CONNECTOR][CONNECTOR.CONNECTOR_LIST][ConnectorListProperty.TABLE_HEADER_CHECKED], 156 | paginator: state[ROOT.CONNECTOR][CONNECTOR.PAGINATOR], 157 | 158 | snackbar: state[ROOT.CONNECTOR][CONNECTOR.SNACKBAR], 159 | configSchema: state[ROOT.CONNECTOR][CONNECTOR.CONFIG_SCHEMA], 160 | configEditor: state[ROOT.CONNECTOR][CONNECTOR.CONFIG_EDITOR], 161 | createEditor: state[ROOT.CONNECTOR][CONNECTOR.CREATE_EDITOR], 162 | removeDialog: state[ROOT.CONNECTOR][CONNECTOR.REMOVE_DIALOG], 163 | } 164 | } 165 | 166 | function mapDispatchToProps(dispatch) { 167 | return { 168 | actions: bindActionCreators(Actions[ROOT.CONNECTOR], dispatch), 169 | } 170 | } 171 | 172 | export default connect( 173 | mapStateToProps, 174 | mapDispatchToProps 175 | )(ConnectorPage) 176 | 177 | -------------------------------------------------------------------------------- /kafkalot-ui/src/containers/ConnectorPage/style.js: -------------------------------------------------------------------------------- 1 | export const paginator = { 2 | fontWeight: 100, 3 | } 4 | -------------------------------------------------------------------------------- /kafkalot-ui/src/containers/MainPage/index.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, } from 'react' 2 | import { connect, } from 'react-redux' 3 | import { bindActionCreators, } from 'redux' 4 | 5 | import FontIcon from 'material-ui/FontIcon' 6 | import RaisedButton from 'material-ui/RaisedButton' 7 | 8 | import * as style from './style' 9 | 10 | /** responsible for drawing job summary */ 11 | class MainPage extends React.Component { 12 | static propTypes = { 13 | actions: PropTypes.object.isRequired, 14 | } 15 | 16 | 17 | render() { 18 | 19 | const title = ( 20 |
21 | Kafkalot 22 |
23 | ) 24 | 25 | const subTitle = ( 26 |
27 | Kafka Connect, Stream Dashboard 28 |
29 | ) 30 | 31 | const button = ( 32 | } 37 | /> 38 | ) 39 | 40 | return ( 41 |
42 |
43 | {title} 44 | {subTitle} 45 | {button} 46 |
47 |
48 | ) 49 | } 50 | } 51 | 52 | function mapStateToProps(state) { 53 | return { /** nothing to map */ } 54 | } 55 | 56 | function mapDispatchToProps(dispatch) { 57 | return { 58 | actions: bindActionCreators({}, dispatch), 59 | } 60 | } 61 | 62 | export default connect( 63 | mapStateToProps, 64 | mapDispatchToProps 65 | )(MainPage) 66 | -------------------------------------------------------------------------------- /kafkalot-ui/src/containers/MainPage/style.js: -------------------------------------------------------------------------------- 1 | export const container = { 2 | display: 'flex', 3 | justifyContent: 'center', 4 | alignItems: 'center', 5 | } 6 | 7 | export const subContainer = { 8 | position: 'absolute', 9 | top: '50%', 10 | left: '50%', 11 | transform: 'translate(-50%, -50%)', 12 | } 13 | 14 | export const title = { 15 | fontWeight: 300, 16 | fontSize: 45, 17 | } 18 | 19 | export const subTitle = { 20 | marginTop: 20, 21 | fontWeight: 300, 22 | fontSize: 20, 23 | } 24 | 25 | export const githubButton = { 26 | marginTop: 50, 27 | } 28 | -------------------------------------------------------------------------------- /kafkalot-ui/src/global.css: -------------------------------------------------------------------------------- 1 | /** disable materialize input box-shadow */ 2 | input[type=text]:focus:not:([read-only]) { 3 | box-shadow: none; 4 | } 5 | 6 | /** ignore materialize table width */ 7 | table { 8 | width: inherit; 9 | } 10 | 11 | /* dark styling of the editor */ 12 | div.jsoneditor, 13 | div.jsoneditor-menu { 14 | border-color: #4b4b4b; 15 | } 16 | div.jsoneditor-menu { 17 | background-color: #4b4b4b; 18 | } 19 | 20 | div.jsoneditor-tree, 21 | div.jsoneditor textarea.jsoneditor-text { 22 | background-color: #f5f5f5; 23 | color: #ffffff; 24 | } 25 | div.jsoneditor-field, div.jsoneditor-value { 26 | color: #666666; 27 | } 28 | 29 | table.jsoneditor-search div.jsoneditor-frame { 30 | background: #808080; 31 | } 32 | 33 | tr.jsoneditor-highlight, 34 | tr.jsoneditor-selected { 35 | background-color: #808080; 36 | } 37 | 38 | div.jsoneditor-field[contenteditable=true]:focus, 39 | div.jsoneditor-field[contenteditable=true]:hover, 40 | div.jsoneditor-value[contenteditable=true]:focus, 41 | div.jsoneditor-value[contenteditable=true]:hover, 42 | div.jsoneditor-field.jsoneditor-highlight, 43 | div.jsoneditor-value.jsoneditor-highlight { 44 | background-color: #808080; 45 | border-color: #808080; 46 | } 47 | 48 | div.jsoneditor-field.highlight-active, 49 | div.jsoneditor-field.highlight-active:focus, 50 | div.jsoneditor-field.highlight-active:hover, 51 | div.jsoneditor-value.highlight-active, 52 | div.jsoneditor-value.highlight-active:focus, 53 | div.jsoneditor-value.highlight-active:hover { 54 | background-color: #b1b1b1; 55 | border-color: #b1b1b1; 56 | } 57 | 58 | div.jsoneditor-tree button:focus { 59 | background-color: #868686; 60 | } 61 | 62 | /* coloring of JSON in tree mode */ 63 | div.jsoneditor-readonly { 64 | color: #666666; 65 | } 66 | div.jsoneditor td.jsoneditor-separator { 67 | color: #acacac; 68 | } 69 | div.jsoneditor-value.jsoneditor-string { 70 | color: #ffb300; 71 | } 72 | div.jsoneditor-value.jsoneditor-object, 73 | div.jsoneditor-value.jsoneditor-array { 74 | color: #bababa; 75 | } 76 | div.jsoneditor-value.jsoneditor-number { 77 | color: #ff4040; 78 | } 79 | div.jsoneditor-value.jsoneditor-boolean { 80 | color: #ff8048; 81 | } 82 | div.jsoneditor-value.jsoneditor-null { 83 | color: #49a7fc; 84 | } 85 | div.jsoneditor-value.jsoneditor-invalid { 86 | color: white; 87 | } -------------------------------------------------------------------------------- /kafkalot-ui/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Kafkalot 10 | 12 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /kafkalot-ui/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | /** initialize */ 4 | import 'isomorphic-fetch' 5 | import injectTapEventPlugin from 'react-tap-event-plugin' 6 | injectTapEventPlugin() 7 | 8 | import ReactDOM from 'react-dom' 9 | import { Provider, } from 'react-redux' 10 | import { Router, Route, browserHistory, } from 'react-router' 11 | import { syncHistoryWithStore, routerReducer, } from 'react-router-redux' 12 | 13 | import routes from './routes' 14 | import configureStore from './store/configureStore' 15 | 16 | /** import global css (element only, not class) */ 17 | import './global.css' 18 | 19 | const store = configureStore() 20 | const history = syncHistoryWithStore(browserHistory, store) 21 | 22 | ReactDOM.render( 23 | 24 | 25 | , document.getElementById('app') 26 | ) 27 | -------------------------------------------------------------------------------- /kafkalot-ui/src/middlewares/Api.js: -------------------------------------------------------------------------------- 1 | import fetch from 'isomorphic-fetch' 2 | import { take, put, call, fork, select, } from 'redux-saga/effects' 3 | 4 | import URL from './Url' 5 | import { Code as ErrorCode, } from '../constants/Error' 6 | import * as Logger from '../util/Logger' 7 | import * as Selector from '../reducers/ConnectorReducer/Selector' 8 | 9 | /** 10 | * low-level APIs 11 | * 12 | * doesn't handle exceptions 13 | */ 14 | 15 | const HTTP_METHOD = { 16 | GET: 'GET', /** get */ 17 | POST: 'POST', /** create */ 18 | PATCH: 'PATCH', /** partial update */ 19 | PUT: 'PUT', /** replace */ 20 | DELETE: 'DELETE', /** remove */ 21 | } 22 | 23 | const HTTP_HEADERS_JSON = { 24 | 'Accept': 'application/json', 25 | 'Content-Type': 'application/json', 26 | } 27 | 28 | function handleJsonResponse(url, method, promise) { 29 | return promise 30 | .then(response => { 31 | let error = undefined 32 | if (response.status >= 300) 33 | error = true 34 | 35 | return response.text().then(text => { 36 | return { url, method, status: response.status, text, error, } 37 | }) 38 | }) 39 | .then(({ url, method, status, text, error, }) => { 40 | if (error) throw new Error(text) 41 | 42 | return JSON.parse(text) 43 | }) 44 | } 45 | 46 | function getJSON(url) { 47 | const method = HTTP_METHOD.GET 48 | 49 | return handleJsonResponse(url, method, fetch(url, { 50 | method, 51 | credentials: 'include', 52 | headers: HTTP_HEADERS_JSON, 53 | })) 54 | } 55 | 56 | function postJSON(url, body) { 57 | const method = HTTP_METHOD.POST 58 | 59 | return handleJsonResponse(url, method, fetch(url, { 60 | method, 61 | credentials: 'include', 62 | headers: HTTP_HEADERS_JSON, 63 | body: JSON.stringify(body), 64 | })) 65 | } 66 | 67 | function patchJSON(url, body) { 68 | const method = HTTP_METHOD.PATCH 69 | 70 | return handleJsonResponse(url, method, fetch(url, { 71 | method, 72 | credentials: 'include', 73 | headers: HTTP_HEADERS_JSON, 74 | body: JSON.stringify(body), 75 | })) 76 | } 77 | 78 | function putJSON(url, body) { 79 | const method = HTTP_METHOD.PUT 80 | 81 | return handleJsonResponse(url, method, fetch(url, { 82 | method, 83 | credentials: 'include', 84 | headers: HTTP_HEADERS_JSON, 85 | body: JSON.stringify(body), 86 | })) 87 | } 88 | 89 | function deleteJSON(url) { 90 | const method = HTTP_METHOD.DELETE 91 | 92 | return handleJsonResponse(url, method, fetch(url, { 93 | method, 94 | credentials: 'include', 95 | headers: HTTP_HEADERS_JSON, 96 | })) 97 | } 98 | 99 | export function delay(millis) { 100 | return new Promise(resolve => setTimeout(() => { resolve() }, millis)) 101 | } 102 | 103 | /** 104 | * high level API (business related) 105 | * 106 | * exception will be caught in watcher functions 107 | */ 108 | 109 | export function* fetchAllConnectorNames(storageName) { 110 | const connectorsUrl = URL.getConnectorsUrl(storageName) 111 | const connectorNames = yield call(getJSON, connectorsUrl) 112 | 113 | if (!Array.isArray(connectorNames)) 114 | throw new Error(`GET ${connectorsUrl} didn't return an array, got ${connectorNames}`) 115 | 116 | return connectorNames 117 | } 118 | 119 | export function* getConnector(connectorName) { 120 | const storageName = yield select(Selector.getSelectedStorage) 121 | const connectorUrl = URL.getConnectorUrl(storageName, connectorName) 122 | 123 | const connector = yield call(getJSON, connectorUrl) 124 | return connector 125 | } 126 | 127 | export function* getConnectorTasks(connectorName) { 128 | const storageName = yield select(Selector.getSelectedStorage) 129 | const url = URL.getConnectorTaskUrl(storageName, connectorName) 130 | 131 | const tasks = yield call(getJSON, url) 132 | 133 | if (!tasks && !Array.isArray(tasks)) { 134 | Logger.error(`Invalid tasks returned from ${url}`) 135 | throw new Error(ErrorCode.INVALID_TASKS) 136 | } 137 | 138 | return tasks 139 | } 140 | 141 | export function* putConnectorConfig(connectorName, config) { 142 | const storageName = yield select(Selector.getSelectedStorage) 143 | const connectorConfigUrl = URL.getConnectorConfigUrl(storageName, connectorName) 144 | 145 | yield call(putJSON, connectorConfigUrl, config) 146 | } 147 | 148 | export function* postConnector(config, name) { 149 | const storageName = yield select(Selector.getSelectedStorage) 150 | const connectorsUrl = URL.getConnectorsUrl(storageName) 151 | 152 | const connector = { name, config, } // TODO: assembly in server 153 | 154 | return yield call(postJSON, connectorsUrl, connector) 155 | } 156 | 157 | export function* deleteConnector(connectorName) { 158 | const storageName = yield select(Selector.getSelectedStorage) 159 | const connectorsUrl = URL.getConnectorUrl(storageName, connectorName) 160 | 161 | yield call(deleteJSON, connectorsUrl) 162 | } 163 | 164 | 165 | /** command related */ 166 | 167 | export const KEY_OPERATION = 'operation' 168 | export const CONNECTOR_COMMAND = { 169 | START: { [KEY_OPERATION]: 'start', }, 170 | STOP: { [KEY_OPERATION]: 'stop', }, 171 | RESTART: { [KEY_OPERATION]: 'restart', }, 172 | PAUSE: { [KEY_OPERATION]: 'pause', }, 173 | RESUME: { [KEY_OPERATION]: 'resume', }, 174 | ENABLE: { [KEY_OPERATION]: 'enable', }, 175 | DISABLE: { [KEY_OPERATION]: 'disable', }, 176 | } 177 | 178 | export const CONNECTOR_TASK_COMMAND = { 179 | RESTART: { [KEY_OPERATION]: 'restart', }, 180 | } 181 | 182 | export function* postConnectorCommand(connectorName, command) { 183 | const storageName = yield select(Selector.getSelectedStorage) 184 | const url = URL.getConnectorCommandUrl(storageName, connectorName) 185 | 186 | return yield call(postJSON, url, command) 187 | } 188 | 189 | export function* postConnectorTaskCommand(connectorName, taskId, command) { 190 | const storageName = yield select(Selector.getSelectedStorage) 191 | const url = URL.getConnectorTaskCommandUrl(storageName, connectorName, taskId) 192 | 193 | return yield call(postJSON, url, command) 194 | } 195 | 196 | export function* disableConnector(connectorName) { 197 | return yield call(postConnectorCommand, connectorName, CONNECTOR_COMMAND.DISABLE) 198 | } 199 | 200 | export function* enableConnector(connectorName) { 201 | return yield call(postConnectorCommand, connectorName, CONNECTOR_COMMAND.ENABLE) 202 | } 203 | 204 | export function* startConnector(connectorName) { 205 | return yield call(postConnectorCommand, connectorName, CONNECTOR_COMMAND.START) 206 | } 207 | 208 | export function* stopConnector(connectorName) { 209 | return yield call(postConnectorCommand, connectorName, CONNECTOR_COMMAND.STOP) 210 | } 211 | 212 | export function* restartConnector(connectorName) { 213 | return yield call(postConnectorCommand, connectorName, CONNECTOR_COMMAND.RESTART) 214 | } 215 | 216 | export function* pauseConnector(connectorName) { 217 | return yield call(postConnectorCommand, connectorName, CONNECTOR_COMMAND.PAUSE) 218 | } 219 | 220 | export function* resumeConnector(connectorName) { 221 | return yield call(postConnectorCommand, connectorName, CONNECTOR_COMMAND.RESUME) 222 | } 223 | 224 | export function* restartConnectorTask(connectorName, taskId) { 225 | return yield call(postConnectorTaskCommand, connectorName, taskId, CONNECTOR_TASK_COMMAND.RESTART) 226 | } 227 | 228 | /** connector plugins related */ 229 | 230 | export function* getConnectorPlugins() { 231 | const storageName = yield select(Selector.getSelectedStorage) 232 | const url = URL.getConnectorPluginsUrl(storageName) 233 | 234 | const connectorPlugins = yield call(getJSON, url) 235 | 236 | // TODO return class name only in STORAGE 237 | return connectorPlugins.map(connectorPlugin => connectorPlugin.class) 238 | } 239 | 240 | export function* getConnectorPluginsConfigSchema(connectorClass) { 241 | let schema = undefined 242 | 243 | const storageName = yield select(Selector.getSelectedStorage) 244 | const url = URL.getConnectorPluginsSchemaUrl(storageName, connectorClass) 245 | 246 | try { 247 | /** get available connector plugins */ 248 | schema = yield call(getJSON, url) 249 | } catch (error) { 250 | Logger.warn(`${ErrorCode.CANNOT_FIND_CONNECTOR_PLUGINS} (${connectorClass})`) 251 | return undefined 252 | } 253 | 254 | return schema 255 | } 256 | 257 | export function* validateConnectorConfig(connectorClass, connectorConfig) { 258 | let errorMessages = [] 259 | 260 | const storageName = yield select(Selector.getSelectedStorage) 261 | const url = URL.getConnectorPluginsValidateUrl(storageName, connectorClass) 262 | 263 | try { 264 | /** put connector config and get validation result */ 265 | const validationResult = yield call(putJSON, url, connectorConfig) 266 | 267 | if (validationResult.error_messages && Array.isArray(validationResult.error_messages)) 268 | errorMessages = validationResult.error_messages 269 | 270 | } catch (error) { 271 | Logger.warn(`${ErrorCode.CANNOT_FIND_CONNECTOR_PLUGINS} (${connectorClass})`) 272 | return undefined 273 | } 274 | 275 | return errorMessages 276 | } 277 | -------------------------------------------------------------------------------- /kafkalot-ui/src/middlewares/Saga.js: -------------------------------------------------------------------------------- 1 | import { fork, call, put, } from 'redux-saga/effects' 2 | import { takeEvery, } from 'redux-saga' 3 | import { createAction, } from 'redux-actions' 4 | 5 | import * as Handler from './Handler' 6 | 7 | /** 8 | * watcher functions 9 | */ 10 | 11 | export const ActionType = { 12 | FETCH_CONNECTOR: 'CONNECTOR/SAGA/FETCH_CONNECTOR', 13 | REQUEST_TO_OPEN_CONFIG_EDITOR: 'CONNECTOR/SAGA/OPEN_CONFIG_EDITOR', 14 | REQUEST_TO_OPEN_CREATE_EDITOR: 'CONNECTOR/SAGA/OPEN_CREATE_EDITOR', 15 | REQUEST_TO_CHANGE_SELECTED_CONNECTOR_CLASS: 'CONNECTOR/SAGA/CHANGE_SELECTED_CONNECTOR_CLASS', 16 | REQUEST_TO_VALIDATE_CONNECTOR_CONFIG: 'CONNECTOR/SAGA/VALIDATE_CONNECTOR_CONFIG', 17 | REQUEST_TO_OPEN_REMOVE_DIALOG: 'CONNECTOR/SAGA/OPEN_REMOVE_DIALOG', 18 | REQUEST_TO_UPDATE_CONFIG: 'CONNECTOR/SAGA/UPDATE_CONFIG', 19 | REQUEST_TO_CREATE_CONNECTOR: 'CONNECTOR/SAGA/CREATE_CONNECTOR', 20 | REQUEST_TO_REMOVE_CONNECTOR: 'CONNECTOR/SAGA/REMOVE_CONNECTOR', 21 | REQUEST_TO_CHANGE_STORAGE: 'CONNECTOR/SAGA/CHANGE_STORAGE', 22 | REQUEST_TO_DISABLE_CONNECTOR: 'CONNECTOR/SAGA/DISABLE_CONNECTOR', 23 | REQUEST_TO_ENABLE_CONNECTOR: 'CONNECTOR/SAGA/ENABLE_CONNECTOR', 24 | REQUEST_TO_START_CONNECTOR: 'CONNECTOR/SAGA/START_CONNECTOR', 25 | REQUEST_TO_STOP_CONNECTOR: 'CONNECTOR/SAGA/STOP_CONNECTOR', 26 | REQUEST_TO_RESTART_CONNECTOR: 'CONNECTOR/SAGA/RESTART_CONNECTOR', 27 | REQUEST_TO_RESTART_CONNECTOR_TASK: 'CONNECTOR/SAGA/RESTART_CONNECTOR_TASK', 28 | REQUEST_TO_PAUSE_CONNECTOR: 'CONNECTOR/SAGA/PAUSE_CONNECTOR', 29 | REQUEST_TO_RESUME_CONNECTOR: 'CONNECTOR/SAGA/RESUME_CONNECTOR', 30 | } 31 | 32 | export const Action = { 33 | fetchConnector: createAction(ActionType.FETCH_CONNECTOR), 34 | openConfigEditor: createAction(ActionType.REQUEST_TO_OPEN_CONFIG_EDITOR), 35 | openCreateEditor: createAction(ActionType.REQUEST_TO_OPEN_CREATE_EDITOR), 36 | changeSelectedConnectorClass: createAction(ActionType.REQUEST_TO_CHANGE_SELECTED_CONNECTOR_CLASS), 37 | validateConnectorConfig: createAction(ActionType.REQUEST_TO_VALIDATE_CONNECTOR_CONFIG), 38 | openRemoveDialog: createAction(ActionType.REQUEST_TO_OPEN_REMOVE_DIALOG), 39 | updateConfig: createAction(ActionType.REQUEST_TO_UPDATE_CONFIG), 40 | createConnector: createAction(ActionType.REQUEST_TO_CREATE_CONNECTOR), 41 | removeConnector: createAction(ActionType.REQUEST_TO_REMOVE_CONNECTOR), 42 | changeStorage: createAction(ActionType.REQUEST_TO_CHANGE_STORAGE), 43 | disableConnector: createAction(ActionType.REQUEST_TO_DISABLE_CONNECTOR), 44 | enableConnector: createAction(ActionType.REQUEST_TO_ENABLE_CONNECTOR), 45 | startConnector: createAction(ActionType.REQUEST_TO_START_CONNECTOR), 46 | stopConnector: createAction(ActionType.REQUEST_TO_STOP_CONNECTOR), 47 | restartConnector: createAction(ActionType.REQUEST_TO_RESTART_CONNECTOR), 48 | restartConnectorTask: createAction(ActionType.REQUEST_TO_RESTART_CONNECTOR_TASK), 49 | pauseConnector: createAction(ActionType.REQUEST_TO_PAUSE_CONNECTOR), 50 | resumeConnector: createAction(ActionType.REQUEST_TO_RESUME_CONNECTOR), 51 | } 52 | 53 | export function* watchFetchConnector() { 54 | yield* takeEvery( 55 | ActionType.FETCH_CONNECTOR, Handler.handleFetchConnector) 56 | } 57 | 58 | export function* watchOpenConfigEditor() { 59 | yield* takeEvery( 60 | ActionType.REQUEST_TO_OPEN_CONFIG_EDITOR, Handler.handleOpenConfigEditor) 61 | } 62 | 63 | export function* watchOpenCreateEditor() { 64 | yield* takeEvery( 65 | ActionType.REQUEST_TO_OPEN_CREATE_EDITOR, Handler.handleOpenCreateEditor) 66 | } 67 | 68 | export function* watchChangeSelectedConnectorClass() { 69 | yield* takeEvery( 70 | ActionType.REQUEST_TO_CHANGE_SELECTED_CONNECTOR_CLASS, Handler.handleChangeSelectedConnectorClass) 71 | } 72 | 73 | export function* watchValidateConnectorConfig() { 74 | yield* takeEvery( 75 | ActionType.REQUEST_TO_VALIDATE_CONNECTOR_CONFIG, 76 | Handler.handleValidateConnectorConfig) 77 | } 78 | 79 | export function* watchOpenRemoveDialog() { 80 | yield* takeEvery( 81 | ActionType.REQUEST_TO_OPEN_REMOVE_DIALOG, Handler.handleOpenRemoveDialog) 82 | } 83 | 84 | export function* watchUpdateConfig() { 85 | yield* takeEvery( 86 | ActionType.REQUEST_TO_UPDATE_CONFIG, Handler.handleUpdateConfig) 87 | } 88 | 89 | export function* watchRemoveConnector() { 90 | yield* takeEvery( 91 | ActionType.REQUEST_TO_REMOVE_CONNECTOR, Handler.handleRemove,) 92 | } 93 | 94 | export function* watchCreateConnector() { 95 | yield* takeEvery( 96 | ActionType.REQUEST_TO_CREATE_CONNECTOR, Handler.handleCreateConnector) 97 | } 98 | 99 | export function* watchDisableConnector() { 100 | yield* takeEvery( 101 | ActionType.REQUEST_TO_DISABLE_CONNECTOR, Handler.handleDisableConnector) 102 | } 103 | 104 | export function* watchEnableConnector() { 105 | yield* takeEvery( 106 | ActionType.REQUEST_TO_ENABLE_CONNECTOR, Handler.handleEnableConnector) 107 | } 108 | 109 | export function* watchStartConnector() { 110 | yield* takeEvery( 111 | ActionType.REQUEST_TO_START_CONNECTOR, Handler.handleStartConnector) 112 | } 113 | 114 | export function* watchStopConnector() { 115 | yield* takeEvery( 116 | ActionType.REQUEST_TO_STOP_CONNECTOR, Handler.handleStopConnector) 117 | } 118 | 119 | export function* watchRestartConnector() { 120 | yield* takeEvery( 121 | ActionType.REQUEST_TO_RESTART_CONNECTOR, Handler.handleRestartConnector) 122 | } 123 | 124 | export function* watchRestartConnectorTask() { 125 | yield* takeEvery( 126 | ActionType.REQUEST_TO_RESTART_CONNECTOR_TASK, Handler.handleRestartConnectorTask) 127 | } 128 | 129 | export function* watchPauseConnector() { 130 | yield* takeEvery( 131 | ActionType.REQUEST_TO_PAUSE_CONNECTOR, Handler.handlePauseConnector) 132 | } 133 | 134 | export function* watchResumeConnector() { 135 | yield* takeEvery( 136 | ActionType.REQUEST_TO_RESUME_CONNECTOR, Handler.handleResumeConnector) 137 | } 138 | 139 | export default function* root() { 140 | yield [ 141 | fork(Handler.initialize), 142 | fork(watchFetchConnector), 143 | fork(watchOpenConfigEditor), 144 | fork(watchOpenCreateEditor), 145 | fork(watchChangeSelectedConnectorClass), 146 | fork(watchValidateConnectorConfig), 147 | fork(watchOpenRemoveDialog), 148 | fork(watchUpdateConfig), 149 | fork(watchCreateConnector), 150 | fork(watchRemoveConnector), 151 | fork(watchDisableConnector), 152 | fork(watchEnableConnector), 153 | fork(watchStartConnector), 154 | fork(watchStopConnector), 155 | fork(watchRestartConnector), 156 | fork(watchRestartConnectorTask), 157 | fork(watchPauseConnector), 158 | fork(watchResumeConnector), 159 | ] 160 | } 161 | -------------------------------------------------------------------------------- /kafkalot-ui/src/middlewares/Url.js: -------------------------------------------------------------------------------- 1 | import { STORAGES, } from '../constants/Config' 2 | 3 | export const URL_BASE = 'api/v1' 4 | export const URL_BASE_CONNECTORS = `${URL_BASE}/connectors` 5 | export const URL_BASE_CONNECTOR_PLUGINS = `${URL_BASE}/connector-plugins` 6 | 7 | export const STORAGE_PROPERTY = { name: 'name', address: 'address', } 8 | 9 | /** multi storage support */ 10 | export const STORAGE_NAMES = STORAGES.map(storage => storage[STORAGE_PROPERTY.name]) 11 | export const INITIAL_STORAGE_NAME = STORAGE_NAMES[0] 12 | 13 | /** internal functions (tested) */ 14 | 15 | export function _getStorageAddress(storages, storageName) { 16 | const filtered = storages.filter(storage => storage[STORAGE_PROPERTY.name] === storageName) 17 | 18 | if (filtered.length >= 2) throw new Error(`STORAGES has duplicated ${storageName}`) 19 | if (filtered.length === 0) throw new Error(`Can't find address using stroage name: ${storageName}`) 20 | 21 | return filtered[0].address 22 | } 23 | 24 | export function _buildConnectorUrl(storageName, connectorName) { 25 | /** connectorName might be undefined to retrieve all connectors */ 26 | const postfix = (connectorName === void 0) ? '' : `/${connectorName}` 27 | const storageAddress = _getStorageAddress(STORAGES, storageName) 28 | 29 | return `${storageAddress}/${URL_BASE_CONNECTORS}${postfix}` 30 | } 31 | 32 | export function _buildConnectorTaskUrl(storageName, connectorName, taskId) { 33 | /** taskId might be undefined to retrieve all tasks */ 34 | const postfix = (taskId === void 0) ? '' : `/${taskId}` 35 | const storageAddress = _getStorageAddress(STORAGES, storageName) 36 | 37 | return `${storageAddress}/${URL_BASE_CONNECTORS}/${connectorName}/tasks${postfix}` 38 | } 39 | 40 | export function _buildConnectorPluginsUrl(storageName, connectorClass) { 41 | const postfix = (connectorClass === void 0) ? '' : `/${connectorClass}` 42 | const storageAddress = _getStorageAddress(STORAGES, storageName) 43 | 44 | return `${storageAddress}/${URL_BASE_CONNECTOR_PLUGINS}${postfix}` 45 | } 46 | 47 | /** exposed functions, use ENV variables (injected by webpack) */ 48 | export default { 49 | getConnectorsUrl: (storageName) => { 50 | return _buildConnectorUrl(storageName) 51 | }, 52 | 53 | getConnectorUrl: (storageName, connectorName) => { 54 | return _buildConnectorUrl(storageName, connectorName) 55 | }, 56 | 57 | getConnectorConfigUrl: (storageName, connectorName) => { 58 | return `${_buildConnectorUrl(storageName, connectorName)}/config` 59 | }, 60 | 61 | getConnectorCommandUrl: (storageName, connectorName) => { 62 | return `${_buildConnectorUrl(storageName, connectorName)}/command` 63 | }, 64 | 65 | getConnectorTaskUrl: (storageName, connectorName, taskId) => { 66 | return `${_buildConnectorTaskUrl(storageName, connectorName, taskId)}` 67 | }, 68 | 69 | getConnectorTaskCommandUrl: (storageName, connectorName, taskId) => { 70 | return `${_buildConnectorTaskUrl(storageName, connectorName, taskId)}/command` 71 | }, 72 | 73 | getConnectorPluginsUrl: (storageName) => { 74 | return `${_buildConnectorPluginsUrl(storageName)}` 75 | }, 76 | 77 | getConnectorPluginsSchemaUrl: (storageName, connectorClass) => { 78 | return `${_buildConnectorPluginsUrl(storageName, connectorClass)}/schema` 79 | }, 80 | 81 | getConnectorPluginsValidateUrl: (storageName, connectorClass) => { 82 | return `${_buildConnectorPluginsUrl(storageName, connectorClass)}/validate` 83 | }, 84 | } 85 | -------------------------------------------------------------------------------- /kafkalot-ui/src/middlewares/__tests__/Api.spec.js: -------------------------------------------------------------------------------- 1 | import { expect, } from 'chai' 2 | import { fork, take, call, put, select, } from 'redux-saga/effects' 3 | 4 | describe('api', () => { 5 | describe('High-level APIs', () => { 6 | 7 | //describe(`${API.start.name}`, () => { 8 | // it(`should call ${API.patchState.name} passing ${Converter.SERVER_JOB_PROPERTY.active} field only`, () => { 9 | // const container = 'container01' 10 | // const connectorName = 'job01' 11 | // const gen = API.start(container, connectorName) 12 | // expect(gen.next().value).to.deep.equal( 13 | // call(API.patchState, container, connectorName, Converter.createStateToStartJob()) 14 | // ) 15 | // }) 16 | //}) 17 | 18 | }) 19 | }) 20 | -------------------------------------------------------------------------------- /kafkalot-ui/src/reducers/ConnectorReducer/ClosableSnackbarState.js: -------------------------------------------------------------------------------- 1 | import { createAction, handleActions, } from 'redux-actions' 2 | 3 | import * as Logger from '../../util/Logger' 4 | 5 | import { CLOSABLE_SNACKBAR_MODE, } from '../../components/Common/ClosableSnackbar' 6 | 7 | 8 | export const Property = { 9 | MESSAGE: 'message', 10 | SNACKBAR_MODE: 'snackbarMode', 11 | } 12 | 13 | export const Payload = { 14 | ERROR: 'error', 15 | MESSAGE: 'message', 16 | } 17 | 18 | export const ActionType = { 19 | OPEN_INFO_SNACKBAR: 'CONNECTOR/SNACKBAR/OPEN_INFOS', 20 | OPEN_ERROR_SNACKBAR: 'CONNECTOR/SNACKBAR/OPEN_ERROR', 21 | CLOSE_SNACKBAR: 'CONNECTOR/SNACKBAR/CLOSE', 22 | } 23 | 24 | export const Action = { 25 | openInfoSnackbar: createAction(ActionType.OPEN_INFO_SNACKBAR), 26 | openErrorSnackbar: createAction(ActionType.OPEN_ERROR_SNACKBAR), 27 | closeSnackbar: createAction(ActionType.CLOSE_SNACKBAR), 28 | } 29 | 30 | export const INITIAL_STATE = { 31 | [Property.MESSAGE]: '', 32 | [Property.SNACKBAR_MODE]: CLOSABLE_SNACKBAR_MODE.CLOSE, 33 | } 34 | 35 | export const handler = handleActions({ 36 | /** snackbar related */ 37 | [ActionType.CLOSE_SNACKBAR]: (state) => 38 | Object.assign({}, state, { snackbarMode: CLOSABLE_SNACKBAR_MODE.CLOSE, }), 39 | 40 | [ActionType.OPEN_ERROR_SNACKBAR]: (state, { payload, }) => { 41 | 42 | const message = payload[Payload.MESSAGE] 43 | const error = payload[Payload.ERROR] 44 | 45 | Logger.error(message, error) 46 | 47 | return Object.assign({}, state, { 48 | [Property.MESSAGE]: error.message, 49 | [Property.SNACKBAR_MODE]: CLOSABLE_SNACKBAR_MODE.OPEN, 50 | }) 51 | }, 52 | 53 | [ActionType.OPEN_INFO_SNACKBAR]: (state, { payload, }) => { 54 | 55 | const message = payload[Payload.MESSAGE] 56 | 57 | Logger.info(message) 58 | 59 | return Object.assign({}, state, { 60 | [Property.MESSAGE]: message, 61 | [Property.SNACKBAR_MODE]: CLOSABLE_SNACKBAR_MODE.OPEN, 62 | }) 63 | }, 64 | 65 | }, INITIAL_STATE) 66 | 67 | -------------------------------------------------------------------------------- /kafkalot-ui/src/reducers/ConnectorReducer/ConfigEditorState.js: -------------------------------------------------------------------------------- 1 | import { createAction, handleActions, } from 'redux-actions' 2 | 3 | export const ActionType = { 4 | SUCCEED_TO_OPEN_CONFIG_EDITOR: 'CONNECTOR/CONFIG_EDITOR/SUCCEED_TO_OPEN', 5 | CLOSE_CONFIG_EDITOR: 'CONNECTOR/CONFIG_EDITOR/CLOSE', 6 | } 7 | 8 | export const Action = { 9 | closeConfigEditor: createAction(ActionType.CLOSE_CONFIG_EDITOR), 10 | } 11 | 12 | export const PrivateAction = { 13 | succeededToOpenConfigEditor: createAction(ActionType.SUCCEED_TO_OPEN_CONFIG_EDITOR), 14 | } 15 | 16 | export const Payload = { 17 | NAME: 'name', 18 | READONLY: 'readonly', 19 | OPENED: 'opened', 20 | CONFIG: 'config', 21 | } 22 | 23 | export const INITIAL_STATE = { 24 | [Payload.NAME]: '', 25 | [Payload.OPENED]: false, 26 | [Payload.READONLY]: true, 27 | [Payload.CONFIG]: {}, 28 | } 29 | 30 | export const handler = handleActions({ 31 | /** open editor dialog to edit */ 32 | [ActionType.SUCCEED_TO_OPEN_CONFIG_EDITOR]: (state, { payload, }) => 33 | Object.assign({}, state, { 34 | [Payload.NAME]: payload[Payload.NAME], 35 | [Payload.OPENED]: true, 36 | [Payload.READONLY]: payload[Payload.READONLY], 37 | [Payload.CONFIG]: payload[Payload.CONFIG], 38 | }), 39 | 40 | [ActionType.CLOSE_CONFIG_EDITOR]: (state) => 41 | Object.assign({}, INITIAL_STATE /** reset */, { 42 | [Payload.OPENED]: false, 43 | }), 44 | }, INITIAL_STATE) 45 | 46 | -------------------------------------------------------------------------------- /kafkalot-ui/src/reducers/ConnectorReducer/ConfigSchemaState.js: -------------------------------------------------------------------------------- 1 | import { createAction, handleActions, } from 'redux-actions' 2 | 3 | export const ActionType = { 4 | SUCCEED_TO_VALIDATE_CONFIG: 'CONNECTOR/CONFIG_SCHEMA/SUCCEED_TO_VALIDATE', 5 | SUCCEED_TO_SET_CONFIG_SCHEMA_ONLY: 'CONNECTOR/CONFIG_SCHEMA/SUCCEED_TO_SET', 6 | } 7 | 8 | export const PrivateAction = { 9 | succeededToValidateConfig: createAction(ActionType.SUCCEED_TO_VALIDATE_CONFIG), 10 | succeededToSetConfigSchemaOnly: createAction(ActionType.SUCCEED_TO_SET_CONFIG_SCHEMA_ONLY), 11 | } 12 | 13 | export const Payload = { 14 | CONNECTOR_CLASS: 'connectorClass', 15 | CONNECTOR_CONFIG: 'connectorConfig', 16 | ERROR_MESSAGES: 'errorMessages', 17 | CONFIG_SCHEMA: 'configSchema', /** JSONSchema of `config` */ 18 | } 19 | 20 | /** handle connector schema, validation result only. (not config itself) */ 21 | export const INITIAL_STATE = { 22 | [Payload.ERROR_MESSAGES]: [], 23 | [Payload.CONFIG_SCHEMA]: undefined, 24 | } 25 | 26 | export const handler = handleActions({ 27 | /** handle clicking `validate` button */ 28 | [ActionType.SUCCEED_TO_VALIDATE_CONFIG]: (state, { payload, }) => 29 | Object.assign({}, state, { 30 | [Payload.ERROR_MESSAGES]: payload[Payload.ERROR_MESSAGES], 31 | [Payload.CONFIG_SCHEMA]: payload[Payload.CONFIG_SCHEMA], 32 | }), 33 | 34 | /** handle initial dialog open, changing connector class */ 35 | [ActionType.SUCCEED_TO_SET_CONFIG_SCHEMA_ONLY]: (state, { payload, }) => 36 | Object.assign({}, INITIAL_STATE, { 37 | [Payload.CONFIG_SCHEMA]: payload[Payload.CONFIG_SCHEMA], 38 | }), 39 | 40 | }, INITIAL_STATE) 41 | 42 | -------------------------------------------------------------------------------- /kafkalot-ui/src/reducers/ConnectorReducer/ConnectorActionType.js: -------------------------------------------------------------------------------- 1 | /** common action type shared between state handlers */ 2 | 3 | export const CommonActionType = { 4 | CHANGE_FILTER_KEYWORD: 'CONNECTOR/COMMON/CHANGE_FILTER_KEYWORD', 5 | CHANGE_SORTER: 'CONNECTOR/COMMON/CHANGE_SORTER', 6 | } 7 | -------------------------------------------------------------------------------- /kafkalot-ui/src/reducers/ConnectorReducer/ConnectorListState.js: -------------------------------------------------------------------------------- 1 | import { createAction, handleActions, } from 'redux-actions' 2 | 3 | import { Property as PaginatorProperty, } from './PaginatorState' 4 | import { ConnectorState, } from '../../constants/ConnectorState' 5 | import { SorterType, AvailableSorters, } from '../../constants/Sorter' 6 | import { CommonActionType, } from './ConnectorActionType' 7 | 8 | 9 | export const Payload = { 10 | CONNECTOR: 'connector', 11 | CONNECTORS: 'connectors', 12 | FILTER_KEYWORD: 'filterKeyword', 13 | NEW_PAGE_OFFSET: 'newPageOffset', 14 | SORTER: 'sorter', 15 | } 16 | 17 | export function isEmptyName(name) { 18 | return (name === void 0) || (name === null) || (name === '') 19 | } 20 | 21 | export const ActionType = { 22 | ADD_FETCHED_CONNECTOR: 'CONNECTOR/LIST/ADD_FETCHED_CONNECTOR', 23 | SET_CONNECTOR_PROPERTY: 'CONNECTOR/LIST/SET_CONNECTOR_PROPERTY', 24 | SET_CONNECTOR_TASKS: 'CONNECTOR/LIST/SET_CONNECTOR_TASKS', 25 | ADD_CREATED_CONNECTOR: 'CONNECTOR/LIST/ADD_CREATED_CONNECTOR', 26 | REMOVE_DELETED_CONNECTOR: 'CONNECTOR/LIST/REMOVE_DELETED_CONNECTOR', 27 | SET_CHECKED: 'CONNECTOR/LIST/SET_CHECKED', 28 | TOGGLE_CURRENT_PAGE_CHECKBOXES: 'CONNECTOR/LIST/TOGGLE_CURRENT_PAGE_CHECKBOXES', 29 | } 30 | 31 | export const Action = { 32 | setConnectorChecked: createAction(ActionType.SET_CHECKED), 33 | changeFilterKeyword: createAction(CommonActionType.CHANGE_FILTER_KEYWORD), 34 | changeSorter: createAction(CommonActionType.CHANGE_SORTER), 35 | 36 | toggleCurrentPageCheckboxes: createAction(ActionType.TOGGLE_CURRENT_PAGE_CHECKBOXES), 37 | } 38 | 39 | export const PrivateAction = { 40 | setConnector: createAction(ActionType.SET_CONNECTOR_PROPERTY), 41 | setConnectorTasks: createAction(ActionType.SET_CONNECTOR_TASKS), 42 | addCreatedConnector: createAction(ActionType.ADD_CREATED_CONNECTOR), 43 | removeDeletedConnector: createAction(ActionType.REMOVE_DELETED_CONNECTOR), 44 | addFetchedConnector: createAction(ActionType.ADD_FETCHED_CONNECTOR), 45 | } 46 | 47 | export const ConnectorListProperty = { 48 | CONNECTORS: 'connectors', 49 | FILTER_KEYWORD: 'filterKeyword', 50 | SORTER: 'sorter', 51 | TABLE_HEADER_CHECKED: 'tableHeaderChecked', 52 | } 53 | 54 | export const ConnectorProperty = { 55 | NAME: 'name', 56 | STATE: 'state', 57 | CONFIG: 'config', 58 | TASKS: 'tasks', 59 | UPTIME: 'uptime', 60 | TAGS: 'tags', 61 | CHECKED: 'checked', 62 | } 63 | 64 | export const ConnectorTaskProperty = { 65 | ID: 'id', 66 | WORKER_ID: 'worker_id', 67 | STATE: 'state', 68 | TRACE: 'trace', 69 | } 70 | 71 | export function isRunningState(state) { return state === ConnectorState.RUNNING } 72 | export function isUnassignedState(state) { return state === ConnectorState.UNASSIGNED } 73 | export function isPausedState(state) { return state === ConnectorState.PAUSED } 74 | export function isFailedState(state) { return state === ConnectorState.FAILED } 75 | export function isRegisteredState(state) { return state === ConnectorState.REGISTERED } 76 | export function isDisabledState(state) { return state === ConnectorState.DISABLED } 77 | export function isWorkingState(state) { return (!isRegisteredState(state) && !isDisabledState(state)) } 78 | 79 | export function isFailedConnector(connector) { return isFailedState(connector[ConnectorProperty.STATE]) } 80 | export function isPausedConnector(connector) { return isPausedState(connector[ConnectorProperty.STATE]) } 81 | export function isRunningConnector(connector) { return isRunningState(connector[ConnectorProperty.STATE]) } 82 | export function isRegisteredConnector(connector) { return isRegisteredState(connector[ConnectorProperty.STATE]) } 83 | export function isDisabledConnector(connector) { return isDisabledState(connector[ConnectorProperty.STATE]) } 84 | 85 | export function isRunningTask(task) { 86 | if (task[ConnectorTaskProperty.STATE] === ConnectorState.RUNNING) return true 87 | else return false 88 | } 89 | 90 | const INITIAL_FILTER_STATE = '' 91 | const INITIAL_SORTER_STATE = AvailableSorters[0] 92 | 93 | const INITIAL_CONNECTOR_LIST_STATE = { 94 | [ConnectorListProperty.CONNECTORS]: [], 95 | [ConnectorListProperty.FILTER_KEYWORD]: INITIAL_FILTER_STATE, 96 | [ConnectorListProperty.SORTER]: INITIAL_SORTER_STATE, 97 | [ConnectorListProperty.TABLE_HEADER_CHECKED]: false, 98 | } 99 | 100 | const INITIAL_CONNECTOR_STATE = { 101 | name: '', 102 | state: ConnectorState.RUNNING, 103 | config: {}, 104 | tasks: [], 105 | 106 | switching: false, 107 | checked: false, 108 | uptime: '', 109 | tags: [], 110 | } 111 | 112 | export const handler = handleActions({ 113 | [ActionType.ADD_FETCHED_CONNECTOR]: (state, { payload, }) => { 114 | const fetched = Object.assign({}, INITIAL_CONNECTOR_STATE, { 115 | [ConnectorProperty.NAME]: payload[ConnectorProperty.NAME], 116 | [ConnectorProperty.STATE]: payload[ConnectorProperty.STATE], 117 | [ConnectorProperty.CONFIG]: payload[ConnectorProperty.CONFIG], 118 | [ConnectorProperty.TASKS]: payload[ConnectorProperty.TASKS], 119 | }) 120 | 121 | const connectors = state[ConnectorListProperty.CONNECTORS] 122 | const updatedConnectors = connectors.concat([ fetched, ]) 123 | 124 | return Object.assign({}, state, { 125 | [ConnectorListProperty.CONNECTORS]: updatedConnectors, 126 | }) 127 | }, 128 | 129 | [ActionType.SET_CONNECTOR_PROPERTY]: (state, { payload, }) => { 130 | const name = payload[ConnectorProperty.NAME] 131 | 132 | const connectors = state[ConnectorListProperty.CONNECTORS] 133 | const updatedConnectors = connectors.map(connector => { 134 | if (connector[ConnectorProperty.NAME] === name) { 135 | return Object.assign({}, connector, { 136 | [ConnectorProperty.NAME]: name, 137 | [ConnectorProperty.STATE]: payload[ConnectorProperty.STATE], 138 | [ConnectorProperty.CONFIG]: payload[ConnectorProperty.CONFIG], 139 | [ConnectorProperty.TASKS]: payload[ConnectorProperty.TASKS], 140 | }) 141 | } 142 | 143 | return connector 144 | }) 145 | 146 | return Object.assign({}, state, { 147 | [ConnectorListProperty.CONNECTORS]: updatedConnectors, 148 | }) 149 | }, 150 | 151 | [ActionType.SET_CONNECTOR_TASKS]: (state, { payload, }) => { 152 | const name = payload[ConnectorProperty.NAME] 153 | const tasks = payload[ConnectorProperty.TASKS] 154 | 155 | const connectors = state[ConnectorListProperty.CONNECTORS] 156 | const updatedConnectors = connectors.map(connector => { 157 | if (connector[ConnectorProperty.NAME] === name) { 158 | return Object.assign({}, connector, { 159 | [ConnectorProperty.TASKS]: tasks, 160 | }) 161 | } 162 | 163 | return connector 164 | }) 165 | 166 | return Object.assign({}, state, { 167 | [ConnectorListProperty.CONNECTORS]: updatedConnectors, 168 | }) 169 | }, 170 | 171 | [ActionType.SET_CHECKED]: (state, { payload, }) => { 172 | const name = payload[ConnectorProperty.NAME] 173 | const checked = payload[ConnectorProperty.CHECKED] 174 | 175 | const connectors = state[ConnectorListProperty.CONNECTORS] 176 | const updatedConnectors = connectors.map(connector => { 177 | if (connector[ConnectorProperty.NAME] === name) { 178 | return Object.assign({}, connector, { 179 | [ConnectorProperty.CHECKED]: checked, 180 | }) 181 | } 182 | 183 | return connector 184 | }) 185 | 186 | return Object.assign({}, state, { 187 | [ConnectorListProperty.CONNECTORS]: updatedConnectors, 188 | }) 189 | }, 190 | 191 | [ActionType.ADD_CREATED_CONNECTOR]: (state, { payload, }) => { 192 | const created = payload[Payload.CONNECTOR] 193 | const connectors = state[ConnectorListProperty.CONNECTORS] 194 | const updatedConnectors = [ created, ].concat(connectors.slice()) 195 | 196 | return Object.assign({}, state, { 197 | [ConnectorListProperty.CONNECTORS]: updatedConnectors, 198 | }) 199 | }, 200 | 201 | [ActionType.REMOVE_DELETED_CONNECTOR]: (state, { payload, }) => { 202 | const name = payload[ConnectorProperty.NAME] 203 | 204 | const connectors = state[ConnectorListProperty.CONNECTORS] 205 | const filtered = connectors.filter(c => name !== c[ConnectorProperty.NAME]) 206 | 207 | return Object.assign({}, state, { 208 | [ConnectorListProperty.CONNECTORS]: filtered, 209 | }) 210 | }, 211 | 212 | [CommonActionType.CHANGE_SORTER]: (state, { payload, }) => { 213 | const requestedSorter = payload[Payload.SORTER] 214 | 215 | const connectors = state[ConnectorListProperty.CONNECTORS] 216 | const copiedConnectors = connectors.slice() 217 | 218 | switch(requestedSorter) { 219 | case SorterType.CHECKED: 220 | copiedConnectors.sort((c1, c2) => { 221 | const c1Checked = c1[ConnectorProperty.CHECKED] 222 | const c2Checked = c2[ConnectorProperty.CHECKED] 223 | 224 | if (c1Checked && !c2Checked ) return -1 225 | else if (!c1Checked && c2Checked ) return 1 226 | else return 0 227 | }) 228 | break 229 | case SorterType.UNCHECKED: 230 | copiedConnectors.sort((c1, c2) => { 231 | const c1Checked = c1[ConnectorProperty.CHECKED] 232 | const c2Checked = c2[ConnectorProperty.CHECKED] 233 | 234 | if (!c1Checked && c2Checked ) return -1 235 | else if (c1Checked && !c2Checked ) return 1 236 | else return 0 237 | }) 238 | break 239 | default: 240 | /** sort by connector state */ 241 | copiedConnectors.sort((c1, c2) => { 242 | const c1State = c1[ConnectorProperty.STATE] 243 | const c2State = c2[ConnectorProperty.STATE] 244 | 245 | if (c1State === requestedSorter && c2State !== requestedSorter) return -1 246 | else if (c1State !== requestedSorter && c2State === requestedSorter) return 1 247 | else return 0 248 | }) 249 | } 250 | 251 | return Object.assign({}, state, { 252 | [ConnectorListProperty.CONNECTORS]: copiedConnectors, 253 | [ConnectorListProperty.SORTER]: requestedSorter, 254 | }) 255 | }, 256 | 257 | [CommonActionType.CHANGE_FILTER_KEYWORD]: (state, { payload, }) => { 258 | return Object.assign({}, state, { 259 | [ConnectorListProperty.FILTER_KEYWORD]: payload[Payload.FILTER_KEYWORD], 260 | }) 261 | }, 262 | 263 | [ActionType.TOGGLE_CURRENT_PAGE_CHECKBOXES]: (state, { payload, }) => { 264 | const { 265 | [ConnectorListProperty.TABLE_HEADER_CHECKED]: tableHeaderChecked, 266 | [PaginatorProperty.ITEM_OFFSET]: itemOffset, 267 | [PaginatorProperty.ITEM_COUNT_PER_PAGE]: itemCountPerPage, 268 | } = payload 269 | 270 | const copied = state[ConnectorListProperty.CONNECTORS].slice() 271 | 272 | for (let i = itemOffset; i < itemOffset + itemCountPerPage && i < copied.length; i++) { 273 | const c = copied[i] 274 | c[ConnectorProperty.CHECKED] = tableHeaderChecked 275 | } 276 | 277 | return Object.assign({}, state, { 278 | [ConnectorListProperty.CONNECTORS]: copied, 279 | [ConnectorListProperty.TABLE_HEADER_CHECKED]: tableHeaderChecked, 280 | }) 281 | }, 282 | 283 | }, INITIAL_CONNECTOR_LIST_STATE) 284 | -------------------------------------------------------------------------------- /kafkalot-ui/src/reducers/ConnectorReducer/CreateEditorState.js: -------------------------------------------------------------------------------- 1 | import { createAction, handleActions, } from 'redux-actions' 2 | 3 | export const ActionType = { 4 | SUCCEED_TO_OPEN_CREATE_EDITOR: 'CONNECTOR/CREATE_EDITOR/SUCCEED_TO_OPEN', 5 | SUCCEED_TO_CHANGE_SELECTED_CONNECTOR_CLASS: 'CONNECTOR/CREATE_EDITOR/SUCCEED_TO_CHANGE_SELECTED_CONNECTOR_CLASS', 6 | CLOSE_CREATE_EDITOR: 'CONNECTOR/CREATE_EDITOR/CLOSE', 7 | } 8 | 9 | export const Action = { 10 | closeCreateEditor: createAction(ActionType.CLOSE_CREATE_EDITOR), 11 | } 12 | 13 | export const PrivateAction = { 14 | succeedToChangeSelectedConnectorClass: createAction(ActionType.SUCCEED_TO_CHANGE_SELECTED_CONNECTOR_CLASS), 15 | succeedToOpenCreateEditor: createAction(ActionType.SUCCEED_TO_OPEN_CREATE_EDITOR), 16 | } 17 | 18 | export const Payload = { 19 | OPENED: 'opened', 20 | CONNECTOR_NAME: 'connectorName', 21 | CONNECTOR_CONFIG: 'connectorConfig', 22 | AVAILABLE_CONNECTORS: 'availableConnectors', 23 | SELECTED_CONNECTOR_CLASS: 'selectedConnectorClass', 24 | } 25 | 26 | export const INITIAL_STATE = { 27 | [Payload.OPENED]: false, 28 | [Payload.AVAILABLE_CONNECTORS]: [], 29 | [Payload.SELECTED_CONNECTOR_CLASS]: null, 30 | } 31 | 32 | export const handler = handleActions({ 33 | /** open editor dialog to edit */ 34 | [ActionType.SUCCEED_TO_OPEN_CREATE_EDITOR]: (state, { payload, }) => 35 | Object.assign({}, INITIAL_STATE, { 36 | [Payload.OPENED]: true, 37 | [Payload.AVAILABLE_CONNECTORS]: payload[Payload.AVAILABLE_CONNECTORS], 38 | }), 39 | 40 | [ActionType.SUCCEED_TO_CHANGE_SELECTED_CONNECTOR_CLASS]: (state, { payload, }) => 41 | Object.assign({}, state, { 42 | [Payload.SELECTED_CONNECTOR_CLASS]: payload[Payload.SELECTED_CONNECTOR_CLASS], 43 | }), 44 | 45 | [ActionType.CLOSE_CREATE_EDITOR]: (state) => 46 | Object.assign({}, INITIAL_STATE, { 47 | [Payload.OPENED]: false, 48 | }), 49 | 50 | }, INITIAL_STATE) 51 | 52 | -------------------------------------------------------------------------------- /kafkalot-ui/src/reducers/ConnectorReducer/PaginatorState.js: -------------------------------------------------------------------------------- 1 | import { createAction, handleActions, } from 'redux-actions' 2 | 3 | import { CommonActionType, } from './ConnectorActionType' 4 | 5 | export const ActionType = { 6 | CHANGE_PAGE_OFFSET: 'CONNECTOR/PAGINATOR/CHANGE_PAGE_OFFSET', 7 | CHANGE_PAGE_ITEM_COUNT: 'CONNECTOR/PAGINATOR/CHANGE_PAGE_ITEM_COUNT', 8 | } 9 | 10 | export const Action = { 11 | changePageOffset: createAction(ActionType.CHANGE_PAGE_OFFSET), 12 | changePageItemCount: createAction(ActionType.CHANGE_PAGE_ITEM_COUNT), 13 | } 14 | 15 | export const Property = { 16 | PAGE_OFFSET: 'pageOffset', 17 | ITEM_OFFSET: 'itemOffset', 18 | ITEM_COUNT_PER_PAGE: 'itemCountPerPage', 19 | AVAILABLE_ITEM_COUNTS_PER_PAGE: 'availableItemCountsPerPage', 20 | } 21 | 22 | const AvailableItemCountsPerPage = [ 10, 25, 50, 100, ] 23 | 24 | export const INITIAL_STATE = { 25 | [Property.PAGE_OFFSET]: 0, 26 | [Property.ITEM_OFFSET]: 0, 27 | [Property.ITEM_COUNT_PER_PAGE]: AvailableItemCountsPerPage[0], 28 | [Property.AVAILABLE_ITEM_COUNTS_PER_PAGE]: AvailableItemCountsPerPage, 29 | } 30 | 31 | export const handler = handleActions({ 32 | 33 | /** reset offset if the filter keyword changes */ 34 | [CommonActionType.CHANGE_FILTER_KEYWORD]: (state) => { 35 | return Object.assign({}, state, { 36 | [Property.PAGE_OFFSET]: INITIAL_STATE[Property.PAGE_OFFSET], 37 | [Property.ITEM_OFFSET]: INITIAL_STATE[Property.ITEM_OFFSET], 38 | }) 39 | }, 40 | 41 | /** reset offset if the sorter changes */ 42 | [CommonActionType.CHANGE_SORTER]: (state) => { 43 | return Object.assign({}, state, { 44 | [Property.PAGE_OFFSET]: INITIAL_STATE[Property.PAGE_OFFSET], 45 | [Property.ITEM_OFFSET]: INITIAL_STATE[Property.ITEM_OFFSET], 46 | }) 47 | }, 48 | 49 | [ActionType.CHANGE_PAGE_OFFSET]: (state, { payload, }) => { 50 | const newPageOffset = payload[Property.PAGE_OFFSET] 51 | const currentItemOffset = newPageOffset * state[Property.ITEM_COUNT_PER_PAGE] 52 | 53 | return Object.assign({}, state, { 54 | [Property.PAGE_OFFSET]: newPageOffset, 55 | [Property.ITEM_OFFSET]: currentItemOffset, 56 | }) 57 | }, 58 | 59 | [ActionType.CHANGE_PAGE_ITEM_COUNT]: (state, { payload, }) => { 60 | const pageOffset = INITIAL_STATE[Property.PAGE_OFFSET] /** reset */ 61 | const newPageItemCount = payload[Property.ITEM_COUNT_PER_PAGE] 62 | const currentItemOffset = pageOffset * newPageItemCount 63 | 64 | return Object.assign({}, state, { 65 | [Property.PAGE_OFFSET]: pageOffset, 66 | [Property.ITEM_OFFSET]: currentItemOffset, 67 | [Property.ITEM_COUNT_PER_PAGE]: newPageItemCount, 68 | }) 69 | }, 70 | 71 | }, INITIAL_STATE) 72 | -------------------------------------------------------------------------------- /kafkalot-ui/src/reducers/ConnectorReducer/RemoveDialogState.js: -------------------------------------------------------------------------------- 1 | import { createAction, handleActions, } from 'redux-actions' 2 | 3 | export const ActionType = { 4 | SUCCEED_TO_OPEN_REMOVE_DIALOG: 'CONNECTOR/REMOVE_DIALOG/SUCCEED_TO_OPEN', 5 | CLOSE_REMOVE_DIALOG: 'CONNECTOR/REMOVE_DIALOG/CLOSE', 6 | } 7 | 8 | export const Action = { 9 | closeRemoveDialog: createAction(ActionType.CLOSE_REMOVE_DIALOG), 10 | } 11 | 12 | export const PrivateAction = { 13 | succeededToOpenRemoveDialog: createAction(ActionType.SUCCEED_TO_OPEN_REMOVE_DIALOG), 14 | } 15 | 16 | export const Property = { 17 | OPENED: 'opened', 18 | CONNECTOR_NAMES: 'connectorNames', 19 | } 20 | 21 | export const INITIAL_STATE = { 22 | [Property.OPENED]: false, 23 | [Property.CONNECTOR_NAMES]: [], 24 | } 25 | 26 | export const handler = handleActions({ 27 | [ActionType.SUCCEED_TO_OPEN_REMOVE_DIALOG]: (state, { payload, }) => 28 | Object.assign({}, state, { 29 | [Property.OPENED]: true, 30 | [Property.CONNECTOR_NAMES]: payload[Property.CONNECTOR_NAMES], 31 | }), 32 | 33 | [ActionType.CLOSE_REMOVE_DIALOG]: (state) => 34 | Object.assign({}, INITIAL_STATE), /** reset */ 35 | }, INITIAL_STATE) 36 | 37 | -------------------------------------------------------------------------------- /kafkalot-ui/src/reducers/ConnectorReducer/Selector.js: -------------------------------------------------------------------------------- 1 | import { ROOT, CONNECTOR, } from '../../constants/State' 2 | import { ConnectorListProperty, ConnectorProperty, } from './ConnectorListState' 3 | import { Property as StorageSelectorProperty, } from './StorageSelectorState' 4 | 5 | export function findConnector(state, connectorName) { 6 | const connectors = state[ROOT.CONNECTOR][CONNECTOR.CONNECTOR_LIST][ConnectorListProperty.CONNECTORS] 7 | const found = connectors.filter(c => c[ConnectorProperty.NAME] === connectorName) 8 | 9 | if (found) return found[0] 10 | else return null 11 | } 12 | 13 | export function getCheckedConnectors(state) { 14 | const connectors = state[ROOT.CONNECTOR][CONNECTOR.CONNECTOR_LIST][ConnectorListProperty.CONNECTORS] 15 | return connectors.filter(c => c[ConnectorProperty.CHECKED] === true) 16 | } 17 | 18 | export function getCurrentSorter(state) { 19 | return state[ROOT.CONNECTOR][CONNECTOR.CONNECTOR_LIST][ConnectorListProperty.SORTER] 20 | } 21 | 22 | export function getSelectedStorage(state) { 23 | return state[ROOT.CONNECTOR][CONNECTOR.STORAGE_SELECTOR][StorageSelectorProperty.SELECTED_STORAGE] 24 | } 25 | -------------------------------------------------------------------------------- /kafkalot-ui/src/reducers/ConnectorReducer/StorageSelectorState.js: -------------------------------------------------------------------------------- 1 | import { createAction, handleActions, } from 'redux-actions' 2 | 3 | import * as URL from '../../middlewares/Url' 4 | 5 | export const ActionType = { 6 | SET_STORAGE: 'CONNECTOR/STORAGE_SELECTOR/SET_STORAGE', 7 | } 8 | 9 | export const Payload = { 10 | STORAGE: 'storage', 11 | } 12 | 13 | export const Property = { 14 | SELECTED_STORAGE: 'selectedStorage', 15 | AVAILABLE_STORAGES: 'availableStorages', 16 | } 17 | 18 | export const INITIAL_STATE = { 19 | [Property.SELECTED_STORAGE]: URL.INITIAL_STORAGE_NAME, 20 | [Property.AVAILABLE_STORAGES]: URL.STORAGE_NAMES, 21 | } 22 | 23 | export const handler = handleActions({ 24 | [ActionType.SET_STORAGE]: (state, { payload, }) => { 25 | return Object.assign({}, state, { [Property.SELECTED_STORAGE]: payload[Payload.STORAGE], }) 26 | }, 27 | 28 | }, INITIAL_STATE) 29 | -------------------------------------------------------------------------------- /kafkalot-ui/src/reducers/ConnectorReducer/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers, } from 'redux' 2 | import { handleActions, } from 'redux-actions' 3 | 4 | import * as ConnectorListState from './ConnectorListState' 5 | import * as StorageSelectorState from './StorageSelectorState' 6 | import * as ConfigSchemaState from './ConfigSchemaState' 7 | import * as PaginatorState from './PaginatorState' 8 | 9 | import * as CreateEditorState from './CreateEditorState' 10 | import * as ConfigEditorState from './ConfigEditorState' 11 | import * as RemoveDialogState from './RemoveDialogState' 12 | import * as ClosableSnackbarState from './ClosableSnackbarState' 13 | 14 | import { CONNECTOR, } from '../../constants/State' 15 | 16 | export default combineReducers({ 17 | [CONNECTOR.CONNECTOR_LIST]: ConnectorListState.handler, 18 | [CONNECTOR.CONFIG_SCHEMA]: ConfigSchemaState.handler, 19 | [CONNECTOR.PAGINATOR]: PaginatorState.handler, 20 | [CONNECTOR.STORAGE_SELECTOR]: StorageSelectorState.handler, 21 | 22 | [CONNECTOR.CONFIG_EDITOR]: ConfigEditorState.handler, 23 | [CONNECTOR.CREATE_EDITOR]: CreateEditorState.handler, 24 | [CONNECTOR.REMOVE_DIALOG]: RemoveDialogState.handler, 25 | [CONNECTOR.SNACKBAR]: ClosableSnackbarState.handler, 26 | }) 27 | -------------------------------------------------------------------------------- /kafkalot-ui/src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers, } from 'redux' 2 | import { routerReducer, } from 'react-router-redux' 3 | 4 | import { ROOT, } from '../constants/State' 5 | import ConnectorReducer from './ConnectorReducer' 6 | 7 | const rootReducer = combineReducers({ 8 | [ROOT.CONNECTOR]: ConnectorReducer, 9 | [ROOT.ROUTING]: routerReducer, 10 | }) 11 | 12 | export default rootReducer 13 | 14 | 15 | -------------------------------------------------------------------------------- /kafkalot-ui/src/routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Route, IndexRoute, } from 'react-router' 3 | 4 | import App from './components/Common/App' 5 | import * as Page from './constants/Page' 6 | import MainPage from './containers/MainPage' 7 | import ConnectorPage from './containers/ConnectorPage' 8 | import NotFoundPage from './components/Common/NotFoundPage' 9 | 10 | export default ( 11 | 12 | 13 | 14 | 15 | 16 | 17 | ) 18 | -------------------------------------------------------------------------------- /kafkalot-ui/src/store/configureStore.dev.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose, combineReducers, } from 'redux' 2 | import { routerReducer, } from 'react-router-redux' 3 | import createSagaMiddleware from 'redux-saga' 4 | 5 | import RootReducer from '../reducers' 6 | import sagas from '../middlewares/Saga' 7 | 8 | const sagaMiddleware = createSagaMiddleware() 9 | 10 | const middlewares = [sagaMiddleware,] 11 | 12 | export default function configureStore(initialState) { 13 | let store 14 | if (window.devToolsExtension) { 15 | store = createStore( 16 | RootReducer, 17 | initialState, 18 | compose( 19 | applyMiddleware(...middlewares), 20 | window.devToolsExtension ? window.devToolsExtension() : f => f 21 | )) 22 | } else { 23 | store = createStore( 24 | RootReducer, 25 | initialState, 26 | applyMiddleware(...middlewares) 27 | ) 28 | } 29 | 30 | if (module.hot) { 31 | module.hot.accept('../reducers', () => { 32 | const nextReducer = require('../reducers') 33 | store.replaceReducer(nextReducer) 34 | }) 35 | } 36 | 37 | sagaMiddleware.run(sagas) 38 | 39 | return store 40 | } 41 | -------------------------------------------------------------------------------- /kafkalot-ui/src/store/configureStore.js: -------------------------------------------------------------------------------- 1 | import { NODE_ENV, ENV_PROD, } from '../constants/Config' 2 | 3 | if (NODE_ENV === ENV_PROD) { module.exports = require('./configureStore.prod') } 4 | else { module.exports = require('./configureStore.dev') } 5 | -------------------------------------------------------------------------------- /kafkalot-ui/src/store/configureStore.prod.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, combineReducers, } from 'redux' 2 | import { routerReducer, } from 'react-router-redux' 3 | import createSagaMiddleware from 'redux-saga' 4 | 5 | import RootReducer from '../reducers' 6 | import sagas from '../middlewares/Saga' 7 | 8 | const sagaMiddleware = createSagaMiddleware() 9 | 10 | const middlewares = [sagaMiddleware,] 11 | 12 | export default function configureStore(initialState) { 13 | const store = createStore( 14 | RootReducer, 15 | initialState, 16 | applyMiddleware(...middlewares) 17 | ) 18 | 19 | sagaMiddleware.run(sagas) 20 | 21 | return store 22 | } 23 | -------------------------------------------------------------------------------- /kafkalot-ui/src/util/Logger.js: -------------------------------------------------------------------------------- 1 | import { NODE_ENV, } from '../constants/Config' 2 | 3 | export function isLoggerDisabled() { 4 | return (NODE_ENV === 'test') 5 | } 6 | 7 | export const Tag = { 8 | ERROR: '[ERROR]', 9 | WARN: '[WARN] ', 10 | INFO: '[INFO] ', 11 | } 12 | 13 | export function error(message, error) { 14 | if (isLoggerDisabled()) return 15 | 16 | /* eslint-disable no-console */ 17 | console.error(`${Tag.ERROR}: ${message}`) 18 | 19 | if (error !== void 0) console.error(error.stack) 20 | /* eslint-enable no-console */ 21 | } 22 | 23 | export function warn(message) { 24 | if (isLoggerDisabled()) return 25 | 26 | /* eslint-disable no-console */ 27 | console.warn(`${Tag.WARN}: ${message}`) 28 | /* eslint-enable no-console */ 29 | } 30 | 31 | export function info(message) { 32 | if (isLoggerDisabled()) return 33 | 34 | /* eslint-disable no-console */ 35 | console.info(`${Tag.INFO}: ${message}`) 36 | /* eslint-enable no-console */ 37 | } 38 | -------------------------------------------------------------------------------- /kafkalot-ui/src/util/SchemaUtil.js: -------------------------------------------------------------------------------- 1 | import * as Logger from './Logger' 2 | import { Code as ErrorCode, } from '../constants/Error' 3 | 4 | export function extractConnectorClassFromConfig(name, config) { 5 | let connectorClass = undefined 6 | 7 | try { 8 | connectorClass = config['connector.class'] 9 | } catch (error) { 10 | Logger.warn(`${ErrorCode.CONNECTOR_CONFIG_HAS_NO_CONNECTOR_CLASS_FIELD} (${name}`) 11 | } 12 | 13 | return connectorClass 14 | } 15 | 16 | export const InitialConnectorConfig = { 17 | 'connector.class': 'io.github.1ambda.ExampleSinkConnector', 18 | 'tasks.max': '4', 19 | 'topics': 'example-topic', 20 | 'name': 'example-connector', 21 | } 22 | 23 | export const defaultConnectorConfigSchema = { 24 | '$schema': 'http://json-schema.org/draft-04/schema#', 25 | 'title': 'Default Schema for Connector', 26 | 'description': 'Default Schema for Connector', 27 | 'type': 'object', 28 | 'properties': { 29 | 'name': { 'type': 'string', }, 30 | 'connector.class': { 'type': 'string', }, 31 | 'topics': { 'type': 'string', }, 32 | 'tasks.max': {'type': 'string', }, 33 | }, 34 | 'required': [ 'name', 'connector.class', 'topics', 'tasks.max', ], 35 | } 36 | 37 | export function createConnectorClassSchemaInCreateDialog(configSchema) { 38 | const title = configSchema.title 39 | const $schema = configSchema.$schema 40 | const description = configSchema.description 41 | 42 | delete configSchema.title 43 | delete configSchema.$schema 44 | delete configSchema.description 45 | 46 | let schema = { 47 | 'title': title, 48 | '$schema': $schema, 49 | 'description': description, 50 | 'type': 'object', 51 | properties: { 52 | 'name': { 'type': 'string', }, 53 | 'config': configSchema, 54 | }, 55 | 'required': [ 'name', 'config', ], 56 | } 57 | 58 | return schema 59 | } 60 | -------------------------------------------------------------------------------- /kafkalot-ui/tools/BuildBundle.js: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack' 2 | import webpackConfigBuilder from '../webpack.config' 3 | import colors from 'colors' 4 | import { argv as args, } from 'yargs' 5 | 6 | import * as Logger from './BuildLogger' 7 | import { OUTPUT_DIR, ENV_PROD, } from './BuildConfig' 8 | 9 | const webpackConfig = webpackConfigBuilder(ENV_PROD) 10 | 11 | const TAG = 'BuildBundle' 12 | 13 | webpack(webpackConfig).run((err, stats) => { 14 | const inSilentMode = args.s 15 | 16 | if (err) { 17 | Logger.error(TAG, err) 18 | return 1 19 | } 20 | 21 | const jsonStats = stats.toJson() 22 | 23 | if (jsonStats.hasErrors) { 24 | return jsonStats.errors.map(error => Logger.error(TAG, error)) 25 | } 26 | 27 | if (jsonStats.hasWarnings && !inSilentMode) { 28 | Logger.info(TAG, 'Webpack generated the following warnings: '.bold.yellow) 29 | jsonStats.warnings.map(warning => Logger.warn(warning)) 30 | } 31 | 32 | Logger.info(TAG, `Build Done. \n${OUTPUT_DIR}\n`.green.bold) 33 | 34 | return 0 35 | }) 36 | -------------------------------------------------------------------------------- /kafkalot-ui/tools/BuildConfig.js: -------------------------------------------------------------------------------- 1 | import * as path from 'path' 2 | 3 | export const ENV_DEV = 'development' 4 | export const ENV_PROD = 'production' 5 | export const ENV_TEST = 'test' 6 | 7 | export const OUTPUT_DIR = path.join(__dirname, '../../kafkalot-storage/src/main/resources/public') 8 | -------------------------------------------------------------------------------- /kafkalot-ui/tools/BuildHtml.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs-extra' 2 | import cheerio from 'cheerio' 3 | 4 | import * as Logger from './BuildLogger' 5 | import { OUTPUT_DIR, } from './BuildConfig' 6 | 7 | const ENCODING = 'utf8' 8 | const TAG = 'BuildHtml' 9 | 10 | /** copy bower_component dir to dist dir */ 11 | const bowerDir = JSON.parse(fs.readFileSync('./.bowerrc', ENCODING)).directory 12 | const bowerDirName = bowerDir.substring(bowerDir.lastIndexOf('/') + 1, bowerDir.length) 13 | const outputBowerDir = `${OUTPUT_DIR}/${bowerDirName}` 14 | 15 | Logger.info(TAG, `copying ${bowerDir} to ${outputBowerDir}`) 16 | fs.copySync(bowerDir, outputBowerDir) 17 | 18 | /** write index.html */ 19 | const html = fs.readFileSync('src/index.html', ENCODING) 20 | const $ = cheerio.load(html) 21 | 22 | Logger.info(TAG, `index.html written to ${OUTPUT_DIR}`) 23 | fs.writeFileSync(`${OUTPUT_DIR}/index.html`, $.html(), ENCODING) 24 | -------------------------------------------------------------------------------- /kafkalot-ui/tools/BuildLogger.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment' 2 | import colors from 'colors' 3 | 4 | const prefix = 'KAFKALOT-UI' 5 | 6 | function getNow() { 7 | return moment().format("YYYY-MM-DD HH:mm:ss:SSS") 8 | } 9 | 10 | export function info(tag, message) { 11 | const now = getNow() 12 | 13 | const TAG = (tag === '' || tag === void 0) ? '': `${tag.magenta}|` 14 | console.log(`[${prefix.blue}|${TAG}${now.cyan}] (${'INFO'.green}) ${message}`) // eslint-disable-line no-console 15 | } 16 | 17 | export function warn(tag, message) { 18 | const now = getNow() 19 | 20 | const TAG = (tag === '' || tag === void 0) ? '': `${tag.magenta}|` 21 | console.log(`[${prefix.blue}|${TAG}${now.cyan}] (${'WARN'.yellow}) ${message}`) // eslint-disable-line no-console 22 | } 23 | 24 | export function error(tag, message) { 25 | const now = getNow() 26 | const TAG = (tag === '' || tag === void 0) ? '': `${tag.magenta}|` 27 | 28 | console.log(`[${prefix.blue}|${TAG}${now.cyan}] (${'ERROR'.red}) ${message}`) // eslint-disable-line no-console 29 | } 30 | -------------------------------------------------------------------------------- /kafkalot-ui/tools/Clean.js: -------------------------------------------------------------------------------- 1 | import { OUTPUT_DIR, } from './BuildConfig' 2 | 3 | const mkdirp = require('mkdirp') 4 | const rimraf = require('rimraf') 5 | 6 | rimraf.sync(OUTPUT_DIR) 7 | mkdirp.sync(OUTPUT_DIR) -------------------------------------------------------------------------------- /kafkalot-ui/tools/Config.js: -------------------------------------------------------------------------------- 1 | import { ENV_DEV, ENV_PROD, ENV_TEST, } from './BuildConfig' 2 | import * as DEV_CONFIG from '../config/development.config' 3 | import * as PROD_CONFIG from '../config/production.config' 4 | 5 | const env = process.env.NODE_ENV 6 | 7 | export const CONFIG = (env === ENV_DEV) ? DEV_CONFIG : PROD_CONFIG 8 | 9 | export const GLOBAL_VARIABLES = { /** used by Webpack.DefinePlugin */ 10 | 'process.env.ENV_DEV': JSON.stringify(ENV_DEV), 11 | 'process.env.ENV_PROD': JSON.stringify(ENV_PROD), 12 | 'process.env.NODE_ENV': JSON.stringify(env), 13 | 14 | /** variables defined in `CONFIG` file ares already stringified */ 15 | 'process.env.STORAGES': CONFIG.STORAGES, 16 | 'process.env.TITLE': CONFIG.TITLE, 17 | } 18 | -------------------------------------------------------------------------------- /kafkalot-ui/tools/DevServer.js: -------------------------------------------------------------------------------- 1 | import browserSync from 'browser-sync' 2 | 3 | import historyApiFallback from 'connect-history-api-fallback' 4 | import webpack from 'webpack' 5 | import webpackDevMiddleware from 'webpack-dev-middleware' 6 | import webpackHotMiddleware from 'webpack-hot-middleware' 7 | import webpackConfigBuilder from '../webpack.config' 8 | 9 | const webpackConfig = webpackConfigBuilder('development') 10 | const bundler = webpack(webpackConfig) 11 | 12 | browserSync.init({ 13 | server: { 14 | baseDir: ['src', 'lib', ], 15 | 16 | middleware: [ 17 | webpackDevMiddleware(bundler, { 18 | publicPath: webpackConfig.output.publicPath, 19 | stats: { colors: true, chunks: false, }, 20 | noInfo: true, 21 | }), 22 | webpackHotMiddleware(bundler), 23 | historyApiFallback(), 24 | ], 25 | }, 26 | 27 | files: [ 28 | 'src/*.html', 29 | ], 30 | }) 31 | -------------------------------------------------------------------------------- /kafkalot-ui/tools/ProdServer.js: -------------------------------------------------------------------------------- 1 | import browserSync from 'browser-sync' 2 | import historyApiFallback from 'connect-history-api-fallback' 3 | 4 | import { OUTPUT_DIR, } from './BuildConfig' 5 | 6 | browserSync.init({ 7 | port: 3000, 8 | ui: { port: 3001, }, 9 | server: { 10 | baseDir: [ OUTPUT_DIR, ], 11 | }, 12 | 13 | files: [ 'src/*.html', ], 14 | 15 | middleware: [ 16 | historyApiFallback(), 17 | ], 18 | }) 19 | -------------------------------------------------------------------------------- /kafkalot-ui/webpack.config.js: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack' 2 | import prettyjson from 'prettyjson' 3 | import path from 'path' 4 | import ExtractTextPlugin from 'extract-text-webpack-plugin' 5 | 6 | import { GLOBAL_VARIABLES, } from './tools/Config' 7 | import { OUTPUT_DIR, ENV_DEV, ENV_PROD, ENV_TEST, } from './tools/BuildConfig' 8 | import * as Logger from './tools/BuildLogger' 9 | 10 | 11 | const TAG = 'WebpackConfig' 12 | Logger.info(TAG, `GLOBAL VARIABLES \n${prettyjson.render(GLOBAL_VARIABLES)}`) 13 | 14 | const getPlugins = function (env) { 15 | const plugins = [ 16 | new webpack.optimize.OccurenceOrderPlugin(), 17 | new webpack.DefinePlugin(GLOBAL_VARIABLES), 18 | ] 19 | 20 | switch (env) { 21 | case ENV_PROD: 22 | plugins.push(new ExtractTextPlugin('styles.css')) 23 | plugins.push(new webpack.optimize.DedupePlugin()) 24 | plugins.push(new webpack.optimize.UglifyJsPlugin()) 25 | break 26 | 27 | case ENV_DEV: 28 | plugins.push(new webpack.HotModuleReplacementPlugin()) 29 | plugins.push(new webpack.NoErrorsPlugin()) 30 | break 31 | } 32 | 33 | return plugins 34 | } 35 | 36 | const getEntry = function (env) { 37 | const entry = [] 38 | 39 | if (env === ENV_DEV) entry.push('webpack-hot-middleware/client') 40 | 41 | entry.push('./src/index') 42 | 43 | return entry 44 | } 45 | 46 | const getPostcssPlugins = function (env) { 47 | 48 | let browsers = ['last 10 version', '> 5%', 'ie >= 8',] 49 | 50 | let plugins = [ 51 | require('postcss-url')({ 52 | copy: 'rebase', 53 | }), 54 | require('postcss-cssnext')({ 55 | browsers: browsers, 56 | }), 57 | require('postcss-reporter')({ 58 | clearMessages: true, 59 | }), 60 | require('postcss-import')(), 61 | ] 62 | 63 | return plugins 64 | } 65 | 66 | 67 | const getLoaders = function (env) { 68 | const loaders = [ 69 | { 70 | test: /\.js$/, 71 | include: path.join(__dirname, 'src'), 72 | loaders: ['babel', 'eslint',], 73 | }, 74 | { /** globally used css in node_modules */ 75 | test: /(\.css)$/, 76 | include: [ path.join(__dirname, 'node_modules'), ], 77 | loaders: ['style', 'css?sourceMap&importLoaders=1', 'postcss',], 78 | }, 79 | { /** globally used css in src */ 80 | test: /global\.css/, 81 | include: [ path.join(__dirname, 'src'), /** global css only */ ], 82 | loaders: ['style', 'css?sourceMap&importLoaders=1', 'postcss',], 83 | }, 84 | { 85 | test: /\.woff(\?\S*)?$/, 86 | loader: 'url-loader?limit=10000&minetype=application/font-woff', 87 | }, 88 | { 89 | test: /\.woff2(\?\S*)?$/, 90 | loader: 'url-loader?limit=10000&minetype=application/font-woff', 91 | }, 92 | { 93 | test: /\.eot(\?\S*)?$/, 94 | loader: 'url-loader', 95 | }, { 96 | test: /\.ttf(\?\S*)?$/, 97 | loader: 'url-loader', 98 | }, 99 | { 100 | test: /\.svg(\?\S*)?$/, 101 | loader: 'url-loader', 102 | }, 103 | ] 104 | 105 | if (env === ENV_PROD) { 106 | loaders.push({ 107 | test: /(\.css)$/, 108 | include: path.join(__dirname, 'src'), 109 | exclude: /global\.css/, 110 | loader: ExtractTextPlugin.extract(['style', 'css?sourceMap&module&importLoaders=1', 'postcss',]), 111 | }) 112 | } else { 113 | loaders.push({ 114 | test: /(\.css)$/, 115 | include: path.join(__dirname, 'src'), 116 | exclude: /global\.css/, 117 | loaders: ['style', 'css?sourceMap&module&importLoaders=1', 'postcss',], 118 | }) 119 | } 120 | 121 | return loaders 122 | } 123 | 124 | function getConfig(env) { 125 | return { 126 | debug: true, 127 | devtool: env === ENV_PROD? 'source-map' : 'eval-cheap-module-source-map', // more info:https://webpack.github.io/docs/build-performance.html#sourcemaps and https://webpack.github.io/docs/configuration.html#devtool 128 | entry: getEntry(env), 129 | target: env === ENV_TEST? 'node' : 'web', 130 | output: { 131 | path: OUTPUT_DIR, 132 | publicPath: '', 133 | filename: 'bundle.js', 134 | }, 135 | externals: { 136 | //required: 'variable', 137 | }, 138 | plugins: getPlugins(env), 139 | module: { loaders: getLoaders(env), }, 140 | postcss: getPostcssPlugins(), 141 | 142 | /** suppress error shown in console, so it has to be set to false */ 143 | quiet: false, 144 | noInfo: true, 145 | } 146 | } 147 | 148 | export default getConfig 149 | -------------------------------------------------------------------------------- /project/Dep.scala: -------------------------------------------------------------------------------- 1 | import sbt._ 2 | import Keys._ 3 | 4 | object Dep { 5 | 6 | object V { 7 | val SCALA_TEST = "2.2.4" 8 | val FINCH = "0.11.0-M2" 9 | val TWITTER_SERVER = "1.19.0" 10 | val SHAPELESS = "2.3.0" 11 | val CIRCLE = "0.5.0-M2" 12 | val MONGO_CASBAH = "3.1.1" 13 | val SALAT = "1.9.9" 14 | 15 | val TYPESAFE_CONFIG = "1.3.0" 16 | val FICUS = "1.2.3" 17 | val TYPESAFE_SCALA_LOGGING = "3.4.0" 18 | val SLF4j = "1.7.21" 19 | val LOGBACK = "1.1.7" 20 | } 21 | 22 | val STORAGE = Def.setting(Seq( 23 | "org.scalatest" %% "scalatest" % V.SCALA_TEST % "test" 24 | , "com.github.finagle" %% "finch-core" % V.FINCH 25 | , "com.github.finagle" %% "finch-circe" % V.FINCH 26 | , "com.github.finagle" %% "finch-test" % V.FINCH 27 | , "com.chuusai" %% "shapeless" % V.SHAPELESS 28 | , "io.circe" %% "circe-core" % V.CIRCLE 29 | , "io.circe" %% "circe-generic" % V.CIRCLE 30 | , "io.circe" %% "circe-parser" % V.CIRCLE 31 | , "com.novus" %% "salat" % V.SALAT 32 | , "org.mongodb" %% "casbah" % V.MONGO_CASBAH 33 | , "com.typesafe" % "config" % V.TYPESAFE_CONFIG 34 | , "com.iheart" %% "ficus" % V.FICUS 35 | , "com.typesafe.scala-logging" %% "scala-logging" % V.TYPESAFE_SCALA_LOGGING 36 | , "org.slf4j" % "slf4j-api" % V.SLF4j 37 | , "ch.qos.logback" % "logback-classic" % V.LOGBACK 38 | )) 39 | } -------------------------------------------------------------------------------- /project/NpmPlugin.scala: -------------------------------------------------------------------------------- 1 | import sbt._ 2 | import sbt.Keys._ 3 | 4 | object NpmPlugin extends AutoPlugin { 5 | object autoImport { 6 | val npm = NpmPluginKeys.npm 7 | var targetDirectory = NpmPluginKeys.targetDirectory 8 | val npmTasks = NpmPluginKeys.npmTasks 9 | 10 | type NpmTask = NpmTypes.NpmTask 11 | val NpmTask = NpmTypes.NpmTask 12 | type NpmEnv = NpmTypes.NpmEnv 13 | val NpmEnv = NpmTypes.NpmEnv 14 | } 15 | 16 | override def projectSettings = NpmSettings.baseNpmSettings 17 | } 18 | 19 | object NpmPluginKeys { 20 | import NpmTypes. _ 21 | 22 | val npm = taskKey[Unit]("NPM task") 23 | val targetDirectory = SettingKey[File]("Target Directory where npm command will be executed.") 24 | var npmTasks = SettingKey[Seq[NpmTask]]("NPM tasks to be executed.") 25 | } 26 | 27 | object NpmSettings { 28 | import NpmPluginKeys._ 29 | import NpmTypes._ 30 | 31 | def createEnvString(envs: Seq[NpmEnv]): String = { 32 | if (envs.isEmpty) "" 33 | else envs.map(env => s"${env.key}=${env.value}").mkString(" ") 34 | } 35 | 36 | def createEnvTuples(envs: Seq[NpmEnv]): Seq[(String, String)] = { 37 | envs.map { env => (env.key, env.value) } 38 | } 39 | 40 | lazy val baseNpmSettings = Seq( 41 | npm := { 42 | val log = streams.value.log 43 | val targetDirectory = (NpmPluginKeys.targetDirectory in npm).value 44 | val npmTasks = (NpmPluginKeys.npmTasks in npm).value 45 | 46 | log.info(s"NPM TargetDirectory ${targetDirectory.getAbsolutePath}") 47 | log.info(s"NPM Tasks: ${npmTasks}") 48 | 49 | npmTasks.map { task => 50 | val command = task.command 51 | val envString = createEnvString(task.envs) 52 | val commandString = if (envString.isEmpty) s"npm ${command}" else s"${envString} npm ${command}" 53 | 54 | log.info(s"Executing: ${commandString}") 55 | val processResult = Process( 56 | s"npm ${command}", 57 | targetDirectory, 58 | createEnvTuples(task.envs):_* 59 | ).! 60 | 61 | if (processResult != 0) { 62 | throw new Exception(s"Failed: ${commandString}") 63 | } 64 | } 65 | 66 | } 67 | ) 68 | } 69 | 70 | object NpmTypes { 71 | case class NpmTask(command: String, envs: Seq[NpmEnv] = Seq()) 72 | case class NpmEnv(key: String, value: String) 73 | } 74 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | logLevel := Level.Warn 2 | 3 | addSbtPlugin("io.spray" % "sbt-revolver" % "0.8.0") 4 | 5 | addSbtPlugin("com.geirsson" %% "sbt-scalafmt" % "0.2.3") 6 | 7 | addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.1.1") 8 | 9 | addSbtPlugin("se.marcuslonnberg" % "sbt-docker" % "1.4.0") 10 | -------------------------------------------------------------------------------- /with-kafka.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | zk: 4 | image: 31z4/zookeeper:3.4.8 5 | 6 | kafka: 7 | image: ches/kafka 8 | links: 9 | - zk 10 | environment: 11 | KAFKA_BROKER_ID: 0 12 | KAFKA_ADVERTISED_HOST_NAME: kafka 13 | KAFKA_ADVERTISED_PORT: 9092 14 | ZOOKEEPER_CONNECTION_STRING: zk:2181 15 | ZOOKEEPER_CHROOT: /brokers-0 16 | 17 | connect: 18 | image: 1ambda/kafka-connect 19 | links: 20 | - kafka 21 | ports: 22 | - "8083" 23 | environment: 24 | CONNECT_BOOTSTRAP_SERVERS: kafka:9092 25 | CONNECT_GROUP_ID: connect-cluster-A 26 | 27 | storage: 28 | links: 29 | - connect 30 | environment: 31 | - KAFKALOT_STORAGE_CONNECTOR_CLUSTERHOST=connect 32 | - KAFKALOT_STORAGE_CONNECTOR_CLUSTERPORT=8083 33 | --------------------------------------------------------------------------------