├── .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 | [](https://travis-ci.org/1ambda/kafka-connect-dashboard) [](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 | 
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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------