Merge pull request #403 from provectus/feature/connect-views

Add connect views
This commit is contained in:
Marat Adiyatullin 2021-05-12 14:45:12 +03:00 committed by GitHub
commit 8b3b51fa31
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
72 changed files with 5365 additions and 277 deletions

View file

@ -53,7 +53,7 @@ public interface KafkaConnectMapper {
.type(triple.getLeft().getType())
.topics(getTopicsFromConfig.apply(triple.getMiddle()))
.status(
triple.getLeft().getStatus().getState()
triple.getLeft().getStatus()
)
.tasksCount(triple.getRight().size())
.failedTasksCount((int) triple.getRight().stream()

View file

@ -1615,6 +1615,8 @@ components:
type: string
address:
type: string
required:
- name
ConnectorConfig:
type: object
@ -1638,6 +1640,8 @@ components:
$ref: '#/components/schemas/TaskStatus'
config:
$ref: '#/components/schemas/ConnectorConfig'
required:
- status
NewConnector:
type: object
@ -1665,6 +1669,10 @@ components:
$ref: '#/components/schemas/ConnectorStatus'
connect:
type: string
required:
- type
- status
- connect
ConnectorType:
type: string
@ -1683,6 +1691,10 @@ components:
type: string
trace:
type: string
required:
- id
- state
- worker_id
ConnectorStatus:
type: object
@ -1691,6 +1703,8 @@ components:
$ref: '#/components/schemas/ConnectorTaskStatus'
worker_id:
type: string
required:
- state
ConnectorTaskStatus:
type: string
@ -1703,9 +1717,9 @@ components:
ConnectorAction:
type: string
enum:
- restart
- pause
- resume
- RESTART
- PAUSE
- RESUME
TaskAction:
type: string
@ -1823,8 +1837,12 @@ components:
items:
type: string
status:
$ref: '#/components/schemas/ConnectorTaskStatus'
$ref: '#/components/schemas/ConnectorStatus'
tasks_count:
type: integer
failed_tasks_count:
type: integer
required:
- name
- connect
- status

View file

@ -2,7 +2,7 @@
"$schema": "node_modules/@openapitools/openapi-generator-cli/config.schema.json",
"spaces": 2,
"generator-cli": {
"version": "5.0.1",
"version": "5.1.1",
"generators": {
"fetch": {
"generatorName": "typescript-fetch",
@ -14,6 +14,9 @@
"supportsES6": true,
"nullSafeAdditionalProps": true,
"withInterfaces": true
},
"typeMappings": {
"object": "any"
}
}
}

View file

@ -10,6 +10,7 @@
"dependencies": {
"@fortawesome/fontawesome-free": "^5.15.3",
"@hookform/error-message": "0.0.5",
"@hookform/resolvers": "^1.3.7",
"@rooks/use-outside-click-ref": "^4.10.1",
"ace-builds": "^1.4.12",
"bulma": "^0.9.2",
@ -35,7 +36,8 @@
"reselect": "^4.0.0",
"typesafe-actions": "^5.1.0",
"use-debounce": "^6.0.1",
"uuid": "^8.3.1"
"uuid": "^8.3.1",
"yup": "^0.32.9"
},
"devDependencies": {
"@jest/types": "^26.6.2",
@ -52,6 +54,7 @@
"@types/react-dom": "^17.0.2",
"@types/react-redux": "^7.1.11",
"@types/react-router-dom": "^5.1.6",
"@types/react-test-renderer": "^17.0.1",
"@types/redux-mock-store": "^1.0.2",
"@types/uuid": "^8.3.0",
"@typescript-eslint/eslint-plugin": "^4.20.0",
@ -77,6 +80,7 @@
"node-sass": "^5.0.0",
"prettier": "^2.2.1",
"react-scripts": "4.0.3",
"react-test-renderer": "^17.0.2",
"redux-mock-store": "^1.5.4",
"ts-jest": "^26.5.4",
"ts-node": "^9.1.1",
@ -1682,6 +1686,14 @@
"resolved": "https://registry.npmjs.org/@hookform/error-message/-/error-message-0.0.5.tgz",
"integrity": "sha512-es7eLLFA3SXNYAT8aUjvf7Gok1eMHK+9DMILtJA7ZEwYZlCCCPifhpoZmY+5SOopEtF7e+qxFOjX+MJnI3jOcg=="
},
"node_modules/@hookform/resolvers": {
"version": "1.3.7",
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-1.3.7.tgz",
"integrity": "sha512-gC06h1hky7gM31UO4Y7DrUQjgZyNIKA3s0/TxXbdeurwhOj07l8Zx8Pomjt93ANOKoZOU2rcHKaZtrEJx1w7tg==",
"peerDependencies": {
"react-hook-form": ">=6.6.0"
}
},
"node_modules/@hypnosphi/create-react-context": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/@hypnosphi/create-react-context/-/create-react-context-0.3.1.tgz",
@ -2952,8 +2964,7 @@
"node_modules/@types/lodash": {
"version": "4.14.168",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.168.tgz",
"integrity": "sha512-oVfRvqHV/V6D1yifJbVRU3TMp8OT6o6BG+U9MkwuJ3U8/CsDHvalRpsxBqivn71ztOFZBTfJMvETbqHiaNSj7Q==",
"dev": true
"integrity": "sha512-oVfRvqHV/V6D1yifJbVRU3TMp8OT6o6BG+U9MkwuJ3U8/CsDHvalRpsxBqivn71ztOFZBTfJMvETbqHiaNSj7Q=="
},
"node_modules/@types/minimatch": {
"version": "3.0.4",
@ -3080,6 +3091,15 @@
"@types/react-router": "*"
}
},
"node_modules/@types/react-test-renderer": {
"version": "17.0.1",
"resolved": "https://registry.npmjs.org/@types/react-test-renderer/-/react-test-renderer-17.0.1.tgz",
"integrity": "sha512-3Fi2O6Zzq/f3QR9dRnlnHso9bMl7weKCviFmfF6B4LS1Uat6Hkm15k0ZAQuDz+UBq6B3+g+NM6IT2nr5QgPzCw==",
"dev": true,
"dependencies": {
"@types/react": "*"
}
},
"node_modules/@types/redux-mock-store": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@types/redux-mock-store/-/redux-mock-store-1.0.2.tgz",
@ -14307,6 +14327,11 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"node_modules/lodash-es": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="
},
"node_modules/lodash._reinterpolate": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz",
@ -15222,6 +15247,11 @@
"integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==",
"dev": true
},
"node_modules/nanoclone": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/nanoclone/-/nanoclone-0.2.1.tgz",
"integrity": "sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA=="
},
"node_modules/nanoid": {
"version": "3.1.22",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.22.tgz",
@ -17973,6 +18003,11 @@
"react-is": "^16.8.1"
}
},
"node_modules/property-expr": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.4.tgz",
"integrity": "sha512-sFPkHQjVKheDNnPvotjQmm3KD3uk1fWKUN7CrpdbwmUx3CrG3QiM8QpTSimvig5vTXmTvjz7+TDvXOI9+4rkcg=="
},
"node_modules/proxy-addr": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz",
@ -21963,6 +21998,11 @@
"node": ">=0.6"
}
},
"node_modules/toposort": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz",
"integrity": "sha1-riF2gXXRVZ1IvvNUILL0li8JwzA="
},
"node_modules/tough-cookie": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz",
@ -24456,6 +24496,23 @@
"engines": {
"node": ">=10"
}
},
"node_modules/yup": {
"version": "0.32.9",
"resolved": "https://registry.npmjs.org/yup/-/yup-0.32.9.tgz",
"integrity": "sha512-Ci1qN+i2H0XpY7syDQ0k5zKQ/DoxO0LzPg8PAR/X4Mpj6DqaeCoIYEEjDJwhArh3Fa7GWbQQVDZKeXYlSH4JMg==",
"dependencies": {
"@babel/runtime": "^7.10.5",
"@types/lodash": "^4.14.165",
"lodash": "^4.17.20",
"lodash-es": "^4.17.15",
"nanoclone": "^0.2.1",
"property-expr": "^2.0.4",
"toposort": "^2.0.2"
},
"engines": {
"node": ">=10"
}
}
},
"dependencies": {
@ -25984,6 +26041,12 @@
"resolved": "https://registry.npmjs.org/@hookform/error-message/-/error-message-0.0.5.tgz",
"integrity": "sha512-es7eLLFA3SXNYAT8aUjvf7Gok1eMHK+9DMILtJA7ZEwYZlCCCPifhpoZmY+5SOopEtF7e+qxFOjX+MJnI3jOcg=="
},
"@hookform/resolvers": {
"version": "1.3.7",
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-1.3.7.tgz",
"integrity": "sha512-gC06h1hky7gM31UO4Y7DrUQjgZyNIKA3s0/TxXbdeurwhOj07l8Zx8Pomjt93ANOKoZOU2rcHKaZtrEJx1w7tg==",
"requires": {}
},
"@hypnosphi/create-react-context": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/@hypnosphi/create-react-context/-/create-react-context-0.3.1.tgz",
@ -27057,8 +27120,7 @@
"@types/lodash": {
"version": "4.14.168",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.168.tgz",
"integrity": "sha512-oVfRvqHV/V6D1yifJbVRU3TMp8OT6o6BG+U9MkwuJ3U8/CsDHvalRpsxBqivn71ztOFZBTfJMvETbqHiaNSj7Q==",
"dev": true
"integrity": "sha512-oVfRvqHV/V6D1yifJbVRU3TMp8OT6o6BG+U9MkwuJ3U8/CsDHvalRpsxBqivn71ztOFZBTfJMvETbqHiaNSj7Q=="
},
"@types/minimatch": {
"version": "3.0.4",
@ -27185,6 +27247,15 @@
"@types/react-router": "*"
}
},
"@types/react-test-renderer": {
"version": "17.0.1",
"resolved": "https://registry.npmjs.org/@types/react-test-renderer/-/react-test-renderer-17.0.1.tgz",
"integrity": "sha512-3Fi2O6Zzq/f3QR9dRnlnHso9bMl7weKCviFmfF6B4LS1Uat6Hkm15k0ZAQuDz+UBq6B3+g+NM6IT2nr5QgPzCw==",
"dev": true,
"requires": {
"@types/react": "*"
}
},
"@types/redux-mock-store": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@types/redux-mock-store/-/redux-mock-store-1.0.2.tgz",
@ -27299,13 +27370,13 @@
"dev": true
},
"@typescript-eslint/eslint-plugin": {
"version": "4.23.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.23.0.tgz",
"integrity": "sha512-tGK1y3KIvdsQEEgq6xNn1DjiFJtl+wn8JJQiETtCbdQxw1vzjXyAaIkEmO2l6Nq24iy3uZBMFQjZ6ECf1QdgGw==",
"version": "4.22.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.22.0.tgz",
"integrity": "sha512-U8SP9VOs275iDXaL08Ln1Fa/wLXfj5aTr/1c0t0j6CdbOnxh+TruXu1p4I0NAvdPBQgoPjHsgKn28mOi0FzfoA==",
"dev": true,
"requires": {
"@typescript-eslint/experimental-utils": "4.23.0",
"@typescript-eslint/scope-manager": "4.23.0",
"@typescript-eslint/experimental-utils": "4.22.0",
"@typescript-eslint/scope-manager": "4.22.0",
"debug": "^4.1.1",
"functional-red-black-tree": "^1.0.1",
"lodash": "^4.17.15",
@ -27315,43 +27386,43 @@
},
"dependencies": {
"@typescript-eslint/experimental-utils": {
"version": "4.23.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.23.0.tgz",
"integrity": "sha512-WAFNiTDnQfrF3Z2fQ05nmCgPsO5o790vOhmWKXbbYQTO9erE1/YsFot5/LnOUizLzU2eeuz6+U/81KV5/hFTGA==",
"version": "4.22.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.22.0.tgz",
"integrity": "sha512-xJXHHl6TuAxB5AWiVrGhvbGL8/hbiCQ8FiWwObO3r0fnvBdrbWEDy1hlvGQOAWc6qsCWuWMKdVWlLAEMpxnddg==",
"dev": true,
"requires": {
"@types/json-schema": "^7.0.3",
"@typescript-eslint/scope-manager": "4.23.0",
"@typescript-eslint/types": "4.23.0",
"@typescript-eslint/typescript-estree": "4.23.0",
"@typescript-eslint/scope-manager": "4.22.0",
"@typescript-eslint/types": "4.22.0",
"@typescript-eslint/typescript-estree": "4.22.0",
"eslint-scope": "^5.0.0",
"eslint-utils": "^2.0.0"
}
},
"@typescript-eslint/scope-manager": {
"version": "4.23.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.23.0.tgz",
"integrity": "sha512-ZZ21PCFxPhI3n0wuqEJK9omkw51wi2bmeKJvlRZPH5YFkcawKOuRMQMnI8mH6Vo0/DoHSeZJnHiIx84LmVQY+w==",
"version": "4.22.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.22.0.tgz",
"integrity": "sha512-OcCO7LTdk6ukawUM40wo61WdeoA7NM/zaoq1/2cs13M7GyiF+T4rxuA4xM+6LeHWjWbss7hkGXjFDRcKD4O04Q==",
"dev": true,
"requires": {
"@typescript-eslint/types": "4.23.0",
"@typescript-eslint/visitor-keys": "4.23.0"
"@typescript-eslint/types": "4.22.0",
"@typescript-eslint/visitor-keys": "4.22.0"
}
},
"@typescript-eslint/types": {
"version": "4.23.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.23.0.tgz",
"integrity": "sha512-oqkNWyG2SLS7uTWLZf6Sr7Dm02gA5yxiz1RP87tvsmDsguVATdpVguHr4HoGOcFOpCvx9vtCSCyQUGfzq28YCw==",
"version": "4.22.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.22.0.tgz",
"integrity": "sha512-sW/BiXmmyMqDPO2kpOhSy2Py5w6KvRRsKZnV0c4+0nr4GIcedJwXAq+RHNK4lLVEZAJYFltnnk1tJSlbeS9lYA==",
"dev": true
},
"@typescript-eslint/typescript-estree": {
"version": "4.23.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.23.0.tgz",
"integrity": "sha512-5Sty6zPEVZF5fbvrZczfmLCOcby3sfrSPu30qKoY1U3mca5/jvU5cwsPb/CO6Q3ByRjixTMIVsDkqwIxCf/dMw==",
"version": "4.22.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.22.0.tgz",
"integrity": "sha512-TkIFeu5JEeSs5ze/4NID+PIcVjgoU3cUQUIZnH3Sb1cEn1lBo7StSV5bwPuJQuoxKXlzAObjYTilOEKRuhR5yg==",
"dev": true,
"requires": {
"@typescript-eslint/types": "4.23.0",
"@typescript-eslint/visitor-keys": "4.23.0",
"@typescript-eslint/types": "4.22.0",
"@typescript-eslint/visitor-keys": "4.22.0",
"debug": "^4.1.1",
"globby": "^11.0.1",
"is-glob": "^4.0.1",
@ -27360,12 +27431,12 @@
}
},
"@typescript-eslint/visitor-keys": {
"version": "4.23.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.23.0.tgz",
"integrity": "sha512-5PNe5cmX9pSifit0H+nPoQBXdbNzi5tOEec+3riK+ku4e3er37pKxMKDH5Ct5Y4fhWxcD4spnlYjxi9vXbSpwg==",
"version": "4.22.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.22.0.tgz",
"integrity": "sha512-nnMu4F+s4o0sll6cBSsTeVsT4cwxB7zECK3dFxzEjPBii9xLpq4yqqsy/FU5zMfan6G60DKZSCXAa3sHJZrcYw==",
"dev": true,
"requires": {
"@typescript-eslint/types": "4.23.0",
"@typescript-eslint/types": "4.22.0",
"eslint-visitor-keys": "^2.0.0"
}
},
@ -27410,41 +27481,41 @@
}
},
"@typescript-eslint/parser": {
"version": "4.23.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.23.0.tgz",
"integrity": "sha512-wsvjksHBMOqySy/Pi2Q6UuIuHYbgAMwLczRl4YanEPKW5KVxI9ZzDYh3B5DtcZPQTGRWFJrfcbJ6L01Leybwug==",
"version": "4.22.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.22.1.tgz",
"integrity": "sha512-l+sUJFInWhuMxA6rtirzjooh8cM/AATAe3amvIkqKFeMzkn85V+eLzb1RyuXkHak4dLfYzOmF6DXPyflJvjQnw==",
"dev": true,
"requires": {
"@typescript-eslint/scope-manager": "4.23.0",
"@typescript-eslint/types": "4.23.0",
"@typescript-eslint/typescript-estree": "4.23.0",
"@typescript-eslint/scope-manager": "4.22.1",
"@typescript-eslint/types": "4.22.1",
"@typescript-eslint/typescript-estree": "4.22.1",
"debug": "^4.1.1"
},
"dependencies": {
"@typescript-eslint/scope-manager": {
"version": "4.23.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.23.0.tgz",
"integrity": "sha512-ZZ21PCFxPhI3n0wuqEJK9omkw51wi2bmeKJvlRZPH5YFkcawKOuRMQMnI8mH6Vo0/DoHSeZJnHiIx84LmVQY+w==",
"version": "4.22.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.22.1.tgz",
"integrity": "sha512-d5bAiPBiessSmNi8Amq/RuLslvcumxLmyhf1/Xa9IuaoFJ0YtshlJKxhlbY7l2JdEk3wS0EnmnfeJWSvADOe0g==",
"dev": true,
"requires": {
"@typescript-eslint/types": "4.23.0",
"@typescript-eslint/visitor-keys": "4.23.0"
"@typescript-eslint/types": "4.22.1",
"@typescript-eslint/visitor-keys": "4.22.1"
}
},
"@typescript-eslint/types": {
"version": "4.23.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.23.0.tgz",
"integrity": "sha512-oqkNWyG2SLS7uTWLZf6Sr7Dm02gA5yxiz1RP87tvsmDsguVATdpVguHr4HoGOcFOpCvx9vtCSCyQUGfzq28YCw==",
"version": "4.22.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.22.1.tgz",
"integrity": "sha512-2HTkbkdAeI3OOcWbqA8hWf/7z9c6gkmnWNGz0dKSLYLWywUlkOAQ2XcjhlKLj5xBFDf8FgAOF5aQbnLRvgNbCw==",
"dev": true
},
"@typescript-eslint/typescript-estree": {
"version": "4.23.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.23.0.tgz",
"integrity": "sha512-5Sty6zPEVZF5fbvrZczfmLCOcby3sfrSPu30qKoY1U3mca5/jvU5cwsPb/CO6Q3ByRjixTMIVsDkqwIxCf/dMw==",
"version": "4.22.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.22.1.tgz",
"integrity": "sha512-p3We0pAPacT+onSGM+sPR+M9CblVqdA9F1JEdIqRVlxK5Qth4ochXQgIyb9daBomyQKAXbygxp1aXQRV0GC79A==",
"dev": true,
"requires": {
"@typescript-eslint/types": "4.23.0",
"@typescript-eslint/visitor-keys": "4.23.0",
"@typescript-eslint/types": "4.22.1",
"@typescript-eslint/visitor-keys": "4.22.1",
"debug": "^4.1.1",
"globby": "^11.0.1",
"is-glob": "^4.0.1",
@ -27453,12 +27524,12 @@
}
},
"@typescript-eslint/visitor-keys": {
"version": "4.23.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.23.0.tgz",
"integrity": "sha512-5PNe5cmX9pSifit0H+nPoQBXdbNzi5tOEec+3riK+ku4e3er37pKxMKDH5Ct5Y4fhWxcD4spnlYjxi9vXbSpwg==",
"version": "4.22.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.22.1.tgz",
"integrity": "sha512-WPkOrIRm+WCLZxXQHCi+WG8T2MMTUFR70rWjdWYddLT7cEfb2P4a3O/J2U1FBVsSFTocXLCoXWY6MZGejeStvQ==",
"dev": true,
"requires": {
"@typescript-eslint/types": "4.23.0",
"@typescript-eslint/types": "4.22.1",
"eslint-visitor-keys": "^2.0.0"
}
},
@ -36556,6 +36627,11 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"lodash-es": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="
},
"lodash._reinterpolate": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz",
@ -37319,6 +37395,11 @@
"integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==",
"dev": true
},
"nanoclone": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/nanoclone/-/nanoclone-0.2.1.tgz",
"integrity": "sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA=="
},
"nanoid": {
"version": "3.1.22",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.22.tgz",
@ -39611,6 +39692,11 @@
"react-is": "^16.8.1"
}
},
"property-expr": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.4.tgz",
"integrity": "sha512-sFPkHQjVKheDNnPvotjQmm3KD3uk1fWKUN7CrpdbwmUx3CrG3QiM8QpTSimvig5vTXmTvjz7+TDvXOI9+4rkcg=="
},
"proxy-addr": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz",
@ -43026,6 +43112,11 @@
"integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==",
"dev": true
},
"toposort": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz",
"integrity": "sha1-riF2gXXRVZ1IvvNUILL0li8JwzA="
},
"tough-cookie": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz",
@ -45119,6 +45210,20 @@
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
"dev": true
},
"yup": {
"version": "0.32.9",
"resolved": "https://registry.npmjs.org/yup/-/yup-0.32.9.tgz",
"integrity": "sha512-Ci1qN+i2H0XpY7syDQ0k5zKQ/DoxO0LzPg8PAR/X4Mpj6DqaeCoIYEEjDJwhArh3Fa7GWbQQVDZKeXYlSH4JMg==",
"requires": {
"@babel/runtime": "^7.10.5",
"@types/lodash": "^4.14.165",
"lodash": "^4.17.20",
"lodash-es": "^4.17.15",
"nanoclone": "^0.2.1",
"property-expr": "^2.0.4",
"toposort": "^2.0.2"
}
}
}
}

View file

@ -5,6 +5,7 @@
"dependencies": {
"@fortawesome/fontawesome-free": "^5.15.3",
"@hookform/error-message": "0.0.5",
"@hookform/resolvers": "^1.3.7",
"@rooks/use-outside-click-ref": "^4.10.1",
"ace-builds": "^1.4.12",
"bulma": "^0.9.2",
@ -30,7 +31,8 @@
"reselect": "^4.0.0",
"typesafe-actions": "^5.1.0",
"use-debounce": "^6.0.1",
"uuid": "^8.3.1"
"uuid": "^8.3.1",
"yup": "^0.32.9"
},
"lint-staged": {
"*.{js,ts,jsx,tsx}": [
@ -81,6 +83,7 @@
"@types/react-dom": "^17.0.2",
"@types/react-redux": "^7.1.11",
"@types/react-router-dom": "^5.1.6",
"@types/react-test-renderer": "^17.0.1",
"@types/redux-mock-store": "^1.0.2",
"@types/uuid": "^8.3.0",
"@typescript-eslint/eslint-plugin": "^4.20.0",
@ -106,6 +109,7 @@
"node-sass": "^5.0.0",
"prettier": "^2.2.1",
"react-scripts": "4.0.3",
"react-test-renderer": "^17.0.2",
"redux-mock-store": "^1.5.4",
"ts-jest": "^26.5.4",
"ts-node": "^9.1.1",

View file

@ -9,6 +9,7 @@ import {
import {
clusterBrokersPath,
clusterConnectorsPath,
clusterConnectsPath,
clusterConsumerGroupsPath,
clusterSchemasPath,
clusterTopicsPath,
@ -59,6 +60,12 @@ const Cluster: React.FC = () => {
component={Schemas}
/>
)}
{hasKafkaConnectConfigured && (
<Route
path={clusterConnectsPath(':clusterName')}
component={Connect}
/>
)}
{hasKafkaConnectConfigured && (
<Route
path={clusterConnectorsPath(':clusterName')}

View file

@ -0,0 +1,71 @@
import React from 'react';
import { Route, Switch, useParams } from 'react-router-dom';
import { ClusterName, ConnectName, ConnectorName } from 'redux/interfaces';
import {
clusterConnectorsPath,
clusterConnectorNewPath,
clusterConnectConnectorPath,
clusterConnectConnectorEditPath,
} from 'lib/paths';
import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb';
interface RouterParams {
clusterName: ClusterName;
connectName: ConnectName;
connectorName: ConnectorName;
}
const Breadcrumbs: React.FC = () => {
const { clusterName, connectName, connectorName } = useParams<RouterParams>();
const rootLinks = [
{
href: clusterConnectorsPath(clusterName),
label: 'All Connectors',
},
];
const connectorLinks = [
...rootLinks,
{
href: clusterConnectConnectorPath(
clusterName,
connectName,
connectorName
),
label: connectorName,
},
];
return (
<Switch>
<Route exact path={clusterConnectorsPath(':clusterName')}>
<Breadcrumb>All Connectors</Breadcrumb>
</Route>
<Route exact path={clusterConnectorNewPath(':clusterName')}>
<Breadcrumb links={rootLinks}>New Connector</Breadcrumb>
</Route>
<Route
exact
path={clusterConnectConnectorEditPath(
':clusterName',
':connectName',
':connectorName'
)}
>
<Breadcrumb links={connectorLinks}>Edit</Breadcrumb>
</Route>
<Route
path={clusterConnectConnectorPath(
':clusterName',
':connectName',
':connectorName'
)}
>
<Breadcrumb links={rootLinks}>{connectorName}</Breadcrumb>
</Route>
</Switch>
);
};
export default Breadcrumbs;

View file

@ -0,0 +1,65 @@
import React from 'react';
import { create } from 'react-test-renderer';
import Breadcrumbs from 'components/Connect/Breadcrumbs/Breadcrumbs';
import {
clusterConnectConnectorEditPath,
clusterConnectConnectorPath,
clusterConnectorNewPath,
clusterConnectorsPath,
} from 'lib/paths';
import { TestRouterWrapper } from 'lib/testHelpers';
describe('Breadcrumbs', () => {
const setupWrapper = (pathname: string) => (
<TestRouterWrapper
pathname={pathname}
urlParams={{
clusterName: 'my-cluster',
connectName: 'my-connect',
connectorName: 'my-connector',
}}
>
<Breadcrumbs />
</TestRouterWrapper>
);
it('matches snapshot for root path', () => {
expect(
create(setupWrapper(clusterConnectorsPath(':clusterName'))).toJSON()
).toMatchSnapshot();
});
it('matches snapshot for new connector path', () => {
expect(
create(setupWrapper(clusterConnectorNewPath(':clusterName'))).toJSON()
).toMatchSnapshot();
});
it('matches snapshot for connector edit path', () => {
expect(
create(
setupWrapper(
clusterConnectConnectorEditPath(
':clusterName',
':connectName',
':connectorName'
)
)
).toJSON()
).toMatchSnapshot();
});
it('matches snapshot for connector path', () => {
expect(
create(
setupWrapper(
clusterConnectConnectorPath(
':clusterName',
':connectName',
':connectorName'
)
)
).toJSON()
).toMatchSnapshot();
});
});

View file

@ -0,0 +1,109 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Breadcrumbs matches snapshot for connector edit path 1`] = `
<nav
aria-label="breadcrumbs"
className="breadcrumb"
>
<ul>
<li>
<a
href="/ui/clusters/my-cluster/connectors"
onClick={[Function]}
>
All Connectors
</a>
</li>
<li>
<a
href="/ui/clusters/my-cluster/connects/my-connect/connectors/my-connector"
onClick={[Function]}
>
my-connector
</a>
</li>
<li
className="is-active"
>
<span
className=""
>
Edit
</span>
</li>
</ul>
</nav>
`;
exports[`Breadcrumbs matches snapshot for connector path 1`] = `
<nav
aria-label="breadcrumbs"
className="breadcrumb"
>
<ul>
<li>
<a
href="/ui/clusters/my-cluster/connectors"
onClick={[Function]}
>
All Connectors
</a>
</li>
<li
className="is-active"
>
<span
className=""
>
my-connector
</span>
</li>
</ul>
</nav>
`;
exports[`Breadcrumbs matches snapshot for new connector path 1`] = `
<nav
aria-label="breadcrumbs"
className="breadcrumb"
>
<ul>
<li>
<a
href="/ui/clusters/my-cluster/connectors"
onClick={[Function]}
>
All Connectors
</a>
</li>
<li
className="is-active"
>
<span
className=""
>
New Connector
</span>
</li>
</ul>
</nav>
`;
exports[`Breadcrumbs matches snapshot for root path 1`] = `
<nav
aria-label="breadcrumbs"
className="breadcrumb"
>
<ul>
<li
className="is-active"
>
<span
className=""
>
All Connectors
</span>
</li>
</ul>
</nav>
`;

View file

@ -1,16 +1,64 @@
import React from 'react';
import { Switch, Route } from 'react-router-dom';
import { clusterConnectorsPath } from 'lib/paths';
import ListContainer from 'components/Connect/List/ListContainer';
import {
clusterConnectorsPath,
clusterConnectorNewPath,
clusterConnectConnectorPath,
clusterConnectConnectorEditPath,
} from 'lib/paths';
import Breadcrumbs from './Breadcrumbs/Breadcrumbs';
import ListContainer from './List/ListContainer';
import NewContainer from './New/NewContainer';
import DetailsContainer from './Details/DetailsContainer';
import EditContainer from './Edit/EditContainer';
const Connect: React.FC = () => (
<Switch>
<Route
exact
path={clusterConnectorsPath(':clusterName')}
component={ListContainer}
/>
</Switch>
<div className="section">
<Switch>
<Route
path={clusterConnectConnectorPath(
':clusterName',
':connectName',
':connectorName'
)}
component={Breadcrumbs}
/>
<Route
path={clusterConnectorsPath(':clusterName')}
component={Breadcrumbs}
/>
</Switch>
<Switch>
<Route
exact
path={clusterConnectorsPath(':clusterName')}
component={ListContainer}
/>
<Route
exact
path={clusterConnectorNewPath(':clusterName')}
component={NewContainer}
/>
<Route
exact
path={clusterConnectConnectorEditPath(
':clusterName',
':connectName',
':connectorName'
)}
component={EditContainer}
/>
<Route
path={clusterConnectConnectorPath(
':clusterName',
':connectName',
':connectorName'
)}
component={DetailsContainer}
/>
</Switch>
</div>
);
export default Connect;

View file

@ -0,0 +1,168 @@
import React from 'react';
import { Link, useHistory, useParams } from 'react-router-dom';
import { ConnectorTaskStatus } from 'generated-sources';
import { ClusterName, ConnectName, ConnectorName } from 'redux/interfaces';
import {
clusterConnectConnectorEditPath,
clusterConnectorsPath,
} from 'lib/paths';
import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
interface RouterParams {
clusterName: ClusterName;
connectName: ConnectName;
connectorName: ConnectorName;
}
export interface ActionsProps {
deleteConnector(
clusterName: ClusterName,
connectName: ConnectName,
connectorName: ConnectorName
): Promise<void>;
isConnectorDeleting: boolean;
connectorStatus?: ConnectorTaskStatus;
restartConnector(
clusterName: ClusterName,
connectName: ConnectName,
connectorName: ConnectorName
): void;
pauseConnector(
clusterName: ClusterName,
connectName: ConnectName,
connectorName: ConnectorName
): void;
resumeConnector(
clusterName: ClusterName,
connectName: ConnectName,
connectorName: ConnectorName
): void;
isConnectorActionRunning: boolean;
}
const Actions: React.FC<ActionsProps> = ({
deleteConnector,
isConnectorDeleting,
connectorStatus,
restartConnector,
pauseConnector,
resumeConnector,
isConnectorActionRunning,
}) => {
const { clusterName, connectName, connectorName } = useParams<RouterParams>();
const history = useHistory();
const [
isDeleteConnectorConfirmationVisible,
setIsDeleteConnectorConfirmationVisible,
] = React.useState(false);
const deleteConnectorHandler = React.useCallback(async () => {
try {
await deleteConnector(clusterName, connectName, connectorName);
history.push(clusterConnectorsPath(clusterName));
} catch {
// do not redirect
}
}, [deleteConnector, clusterName, connectName, connectorName]);
const restartConnectorHandler = React.useCallback(() => {
restartConnector(clusterName, connectName, connectorName);
}, [restartConnector, clusterName, connectName, connectorName]);
const pauseConnectorHandler = React.useCallback(() => {
pauseConnector(clusterName, connectName, connectorName);
}, [pauseConnector, clusterName, connectName, connectorName]);
const resumeConnectorHandler = React.useCallback(() => {
resumeConnector(clusterName, connectName, connectorName);
}, [resumeConnector, clusterName, connectName, connectorName]);
return (
<div className="buttons">
{connectorStatus === ConnectorTaskStatus.RUNNING && (
<button
type="button"
className="button"
onClick={pauseConnectorHandler}
disabled={isConnectorActionRunning}
>
<span className="icon">
<i className="fas fa-pause" />
</span>
<span>Pause</span>
</button>
)}
{connectorStatus === ConnectorTaskStatus.PAUSED && (
<button
type="button"
className="button"
onClick={resumeConnectorHandler}
disabled={isConnectorActionRunning}
>
<span className="icon">
<i className="fas fa-play" />
</span>
<span>Resume</span>
</button>
)}
<button
type="button"
className="button"
onClick={restartConnectorHandler}
disabled={isConnectorActionRunning}
>
<span className="icon">
<i className="fas fa-sync-alt" />
</span>
<span>Restart all tasks</span>
</button>
{isConnectorActionRunning ? (
<button type="button" className="button" disabled>
<span className="icon">
<i className="fas fa-edit" />
</span>
<span>Edit config</span>
</button>
) : (
<Link
to={clusterConnectConnectorEditPath(
clusterName,
connectName,
connectorName
)}
className="button"
>
<span className="icon">
<i className="fas fa-pencil-alt" />
</span>
<span>Edit config</span>
</Link>
)}
<button
className="button is-danger"
type="button"
onClick={() => setIsDeleteConnectorConfirmationVisible(true)}
disabled={isConnectorActionRunning}
>
<span className="icon">
<i className="far fa-trash-alt" />
</span>
<span>Delete</span>
</button>
<ConfirmationModal
isOpen={isDeleteConnectorConfirmationVisible}
onCancel={() => setIsDeleteConnectorConfirmationVisible(false)}
onConfirm={deleteConnectorHandler}
isConfirming={isConnectorDeleting}
>
Are you sure you want to remove <b>{connectorName}</b> connector?
</ConfirmationModal>
</div>
);
};
export default Actions;

View file

@ -0,0 +1,33 @@
import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
import { RootState } from 'redux/interfaces';
import {
deleteConnector,
restartConnector,
pauseConnector,
resumeConnector,
} from 'redux/actions';
import {
getIsConnectorDeleting,
getConnectorStatus,
getIsConnectorActionRunning,
} from 'redux/reducers/connect/selectors';
import Actions from './Actions';
const mapStateToProps = (state: RootState) => ({
isConnectorDeleting: getIsConnectorDeleting(state),
connectorStatus: getConnectorStatus(state),
isConnectorActionRunning: getIsConnectorActionRunning(state),
});
const mapDispatchToProps = {
deleteConnector,
restartConnector,
pauseConnector,
resumeConnector,
};
export default withRouter(
connect(mapStateToProps, mapDispatchToProps)(Actions)
);

View file

@ -0,0 +1,188 @@
import React from 'react';
import { create } from 'react-test-renderer';
import { mount } from 'enzyme';
import { act } from 'react-dom/test-utils';
import { containerRendersView, TestRouterWrapper } from 'lib/testHelpers';
import { clusterConnectConnectorPath, clusterConnectorsPath } from 'lib/paths';
import ActionsContainer from 'components/Connect/Details/Actions/ActionsContainer';
import Actions, {
ActionsProps,
} from 'components/Connect/Details/Actions/Actions';
import { ConnectorTaskStatus } from 'generated-sources';
import { ConfirmationModalProps } from 'components/common/ConfirmationModal/ConfirmationModal';
const mockHistoryPush = jest.fn();
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useHistory: () => ({
push: mockHistoryPush,
}),
}));
jest.mock(
'components/common/ConfirmationModal/ConfirmationModal',
() => 'mock-ConfirmationModal'
);
describe('Actions', () => {
containerRendersView(<ActionsContainer />, Actions);
describe('view', () => {
const pathname = clusterConnectConnectorPath(
':clusterName',
':connectName',
':connectorName'
);
const clusterName = 'my-cluster';
const connectName = 'my-connect';
const connectorName = 'my-connector';
const setupWrapper = (props: Partial<ActionsProps> = {}) => (
<TestRouterWrapper
pathname={pathname}
urlParams={{ clusterName, connectName, connectorName }}
>
<Actions
deleteConnector={jest.fn()}
isConnectorDeleting={false}
connectorStatus={ConnectorTaskStatus.RUNNING}
restartConnector={jest.fn()}
pauseConnector={jest.fn()}
resumeConnector={jest.fn()}
isConnectorActionRunning={false}
{...props}
/>
</TestRouterWrapper>
);
it('matches snapshot', () => {
const wrapper = create(setupWrapper());
expect(wrapper.toJSON()).toMatchSnapshot();
});
it('matches snapshot when paused', () => {
const wrapper = create(
setupWrapper({ connectorStatus: ConnectorTaskStatus.PAUSED })
);
expect(wrapper.toJSON()).toMatchSnapshot();
});
it('matches snapshot when failed', () => {
const wrapper = create(
setupWrapper({ connectorStatus: ConnectorTaskStatus.FAILED })
);
expect(wrapper.toJSON()).toMatchSnapshot();
});
it('matches snapshot when unassigned', () => {
const wrapper = create(
setupWrapper({ connectorStatus: ConnectorTaskStatus.UNASSIGNED })
);
expect(wrapper.toJSON()).toMatchSnapshot();
});
it('matches snapshot when deleting connector', () => {
const wrapper = create(setupWrapper({ isConnectorDeleting: true }));
expect(wrapper.toJSON()).toMatchSnapshot();
});
it('matches snapshot when running connector action', () => {
const wrapper = create(setupWrapper({ isConnectorActionRunning: true }));
expect(wrapper.toJSON()).toMatchSnapshot();
});
it('opens confirmation modal when delete button clicked and closes when cancel button clicked', () => {
const deleteConnector = jest.fn();
const wrapper = mount(setupWrapper({ deleteConnector }));
wrapper.find({ children: 'Delete' }).simulate('click');
let confirmationModalProps = wrapper
.find('mock-ConfirmationModal')
.props() as ConfirmationModalProps;
expect(confirmationModalProps.isOpen).toBeTruthy();
act(() => {
confirmationModalProps.onCancel();
});
wrapper.update();
confirmationModalProps = wrapper
.find('mock-ConfirmationModal')
.props() as ConfirmationModalProps;
expect(confirmationModalProps.isOpen).toBeFalsy();
});
it('calls deleteConnector when confirm button clicked', () => {
const deleteConnector = jest.fn();
const wrapper = mount(setupWrapper({ deleteConnector }));
(wrapper
.find('mock-ConfirmationModal')
.props() as ConfirmationModalProps).onConfirm();
expect(deleteConnector).toHaveBeenCalledTimes(1);
expect(deleteConnector).toHaveBeenCalledWith(
clusterName,
connectName,
connectorName
);
});
it('redirects after delete', async () => {
const deleteConnector = jest
.fn()
.mockResolvedValueOnce({ message: 'success' });
const wrapper = mount(setupWrapper({ deleteConnector }));
await act(async () => {
(wrapper
.find('mock-ConfirmationModal')
.props() as ConfirmationModalProps).onConfirm();
});
expect(mockHistoryPush).toHaveBeenCalledTimes(1);
expect(mockHistoryPush).toHaveBeenCalledWith(
clusterConnectorsPath(clusterName)
);
});
it('calls restartConnector when restart button clicked', () => {
const restartConnector = jest.fn();
const wrapper = mount(setupWrapper({ restartConnector }));
wrapper.find({ children: 'Restart all tasks' }).simulate('click');
expect(restartConnector).toHaveBeenCalledTimes(1);
expect(restartConnector).toHaveBeenCalledWith(
clusterName,
connectName,
connectorName
);
});
it('calls pauseConnector when pause button clicked', () => {
const pauseConnector = jest.fn();
const wrapper = mount(
setupWrapper({
connectorStatus: ConnectorTaskStatus.RUNNING,
pauseConnector,
})
);
wrapper.find({ children: 'Pause' }).simulate('click');
expect(pauseConnector).toHaveBeenCalledTimes(1);
expect(pauseConnector).toHaveBeenCalledWith(
clusterName,
connectName,
connectorName
);
});
it('calls resumeConnector when resume button clicked', () => {
const resumeConnector = jest.fn();
const wrapper = mount(
setupWrapper({
connectorStatus: ConnectorTaskStatus.PAUSED,
resumeConnector,
})
);
wrapper.find({ children: 'Resume' }).simulate('click');
expect(resumeConnector).toHaveBeenCalledTimes(1);
expect(resumeConnector).toHaveBeenCalledWith(
clusterName,
connectName,
connectorName
);
});
});
});

View file

@ -0,0 +1,483 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Actions view matches snapshot 1`] = `
<div
className="buttons"
>
<button
className="button"
disabled={false}
onClick={[Function]}
type="button"
>
<span
className="icon"
>
<i
className="fas fa-pause"
/>
</span>
<span>
Pause
</span>
</button>
<button
className="button"
disabled={false}
onClick={[Function]}
type="button"
>
<span
className="icon"
>
<i
className="fas fa-sync-alt"
/>
</span>
<span>
Restart all tasks
</span>
</button>
<a
className="button"
href="/ui/clusters/my-cluster/connects/my-connect/connectors/my-connector/edit"
onClick={[Function]}
>
<span
className="icon"
>
<i
className="fas fa-pencil-alt"
/>
</span>
<span>
Edit config
</span>
</a>
<button
className="button is-danger"
disabled={false}
onClick={[Function]}
type="button"
>
<span
className="icon"
>
<i
className="far fa-trash-alt"
/>
</span>
<span>
Delete
</span>
</button>
<mock-ConfirmationModal
isConfirming={false}
isOpen={false}
onCancel={[Function]}
onConfirm={[Function]}
>
Are you sure you want to remove
<b>
my-connector
</b>
connector?
</mock-ConfirmationModal>
</div>
`;
exports[`Actions view matches snapshot when deleting connector 1`] = `
<div
className="buttons"
>
<button
className="button"
disabled={false}
onClick={[Function]}
type="button"
>
<span
className="icon"
>
<i
className="fas fa-pause"
/>
</span>
<span>
Pause
</span>
</button>
<button
className="button"
disabled={false}
onClick={[Function]}
type="button"
>
<span
className="icon"
>
<i
className="fas fa-sync-alt"
/>
</span>
<span>
Restart all tasks
</span>
</button>
<a
className="button"
href="/ui/clusters/my-cluster/connects/my-connect/connectors/my-connector/edit"
onClick={[Function]}
>
<span
className="icon"
>
<i
className="fas fa-pencil-alt"
/>
</span>
<span>
Edit config
</span>
</a>
<button
className="button is-danger"
disabled={false}
onClick={[Function]}
type="button"
>
<span
className="icon"
>
<i
className="far fa-trash-alt"
/>
</span>
<span>
Delete
</span>
</button>
<mock-ConfirmationModal
isConfirming={true}
isOpen={false}
onCancel={[Function]}
onConfirm={[Function]}
>
Are you sure you want to remove
<b>
my-connector
</b>
connector?
</mock-ConfirmationModal>
</div>
`;
exports[`Actions view matches snapshot when failed 1`] = `
<div
className="buttons"
>
<button
className="button"
disabled={false}
onClick={[Function]}
type="button"
>
<span
className="icon"
>
<i
className="fas fa-sync-alt"
/>
</span>
<span>
Restart all tasks
</span>
</button>
<a
className="button"
href="/ui/clusters/my-cluster/connects/my-connect/connectors/my-connector/edit"
onClick={[Function]}
>
<span
className="icon"
>
<i
className="fas fa-pencil-alt"
/>
</span>
<span>
Edit config
</span>
</a>
<button
className="button is-danger"
disabled={false}
onClick={[Function]}
type="button"
>
<span
className="icon"
>
<i
className="far fa-trash-alt"
/>
</span>
<span>
Delete
</span>
</button>
<mock-ConfirmationModal
isConfirming={false}
isOpen={false}
onCancel={[Function]}
onConfirm={[Function]}
>
Are you sure you want to remove
<b>
my-connector
</b>
connector?
</mock-ConfirmationModal>
</div>
`;
exports[`Actions view matches snapshot when paused 1`] = `
<div
className="buttons"
>
<button
className="button"
disabled={false}
onClick={[Function]}
type="button"
>
<span
className="icon"
>
<i
className="fas fa-play"
/>
</span>
<span>
Resume
</span>
</button>
<button
className="button"
disabled={false}
onClick={[Function]}
type="button"
>
<span
className="icon"
>
<i
className="fas fa-sync-alt"
/>
</span>
<span>
Restart all tasks
</span>
</button>
<a
className="button"
href="/ui/clusters/my-cluster/connects/my-connect/connectors/my-connector/edit"
onClick={[Function]}
>
<span
className="icon"
>
<i
className="fas fa-pencil-alt"
/>
</span>
<span>
Edit config
</span>
</a>
<button
className="button is-danger"
disabled={false}
onClick={[Function]}
type="button"
>
<span
className="icon"
>
<i
className="far fa-trash-alt"
/>
</span>
<span>
Delete
</span>
</button>
<mock-ConfirmationModal
isConfirming={false}
isOpen={false}
onCancel={[Function]}
onConfirm={[Function]}
>
Are you sure you want to remove
<b>
my-connector
</b>
connector?
</mock-ConfirmationModal>
</div>
`;
exports[`Actions view matches snapshot when running connector action 1`] = `
<div
className="buttons"
>
<button
className="button"
disabled={true}
onClick={[Function]}
type="button"
>
<span
className="icon"
>
<i
className="fas fa-pause"
/>
</span>
<span>
Pause
</span>
</button>
<button
className="button"
disabled={true}
onClick={[Function]}
type="button"
>
<span
className="icon"
>
<i
className="fas fa-sync-alt"
/>
</span>
<span>
Restart all tasks
</span>
</button>
<button
className="button"
disabled={true}
type="button"
>
<span
className="icon"
>
<i
className="fas fa-edit"
/>
</span>
<span>
Edit config
</span>
</button>
<button
className="button is-danger"
disabled={true}
onClick={[Function]}
type="button"
>
<span
className="icon"
>
<i
className="far fa-trash-alt"
/>
</span>
<span>
Delete
</span>
</button>
<mock-ConfirmationModal
isConfirming={false}
isOpen={false}
onCancel={[Function]}
onConfirm={[Function]}
>
Are you sure you want to remove
<b>
my-connector
</b>
connector?
</mock-ConfirmationModal>
</div>
`;
exports[`Actions view matches snapshot when unassigned 1`] = `
<div
className="buttons"
>
<button
className="button"
disabled={false}
onClick={[Function]}
type="button"
>
<span
className="icon"
>
<i
className="fas fa-sync-alt"
/>
</span>
<span>
Restart all tasks
</span>
</button>
<a
className="button"
href="/ui/clusters/my-cluster/connects/my-connect/connectors/my-connector/edit"
onClick={[Function]}
>
<span
className="icon"
>
<i
className="fas fa-pencil-alt"
/>
</span>
<span>
Edit config
</span>
</a>
<button
className="button is-danger"
disabled={false}
onClick={[Function]}
type="button"
>
<span
className="icon"
>
<i
className="far fa-trash-alt"
/>
</span>
<span>
Delete
</span>
</button>
<mock-ConfirmationModal
isConfirming={false}
isOpen={false}
onCancel={[Function]}
onConfirm={[Function]}
>
Are you sure you want to remove
<b>
my-connector
</b>
connector?
</mock-ConfirmationModal>
</div>
`;

View file

@ -0,0 +1,57 @@
import React from 'react';
import { useParams } from 'react-router';
import {
ClusterName,
ConnectName,
ConnectorConfig,
ConnectorName,
} from 'redux/interfaces';
import PageLoader from 'components/common/PageLoader/PageLoader';
import JSONEditor from 'components/common/JSONEditor/JSONEditor';
interface RouterParams {
clusterName: ClusterName;
connectName: ConnectName;
connectorName: ConnectorName;
}
export interface ConfigProps {
fetchConfig(
clusterName: ClusterName,
connectName: ConnectName,
connectorName: ConnectorName,
silent?: boolean
): void;
isConfigFetching: boolean;
config: ConnectorConfig | null;
}
const Config: React.FC<ConfigProps> = ({
fetchConfig,
isConfigFetching,
config,
}) => {
const { clusterName, connectName, connectorName } = useParams<RouterParams>();
React.useEffect(() => {
fetchConfig(clusterName, connectName, connectorName, true);
}, [fetchConfig, clusterName, connectName, connectorName]);
if (isConfigFetching) {
return <PageLoader />;
}
if (!config) return null;
return (
<JSONEditor
readOnly
value={JSON.stringify(config, null, '\t')}
showGutter={false}
highlightActiveLine={false}
isFixedHeight
/>
);
};
export default Config;

View file

@ -0,0 +1,21 @@
import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
import { RootState } from 'redux/interfaces';
import { fetchConnectorConfig } from 'redux/actions';
import {
getIsConnectorConfigFetching,
getConnectorConfig,
} from 'redux/reducers/connect/selectors';
import Config from './Config';
const mapStateToProps = (state: RootState) => ({
isConfigFetching: getIsConnectorConfigFetching(state),
config: getConnectorConfig(state),
});
const mapDispatchToProps = {
fetchConfig: fetchConnectorConfig,
};
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Config));

View file

@ -0,0 +1,68 @@
import React from 'react';
import { create } from 'react-test-renderer';
import { mount } from 'enzyme';
import { containerRendersView, TestRouterWrapper } from 'lib/testHelpers';
import { clusterConnectConnectorConfigPath } from 'lib/paths';
import ConfigContainer from 'components/Connect/Details/Config/ConfigContainer';
import Config, { ConfigProps } from 'components/Connect/Details/Config/Config';
import { connector } from 'redux/reducers/connect/__test__/fixtures';
jest.mock('components/common/PageLoader/PageLoader', () => 'mock-PageLoader');
jest.mock('components/common/JSONEditor/JSONEditor', () => 'mock-JSONEditor');
describe('Config', () => {
containerRendersView(<ConfigContainer />, Config);
describe('view', () => {
const pathname = clusterConnectConnectorConfigPath(
':clusterName',
':connectName',
':connectorName'
);
const clusterName = 'my-cluster';
const connectName = 'my-connect';
const connectorName = 'my-connector';
const setupWrapper = (props: Partial<ConfigProps> = {}) => (
<TestRouterWrapper
pathname={pathname}
urlParams={{ clusterName, connectName, connectorName }}
>
<Config
fetchConfig={jest.fn()}
isConfigFetching={false}
config={connector.config}
{...props}
/>
</TestRouterWrapper>
);
it('matches snapshot', () => {
const wrapper = create(setupWrapper());
expect(wrapper.toJSON()).toMatchSnapshot();
});
it('matches snapshot when fetching config', () => {
const wrapper = create(setupWrapper({ isConfigFetching: true }));
expect(wrapper.toJSON()).toMatchSnapshot();
});
it('is empty when no config', () => {
const wrapper = mount(setupWrapper({ config: null }));
expect(wrapper.html()).toEqual('');
});
it('fetches config on mount', () => {
const fetchConfig = jest.fn();
mount(setupWrapper({ fetchConfig }));
expect(fetchConfig).toHaveBeenCalledTimes(1);
expect(fetchConfig).toHaveBeenCalledWith(
clusterName,
connectName,
connectorName,
true
);
});
});
});

View file

@ -0,0 +1,18 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Config view matches snapshot 1`] = `
<mock-JSONEditor
highlightActiveLine={false}
isFixedHeight={true}
readOnly={true}
showGutter={false}
value="{
\\"connector.class\\": \\"FileStreamSource\\",
\\"tasks.max\\": \\"10\\",
\\"topic\\": \\"test-topic\\",
\\"file\\": \\"/some/file\\"
}"
/>
`;
exports[`Config view matches snapshot when fetching config 1`] = `<mock-PageLoader />`;

View file

@ -0,0 +1,141 @@
import React from 'react';
import { NavLink, Route, Switch, useParams } from 'react-router-dom';
import { Connector, Task } from 'generated-sources';
import { ClusterName, ConnectName, ConnectorName } from 'redux/interfaces';
import {
clusterConnectConnectorConfigPath,
clusterConnectConnectorPath,
clusterConnectConnectorTasksPath,
} from 'lib/paths';
import PageLoader from 'components/common/PageLoader/PageLoader';
import OverviewContainer from './Overview/OverviewContainer';
import TasksContainer from './Tasks/TasksContainer';
import ConfigContainer from './Config/ConfigContainer';
import ActionsContainer from './Actions/ActionsContainer';
interface RouterParams {
clusterName: ClusterName;
connectName: ConnectName;
connectorName: ConnectorName;
}
export interface DetailsProps {
fetchConnector(
clusterName: ClusterName,
connectName: ConnectName,
connectorName: ConnectorName
): void;
fetchTasks(
clusterName: ClusterName,
connectName: ConnectName,
connectorName: ConnectorName
): void;
isConnectorFetching: boolean;
areTasksFetching: boolean;
connector: Connector | null;
tasks: Task[];
}
const Details: React.FC<DetailsProps> = ({
fetchConnector,
fetchTasks,
isConnectorFetching,
areTasksFetching,
connector,
}) => {
const { clusterName, connectName, connectorName } = useParams<RouterParams>();
React.useEffect(() => {
fetchConnector(clusterName, connectName, connectorName);
}, [fetchConnector, clusterName, connectName, connectorName]);
React.useEffect(() => {
fetchTasks(clusterName, connectName, connectorName);
}, [fetchTasks, clusterName, connectName, connectorName]);
if (isConnectorFetching || areTasksFetching) {
return <PageLoader />;
}
if (!connector) return null;
return (
<div className="box">
<nav className="navbar mb-4" role="navigation">
<div className="navbar-start tabs mb-0">
<NavLink
exact
to={clusterConnectConnectorPath(
clusterName,
connectName,
connectorName
)}
className="navbar-item is-tab"
activeClassName="is-active"
>
Overview
</NavLink>
<NavLink
exact
to={clusterConnectConnectorTasksPath(
clusterName,
connectName,
connectorName
)}
className="navbar-item is-tab"
activeClassName="is-active"
>
Tasks
</NavLink>
<NavLink
exact
to={clusterConnectConnectorConfigPath(
clusterName,
connectName,
connectorName
)}
className="navbar-item is-tab"
activeClassName="is-active"
>
Config
</NavLink>
</div>
<div className="navbar-end">
<ActionsContainer />
</div>
</nav>
<Switch>
<Route
exact
path={clusterConnectConnectorTasksPath(
':clusterName',
':connectName',
':connectorName'
)}
component={TasksContainer}
/>
<Route
exact
path={clusterConnectConnectorConfigPath(
':clusterName',
':connectName',
':connectorName'
)}
component={ConfigContainer}
/>
<Route
exact
path={clusterConnectConnectorPath(
':clusterName',
':connectName',
':connectorName'
)}
component={OverviewContainer}
/>
</Switch>
</div>
);
};
export default Details;

View file

@ -0,0 +1,28 @@
import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
import { RootState } from 'redux/interfaces';
import { fetchConnector, fetchConnectorTasks } from 'redux/actions';
import {
getIsConnectorFetching,
getAreConnectorTasksFetching,
getConnector,
getConnectorTasks,
} from 'redux/reducers/connect/selectors';
import Details from './Details';
const mapStateToProps = (state: RootState) => ({
isConnectorFetching: getIsConnectorFetching(state),
connector: getConnector(state),
areTasksFetching: getAreConnectorTasksFetching(state),
tasks: getConnectorTasks(state),
});
const mapDispatchToProps = {
fetchConnector,
fetchTasks: fetchConnectorTasks,
};
export default withRouter(
connect(mapStateToProps, mapDispatchToProps)(Details)
);

View file

@ -0,0 +1,58 @@
import React from 'react';
import { Connector } from 'generated-sources';
import StatusTag from 'components/Connect/StatusTag';
export interface OverviewProps {
connector: Connector | null;
runningTasksCount: number;
failedTasksCount: number;
}
const Overview: React.FC<OverviewProps> = ({
connector,
runningTasksCount,
failedTasksCount,
}) => {
if (!connector) return null;
return (
<div className="tile is-6">
<table className="table is-fullwidth">
<tbody>
{connector.status?.workerId && (
<tr>
<th>Worker</th>
<td>{connector.status.workerId}</td>
</tr>
)}
<tr>
<th>Type</th>
<td>{connector.type}</td>
</tr>
{connector.config['connector.class'] && (
<tr>
<th>Class</th>
<td>{connector.config['connector.class']}</td>
</tr>
)}
<tr>
<th>State</th>
<td>
<StatusTag status={connector.status.state} />
</td>
</tr>
<tr>
<th>Tasks Running</th>
<td>{runningTasksCount}</td>
</tr>
<tr>
<th>Tasks Failed</th>
<td>{failedTasksCount}</td>
</tr>
</tbody>
</table>
</div>
);
};
export default Overview;

View file

@ -0,0 +1,18 @@
import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
import { RootState } from 'redux/interfaces';
import {
getConnector,
getConnectorRunningTasksCount,
getConnectorFailedTasksCount,
} from 'redux/reducers/connect/selectors';
import Overview from './Overview';
const mapStateToProps = (state: RootState) => ({
connector: getConnector(state),
runningTasksCount: getConnectorRunningTasksCount(state),
failedTasksCount: getConnectorFailedTasksCount(state),
});
export default withRouter(connect(mapStateToProps)(Overview));

View file

@ -0,0 +1,36 @@
import React from 'react';
import { create } from 'react-test-renderer';
import { mount } from 'enzyme';
import { containerRendersView } from 'lib/testHelpers';
import OverviewContainer from 'components/Connect/Details/Overview/OverviewContainer';
import Overview, {
OverviewProps,
} from 'components/Connect/Details/Overview/Overview';
import { connector } from 'redux/reducers/connect/__test__/fixtures';
jest.mock('components/Connect/StatusTag', () => 'mock-StatusTag');
describe('Overview', () => {
containerRendersView(<OverviewContainer />, Overview);
describe('view', () => {
const setupWrapper = (props: Partial<OverviewProps> = {}) => (
<Overview
connector={connector}
runningTasksCount={10}
failedTasksCount={2}
{...props}
/>
);
it('matches snapshot', () => {
const wrapper = create(setupWrapper());
expect(wrapper.toJSON()).toMatchSnapshot();
});
it('is empty when no connector', () => {
const wrapper = mount(setupWrapper({ connector: null }));
expect(wrapper.html()).toBeNull();
});
});
});

View file

@ -0,0 +1,64 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Overview view matches snapshot 1`] = `
<div
className="tile is-6"
>
<table
className="table is-fullwidth"
>
<tbody>
<tr>
<th>
Worker
</th>
<td>
kafka-connect0:8083
</td>
</tr>
<tr>
<th>
Type
</th>
<td>
SOURCE
</td>
</tr>
<tr>
<th>
Class
</th>
<td>
FileStreamSource
</td>
</tr>
<tr>
<th>
State
</th>
<td>
<mock-StatusTag
status="RUNNING"
/>
</td>
</tr>
<tr>
<th>
Tasks Running
</th>
<td>
10
</td>
</tr>
<tr>
<th>
Tasks Failed
</th>
<td>
2
</td>
</tr>
</tbody>
</table>
</div>
`;

View file

@ -0,0 +1,57 @@
import React from 'react';
import { useParams } from 'react-router-dom';
import { Task, TaskId } from 'generated-sources';
import { ClusterName, ConnectName, ConnectorName } from 'redux/interfaces';
import StatusTag from 'components/Connect/StatusTag';
interface RouterParams {
clusterName: ClusterName;
connectName: ConnectName;
connectorName: ConnectorName;
}
export interface ListItemProps {
task: Task;
restartTask(
clusterName: ClusterName,
connectName: ConnectName,
connectorName: ConnectorName,
taskId: TaskId['task']
): Promise<void>;
}
const ListItem: React.FC<ListItemProps> = ({ task, restartTask }) => {
const { clusterName, connectName, connectorName } = useParams<RouterParams>();
const [restarting, setRestarting] = React.useState(false);
const restartTaskHandler = React.useCallback(async () => {
setRestarting(true);
await restartTask(clusterName, connectName, connectorName, task.id?.task);
setRestarting(false);
}, [restartTask, clusterName, connectName, connectorName, task.id?.task]);
return (
<tr>
<td className="has-text-overflow-ellipsis">{task.status?.id}</td>
<td>{task.status?.workerId}</td>
<td>
<StatusTag status={task.status.state} />
</td>
<td>{task.status.trace}</td>
<td>
<button
type="button"
className="button is-small is-pulled-right"
onClick={restartTaskHandler}
disabled={restarting}
>
<span className="icon">
<i className="fas fa-sync-alt" />
</span>
</button>
</td>
</tr>
);
};
export default ListItem;

View file

@ -0,0 +1,23 @@
import { connect } from 'react-redux';
import { RouteComponentProps, withRouter } from 'react-router-dom';
import { Task } from 'generated-sources';
import { RootState } from 'redux/interfaces';
import { restartConnectorTask } from 'redux/actions';
import ListItem from './ListItem';
interface OwnProps extends RouteComponentProps {
task: Task;
}
const mapStateToProps = (state: RootState, { task }: OwnProps) => ({
task,
});
const mapDispatchToProps = {
restartTask: restartConnectorTask,
};
export default withRouter(
connect(mapStateToProps, mapDispatchToProps)(ListItem)
);

View file

@ -0,0 +1,68 @@
import React from 'react';
import { create } from 'react-test-renderer';
import { mount } from 'enzyme';
import { act } from 'react-dom/test-utils';
import { containerRendersView, TestRouterWrapper } from 'lib/testHelpers';
import { clusterConnectConnectorTasksPath } from 'lib/paths';
import ListItemContainer from 'components/Connect/Details/Tasks/ListItem/ListItemContainer';
import ListItem, {
ListItemProps,
} from 'components/Connect/Details/Tasks/ListItem/ListItem';
import { tasks } from 'redux/reducers/connect/__test__/fixtures';
jest.mock('components/Connect/StatusTag', () => 'mock-StatusTag');
describe('ListItem', () => {
containerRendersView(
<table>
<tbody>
<ListItemContainer task={tasks[0]} />
</tbody>
</table>,
ListItem
);
describe('view', () => {
const pathname = clusterConnectConnectorTasksPath(
':clusterName',
':connectName',
':connectorName'
);
const clusterName = 'my-cluster';
const connectName = 'my-connect';
const connectorName = 'my-connector';
const setupWrapper = (props: Partial<ListItemProps> = {}) => (
<TestRouterWrapper
pathname={pathname}
urlParams={{ clusterName, connectName, connectorName }}
>
<table>
<tbody>
<ListItem task={tasks[0]} restartTask={jest.fn()} {...props} />
</tbody>
</table>
</TestRouterWrapper>
);
it('matches snapshot', () => {
const wrapper = create(setupWrapper());
expect(wrapper.toJSON()).toMatchSnapshot();
});
it('calls restartTask on button click', async () => {
const restartTask = jest.fn();
const wrapper = mount(setupWrapper({ restartTask }));
await act(async () => {
wrapper.find('button').simulate('click');
});
expect(restartTask).toHaveBeenCalledTimes(1);
expect(restartTask).toHaveBeenCalledWith(
clusterName,
connectName,
connectorName,
tasks[0].id?.task
);
});
});
});

View file

@ -0,0 +1,40 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ListItem view matches snapshot 1`] = `
<table>
<tbody>
<tr>
<td
className="has-text-overflow-ellipsis"
>
1
</td>
<td>
kafka-connect0:8083
</td>
<td>
<mock-StatusTag
status="RUNNING"
/>
</td>
<td />
<td>
<button
className="button is-small is-pulled-right"
disabled={false}
onClick={[Function]}
type="button"
>
<span
className="icon"
>
<i
className="fas fa-sync-alt"
/>
</span>
</button>
</td>
</tr>
</tbody>
</table>
`;

View file

@ -0,0 +1,68 @@
import React from 'react';
import { useParams } from 'react-router';
import { Task } from 'generated-sources';
import { ClusterName, ConnectName, ConnectorName } from 'redux/interfaces';
import PageLoader from 'components/common/PageLoader/PageLoader';
import ListItemContainer from './ListItem/ListItemContainer';
interface RouterParams {
clusterName: ClusterName;
connectName: ConnectName;
connectorName: ConnectorName;
}
export interface TasksProps {
fetchTasks(
clusterName: ClusterName,
connectName: ConnectName,
connectorName: ConnectorName,
silent?: boolean
): void;
areTasksFetching: boolean;
tasks: Task[];
}
const Tasks: React.FC<TasksProps> = ({
fetchTasks,
areTasksFetching,
tasks,
}) => {
const { clusterName, connectName, connectorName } = useParams<RouterParams>();
React.useEffect(() => {
fetchTasks(clusterName, connectName, connectorName, true);
}, [fetchTasks, clusterName, connectName, connectorName]);
if (areTasksFetching) {
return <PageLoader />;
}
return (
<table className="table is-fullwidth">
<thead>
<tr>
<th>ID</th>
<th>Worker</th>
<th>State</th>
<th>Trace</th>
<th>
<span className="is-pulled-right">Restart</span>
</th>
</tr>
</thead>
<tbody>
{tasks.length === 0 && (
<tr>
<td colSpan={10}>No tasks found</td>
</tr>
)}
{tasks.map((task) => (
<ListItemContainer key={task.status?.id} task={task} />
))}
</tbody>
</table>
);
};
export default Tasks;

View file

@ -0,0 +1,21 @@
import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
import { RootState } from 'redux/interfaces';
import { fetchConnectorTasks } from 'redux/actions';
import {
getAreConnectorTasksFetching,
getConnectorTasks,
} from 'redux/reducers/connect/selectors';
import Tasks from './Tasks';
const mapStateToProps = (state: RootState) => ({
areTasksFetching: getAreConnectorTasksFetching(state),
tasks: getConnectorTasks(state),
});
const mapDispatchToProps = {
fetchTasks: fetchConnectorTasks,
};
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Tasks));

View file

@ -0,0 +1,71 @@
import React from 'react';
import { create } from 'react-test-renderer';
import { mount } from 'enzyme';
import { containerRendersView, TestRouterWrapper } from 'lib/testHelpers';
import { clusterConnectConnectorTasksPath } from 'lib/paths';
import TasksContainer from 'components/Connect/Details/Tasks/TasksContainer';
import Tasks, { TasksProps } from 'components/Connect/Details/Tasks/Tasks';
import { tasks } from 'redux/reducers/connect/__test__/fixtures';
jest.mock('components/common/PageLoader/PageLoader', () => 'mock-PageLoader');
jest.mock(
'components/Connect/Details/Tasks/ListItem/ListItemContainer',
() => 'tr' // need to mock as `tr` to let dom validtion pass
);
describe('Tasks', () => {
containerRendersView(<TasksContainer />, Tasks);
describe('view', () => {
const pathname = clusterConnectConnectorTasksPath(
':clusterName',
':connectName',
':connectorName'
);
const clusterName = 'my-cluster';
const connectName = 'my-connect';
const connectorName = 'my-connector';
const setupWrapper = (props: Partial<TasksProps> = {}) => (
<TestRouterWrapper
pathname={pathname}
urlParams={{ clusterName, connectName, connectorName }}
>
<Tasks
fetchTasks={jest.fn()}
areTasksFetching={false}
tasks={tasks}
{...props}
/>
</TestRouterWrapper>
);
it('matches snapshot', () => {
const wrapper = create(setupWrapper());
expect(wrapper.toJSON()).toMatchSnapshot();
});
it('matches snapshot when fetching tasks', () => {
const wrapper = create(setupWrapper({ areTasksFetching: true }));
expect(wrapper.toJSON()).toMatchSnapshot();
});
it('matches snapshot when no tasks', () => {
const wrapper = create(setupWrapper({ tasks: [] }));
expect(wrapper.toJSON()).toMatchSnapshot();
});
it('fetches tasks on mount', () => {
const fetchTasks = jest.fn();
mount(setupWrapper({ fetchTasks }));
expect(fetchTasks).toHaveBeenCalledTimes(1);
expect(fetchTasks).toHaveBeenCalledWith(
clusterName,
connectName,
connectorName,
true
);
});
});
});

View file

@ -0,0 +1,138 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Tasks view matches snapshot 1`] = `
<table
className="table is-fullwidth"
>
<thead>
<tr>
<th>
ID
</th>
<th>
Worker
</th>
<th>
State
</th>
<th>
Trace
</th>
<th>
<span
className="is-pulled-right"
>
Restart
</span>
</th>
</tr>
</thead>
<tbody>
<tr
task={
Object {
"config": Object {
"batch.size": "2000",
"file": "/some/file",
"task.class": "org.apache.kafka.connect.file.FileStreamSourceTask",
"topic": "test-topic",
},
"id": Object {
"connector": "first",
"task": 1,
},
"status": Object {
"id": 1,
"state": "RUNNING",
"workerId": "kafka-connect0:8083",
},
}
}
/>
<tr
task={
Object {
"config": Object {
"batch.size": "1000",
"file": "/some/file2",
"task.class": "org.apache.kafka.connect.file.FileStreamSourceTask",
"topic": "test-topic",
},
"id": Object {
"connector": "first",
"task": 2,
},
"status": Object {
"id": 2,
"state": "FAILED",
"trace": "Failure 1",
"workerId": "kafka-connect0:8083",
},
}
}
/>
<tr
task={
Object {
"config": Object {
"batch.size": "3000",
"file": "/some/file3",
"task.class": "org.apache.kafka.connect.file.FileStreamSourceTask",
"topic": "test-topic",
},
"id": Object {
"connector": "first",
"task": 3,
},
"status": Object {
"id": 3,
"state": "RUNNING",
"workerId": "kafka-connect0:8083",
},
}
}
/>
</tbody>
</table>
`;
exports[`Tasks view matches snapshot when fetching tasks 1`] = `<mock-PageLoader />`;
exports[`Tasks view matches snapshot when no tasks 1`] = `
<table
className="table is-fullwidth"
>
<thead>
<tr>
<th>
ID
</th>
<th>
Worker
</th>
<th>
State
</th>
<th>
Trace
</th>
<th>
<span
className="is-pulled-right"
>
Restart
</span>
</th>
</tr>
</thead>
<tbody>
<tr>
<td
colSpan={10}
>
No tasks found
</td>
</tr>
</tbody>
</table>
`;

View file

@ -0,0 +1,104 @@
import React from 'react';
import { create } from 'react-test-renderer';
import { mount } from 'enzyme';
import { containerRendersView, TestRouterWrapper } from 'lib/testHelpers';
import { clusterConnectConnectorPath } from 'lib/paths';
import DetailsContainer from 'components/Connect/Details/DetailsContainer';
import Details, { DetailsProps } from 'components/Connect/Details/Details';
import { connector, tasks } from 'redux/reducers/connect/__test__/fixtures';
jest.mock('components/common/PageLoader/PageLoader', () => 'mock-PageLoader');
jest.mock(
'components/Connect/Details/Overview/OverviewContainer',
() => 'mock-OverviewContainer'
);
jest.mock(
'components/Connect/Details/Tasks/TasksContainer',
() => 'mock-TasksContainer'
);
jest.mock(
'components/Connect/Details/Config/ConfigContainer',
() => 'mock-ConfigContainer'
);
jest.mock(
'components/Connect/Details/Actions/ActionsContainer',
() => 'mock-ActionsContainer'
);
describe('Details', () => {
containerRendersView(<DetailsContainer />, Details);
describe('view', () => {
const pathname = clusterConnectConnectorPath(
':clusterName',
':connectName',
':connectorName'
);
const clusterName = 'my-cluster';
const connectName = 'my-connect';
const connectorName = 'my-connector';
const setupWrapper = (props: Partial<DetailsProps> = {}) => (
<TestRouterWrapper
pathname={pathname}
urlParams={{ clusterName, connectName, connectorName }}
>
<Details
fetchConnector={jest.fn()}
fetchTasks={jest.fn()}
isConnectorFetching={false}
areTasksFetching={false}
connector={connector}
tasks={tasks}
{...props}
/>
</TestRouterWrapper>
);
it('matches snapshot', () => {
const wrapper = create(setupWrapper());
expect(wrapper.toJSON()).toMatchSnapshot();
});
it('matches snapshot when fetching connector', () => {
const wrapper = create(setupWrapper({ isConnectorFetching: true }));
expect(wrapper.toJSON()).toMatchSnapshot();
});
it('matches snapshot when fetching tasks', () => {
const wrapper = create(setupWrapper({ areTasksFetching: true }));
expect(wrapper.toJSON()).toMatchSnapshot();
});
it('is empty when no connector', () => {
const wrapper = mount(setupWrapper({ connector: null }));
expect(wrapper.html()).toEqual('');
});
it('fetches connector on mount', () => {
const fetchConnector = jest.fn();
mount(setupWrapper({ fetchConnector }));
expect(fetchConnector).toHaveBeenCalledTimes(1);
expect(fetchConnector).toHaveBeenCalledWith(
clusterName,
connectName,
connectorName
);
});
it('fetches tasks on mount', () => {
const fetchTasks = jest.fn();
mount(setupWrapper({ fetchTasks }));
expect(fetchTasks).toHaveBeenCalledTimes(1);
expect(fetchTasks).toHaveBeenCalledWith(
clusterName,
connectName,
connectorName
);
});
});
});

View file

@ -0,0 +1,103 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Details view matches snapshot 1`] = `
<div
className="box"
>
<nav
className="navbar mb-4"
role="navigation"
>
<div
className="navbar-start tabs mb-0"
>
<a
aria-current="page"
className="navbar-item is-tab is-active"
href="/ui/clusters/my-cluster/connects/my-connect/connectors/my-connector"
onClick={[Function]}
style={Object {}}
>
Overview
</a>
<a
aria-current={null}
className="navbar-item is-tab"
href="/ui/clusters/my-cluster/connects/my-connect/connectors/my-connector/tasks"
onClick={[Function]}
>
Tasks
</a>
<a
aria-current={null}
className="navbar-item is-tab"
href="/ui/clusters/my-cluster/connects/my-connect/connectors/my-connector/config"
onClick={[Function]}
>
Config
</a>
</div>
<div
className="navbar-end"
>
<mock-ActionsContainer />
</div>
</nav>
<mock-OverviewContainer
history={
Object {
"action": "POP",
"block": [Function],
"canGo": [Function],
"createHref": [Function],
"entries": Array [
Object {
"hash": "",
"key": "test",
"pathname": "/ui/clusters/my-cluster/connects/my-connect/connectors/my-connector",
"search": "",
},
],
"go": [Function],
"goBack": [Function],
"goForward": [Function],
"index": 0,
"length": 1,
"listen": [Function],
"location": Object {
"hash": "",
"key": "test",
"pathname": "/ui/clusters/my-cluster/connects/my-connect/connectors/my-connector",
"search": "",
},
"push": [Function],
"replace": [Function],
}
}
location={
Object {
"hash": "",
"key": "test",
"pathname": "/ui/clusters/my-cluster/connects/my-connect/connectors/my-connector",
"search": "",
}
}
match={
Object {
"isExact": true,
"params": Object {
"clusterName": "my-cluster",
"connectName": "my-connect",
"connectorName": "my-connector",
},
"path": "/ui/clusters/:clusterName/connects/:connectName/connectors/:connectorName",
"url": "/ui/clusters/my-cluster/connects/my-connect/connectors/my-connector",
}
}
/>
</div>
`;
exports[`Details view matches snapshot when fetching connector 1`] = `<mock-PageLoader />`;
exports[`Details view matches snapshot when fetching tasks 1`] = `<mock-PageLoader />`;

View file

@ -0,0 +1,142 @@
import React from 'react';
import { useHistory, useParams } from 'react-router-dom';
import { Controller, useForm } from 'react-hook-form';
import { ErrorMessage } from '@hookform/error-message';
import { yupResolver } from '@hookform/resolvers/yup';
import { Connector } from 'generated-sources';
import {
ClusterName,
ConnectName,
ConnectorConfig,
ConnectorName,
} from 'redux/interfaces';
import { clusterConnectConnectorConfigPath } from 'lib/paths';
import yup from 'lib/yupExtended';
import JSONEditor from 'components/common/JSONEditor/JSONEditor';
import PageLoader from 'components/common/PageLoader/PageLoader';
const validationSchema = yup.object().shape({
config: yup.string().required().isJsonObject(),
});
interface RouterParams {
clusterName: ClusterName;
connectName: ConnectName;
connectorName: ConnectorName;
}
interface FormValues {
config: string;
}
export interface EditProps {
fetchConfig(
clusterName: ClusterName,
connectName: ConnectName,
connectorName: ConnectorName
): Promise<void>;
isConfigFetching: boolean;
config: ConnectorConfig | null;
updateConfig(
clusterName: ClusterName,
connectName: ConnectName,
connectorName: ConnectorName,
connectorConfig: ConnectorConfig
): Promise<Connector | undefined>;
}
const Edit: React.FC<EditProps> = ({
fetchConfig,
isConfigFetching,
config,
updateConfig,
}) => {
const { clusterName, connectName, connectorName } = useParams<RouterParams>();
const history = useHistory();
const {
register,
errors,
handleSubmit,
control,
formState: { isDirty, isSubmitting, isValid },
setValue,
} = useForm<FormValues>({
mode: 'onTouched',
resolver: yupResolver(validationSchema),
defaultValues: {
config: JSON.stringify(config, null, '\t'),
},
});
React.useEffect(() => {
fetchConfig(clusterName, connectName, connectorName);
}, [fetchConfig, clusterName, connectName, connectorName]);
React.useEffect(() => {
if (config) {
setValue('config', JSON.stringify(config, null, '\t'));
}
}, [config, setValue]);
const onSubmit = React.useCallback(
async (values: FormValues) => {
const connector = await updateConfig(
clusterName,
connectName,
connectorName,
JSON.parse(values.config)
);
if (connector) {
history.push(
clusterConnectConnectorConfigPath(
clusterName,
connectName,
connectorName
)
);
}
},
[updateConfig, clusterName, connectName, connectorName]
);
if (isConfigFetching) return <PageLoader />;
return (
<div className="box">
<form onSubmit={handleSubmit(onSubmit)}>
<div className="field">
<div className="control">
<Controller
control={control}
name="config"
render={({ name, value, onChange, onBlur }) => (
<JSONEditor
ref={register}
name={name}
value={value}
onChange={onChange}
onBlur={onBlur}
readOnly={isSubmitting}
/>
)}
/>
</div>
<p className="help is-danger">
<ErrorMessage errors={errors} name="config" />
</p>
</div>
<div className="field">
<div className="control">
<input
type="submit"
className="button is-primary"
disabled={!isValid || isSubmitting || !isDirty}
/>
</div>
</div>
</form>
</div>
);
};
export default Edit;

View file

@ -0,0 +1,22 @@
import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
import { RootState } from 'redux/interfaces';
import { fetchConnectorConfig, updateConnectorConfig } from 'redux/actions';
import {
getConnectorConfig,
getIsConnectorConfigFetching,
} from 'redux/reducers/connect/selectors';
import Edit from './Edit';
const mapStateToProps = (state: RootState) => ({
isConfigFetching: getIsConnectorConfigFetching(state),
config: getConnectorConfig(state),
});
const mapDispatchToProps = {
fetchConfig: fetchConnectorConfig,
updateConfig: updateConnectorConfig,
};
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Edit));

View file

@ -0,0 +1,115 @@
import React from 'react';
import { create } from 'react-test-renderer';
import { mount } from 'enzyme';
import { act } from 'react-dom/test-utils';
import { containerRendersView, TestRouterWrapper } from 'lib/testHelpers';
import {
clusterConnectConnectorConfigPath,
clusterConnectConnectorEditPath,
} from 'lib/paths';
import EditContainer from 'components/Connect/Edit/EditContainer';
import Edit, { EditProps } from 'components/Connect/Edit/Edit';
import { connector } from 'redux/reducers/connect/__test__/fixtures';
jest.mock('components/common/PageLoader/PageLoader', () => 'mock-PageLoader');
jest.mock('components/common/JSONEditor/JSONEditor', () => 'mock-JSONEditor');
const mockHistoryPush = jest.fn();
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useHistory: () => ({
push: mockHistoryPush,
}),
}));
describe('Edit', () => {
containerRendersView(<EditContainer />, Edit);
describe('view', () => {
const pathname = clusterConnectConnectorEditPath(
':clusterName',
':connectName',
':connectorName'
);
const clusterName = 'my-cluster';
const connectName = 'my-connect';
const connectorName = 'my-connector';
const setupWrapper = (props: Partial<EditProps> = {}) => (
<TestRouterWrapper
pathname={pathname}
urlParams={{ clusterName, connectName, connectorName }}
>
<Edit
fetchConfig={jest.fn()}
isConfigFetching={false}
config={connector.config}
updateConfig={jest.fn()}
{...props}
/>
</TestRouterWrapper>
);
it('matches snapshot', () => {
const wrapper = create(setupWrapper());
expect(wrapper.toJSON()).toMatchSnapshot();
});
it('matches snapshot when fetching config', () => {
const wrapper = create(setupWrapper({ isConfigFetching: true }));
expect(wrapper.toJSON()).toMatchSnapshot();
});
it('fetches config on mount', () => {
const fetchConfig = jest.fn();
mount(setupWrapper({ fetchConfig }));
expect(fetchConfig).toHaveBeenCalledTimes(1);
expect(fetchConfig).toHaveBeenCalledWith(
clusterName,
connectName,
connectorName
);
});
it('calls updateConfig on form submit', async () => {
const updateConfig = jest.fn();
const wrapper = mount(setupWrapper({ updateConfig }));
await act(async () => {
wrapper.find('form').simulate('submit');
});
expect(updateConfig).toHaveBeenCalledTimes(1);
expect(updateConfig).toHaveBeenCalledWith(
clusterName,
connectName,
connectorName,
connector.config
);
});
it('redirects to connector config view on successful submit', async () => {
const updateConfig = jest.fn().mockResolvedValueOnce(connector);
const wrapper = mount(setupWrapper({ updateConfig }));
await act(async () => {
wrapper.find('form').simulate('submit');
});
expect(mockHistoryPush).toHaveBeenCalledTimes(1);
expect(mockHistoryPush).toHaveBeenCalledWith(
clusterConnectConnectorConfigPath(
clusterName,
connectName,
connectorName
)
);
});
it('does not redirect to connector config view on unsuccessful submit', async () => {
const updateConfig = jest.fn().mockResolvedValueOnce(undefined);
const wrapper = mount(setupWrapper({ updateConfig }));
await act(async () => {
wrapper.find('form').simulate('submit');
});
expect(mockHistoryPush).not.toHaveBeenCalled();
});
});
});

View file

@ -0,0 +1,50 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Edit view matches snapshot 1`] = `
<div
className="box"
>
<form
onSubmit={[Function]}
>
<div
className="field"
>
<div
className="control"
>
<mock-JSONEditor
name="config"
onBlur={[Function]}
onChange={[Function]}
readOnly={false}
value="{
\\"connector.class\\": \\"FileStreamSource\\",
\\"tasks.max\\": \\"10\\",
\\"topic\\": \\"test-topic\\",
\\"file\\": \\"/some/file\\"
}"
/>
</div>
<p
className="help is-danger"
/>
</div>
<div
className="field"
>
<div
className="control"
>
<input
className="button is-primary"
disabled={true}
type="submit"
/>
</div>
</div>
</form>
</div>
`;
exports[`Edit view matches snapshot when fetching config 1`] = `<mock-PageLoader />`;

View file

@ -1,13 +1,14 @@
import React from 'react';
import { Link, useParams } from 'react-router-dom';
import { Connect, FullConnectorInfo } from 'generated-sources';
import { useParams } from 'react-router-dom';
import { ClusterName } from 'redux/interfaces';
import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb';
import { clusterConnectorNewPath } from 'lib/paths';
import ClusterContext from 'components/contexts/ClusterContext';
import Indicator from 'components/common/Dashboard/Indicator';
import MetricsWrapper from 'components/common/Dashboard/MetricsWrapper';
import PageLoader from 'components/common/PageLoader/PageLoader';
import ListItem from 'components/Connect/List/ListItem';
import ListItem from './ListItem';
export interface ListProps {
areConnectsFetching: boolean;
@ -35,13 +36,7 @@ const List: React.FC<ListProps> = ({
}, [fetchConnects, fetchConnectors, clusterName]);
return (
<div className="section">
<Breadcrumb>All Connectors</Breadcrumb>
<article className="message is-warning">
<div className="message-body">
Kafka Connect section is under construction.
</div>
</article>
<>
<MetricsWrapper>
<Indicator
className="level-left is-one-third"
@ -54,9 +49,12 @@ const List: React.FC<ListProps> = ({
{!isReadOnly && (
<div className="level-item level-right">
<button type="button" className="button is-primary" disabled>
<Link
className="button is-primary"
to={clusterConnectorNewPath(clusterName)}
>
Create Connector
</button>
</Link>
</div>
)}
</MetricsWrapper>
@ -96,7 +94,7 @@ const List: React.FC<ListProps> = ({
</table>
</div>
)}
</div>
</>
);
};

View file

@ -1,9 +1,9 @@
import React from 'react';
import cx from 'classnames';
import { FullConnectorInfo } from 'generated-sources';
import { clusterTopicPath } from 'lib/paths';
import { clusterConnectConnectorPath, clusterTopicPath } from 'lib/paths';
import { ClusterName } from 'redux/interfaces';
import { Link } from 'react-router-dom';
import { Link, NavLink } from 'react-router-dom';
import { useDispatch } from 'react-redux';
import { deleteConnector } from 'redux/actions';
import Dropdown from 'components/common/Dropdown/Dropdown';
@ -50,7 +50,16 @@ const ListItem: React.FC<ListItemProps> = ({
return (
<tr>
<td className="has-text-overflow-ellipsis">{name}</td>
<td className="has-text-overflow-ellipsis">
<NavLink
exact
to={clusterConnectConnectorPath(clusterName, connect, name)}
activeClassName="is-active"
className="title is-6"
>
{name}
</NavLink>
</td>
<td>{connect}</td>
<td>{type}</td>
<td>{connectorClass}</td>
@ -61,7 +70,7 @@ const ListItem: React.FC<ListItemProps> = ({
</Link>
))}
</td>
<td>{status && <StatusTag status={status} />}</td>
<td>{status && <StatusTag status={status.state} />}</td>
<td>
{runningTasks && (
<span

View file

@ -3,7 +3,7 @@ import { mount } from 'enzyme';
import { Provider } from 'react-redux';
import { StaticRouter } from 'react-router-dom';
import configureStore from 'redux/store/configureStore';
import { connectorsPayload } from 'redux/reducers/connect/__test__/fixtures';
import { connectors } from 'redux/reducers/connect/__test__/fixtures';
import ClusterContext, {
ContextProps,
initialValue,
@ -66,7 +66,7 @@ describe('Connectors List', () => {
const wrapper = mount(
setupComponent({
areConnectorsFetching: false,
connectors: connectorsPayload,
connectors,
})
);
expect(wrapper.exists('PageLoader')).toBeFalsy();
@ -85,7 +85,7 @@ describe('Connectors List', () => {
setupComponent({}, { ...initialValue, isReadOnly: false })
);
expect(
wrapper.exists('.level-item.level-right > button.is-primary')
wrapper.exists('.level-item.level-right > .button.is-primary')
).toBeTruthy();
});
@ -95,7 +95,7 @@ describe('Connectors List', () => {
setupComponent({}, { ...initialValue, isReadOnly: true })
);
expect(
wrapper.exists('.level-item.level-right > button.is-primary')
wrapper.exists('.level-item.level-right > .button.is-primary')
).toBeFalsy();
});
});

View file

@ -2,19 +2,26 @@ import React from 'react';
import { mount } from 'enzyme';
import { Provider } from 'react-redux';
import { BrowserRouter } from 'react-router-dom';
import { connectorsPayload } from 'redux/reducers/connect/__test__/fixtures';
import { connectors } from 'redux/reducers/connect/__test__/fixtures';
import configureStore from 'redux/store/configureStore';
import ListItem, { ListItemProps } from 'components/Connect/List/ListItem';
import { ConfirmationModalProps } from 'components/common/ConfirmationModal/ConfirmationModal';
const store = configureStore();
const mockDeleteConnector = jest.fn();
jest.mock('redux/actions', () => ({
...jest.requireActual('redux/actions'),
deleteConnector: () => mockDeleteConnector(),
}));
jest.mock(
'components/common/ConfirmationModal/ConfirmationModal',
() => 'mock-ConfirmationModal'
);
describe('Connectors ListItem', () => {
const connector = connectorsPayload[0];
const connector = connectors[0];
const setupWrapper = (props: Partial<ListItemProps> = {}) => (
<Provider store={store}>
<BrowserRouter>
@ -60,7 +67,7 @@ describe('Connectors ListItem', () => {
expect(wrapper.find('td').at(6).text()).toEqual('');
});
it('handles delete', () => {
it('handles cancel', () => {
const wrapper = mount(setupWrapper());
expect(wrapper.find('mock-ConfirmationModal').prop('isOpen')).toBeFalsy();
wrapper.find('DropdownItem').last().simulate('click');
@ -70,6 +77,24 @@ describe('Connectors ListItem', () => {
expect(wrapper.find('mock-ConfirmationModal').prop('isOpen')).toBeFalsy();
});
it('handles delete', () => {
const wrapper = mount(setupWrapper());
const modalProps = wrapper
.find('mock-ConfirmationModal')
.props() as ConfirmationModalProps;
modalProps.onConfirm();
expect(mockDeleteConnector).toHaveBeenCalledTimes(1);
});
it('handles delete when clusterName is not present', () => {
const wrapper = mount(setupWrapper({ clusterName: undefined }));
const modalProps = wrapper
.find('mock-ConfirmationModal')
.props() as ConfirmationModalProps;
modalProps.onConfirm();
expect(mockDeleteConnector).toHaveBeenCalledTimes(0);
});
it('matches snapshot', () => {
const wrapper = mount(setupWrapper());
expect(wrapper).toMatchSnapshot();

View file

@ -45,7 +45,9 @@ exports[`Connectors ListItem matches snapshot 1`] = `
"connectorClass": "FileStreamSource",
"failedTasksCount": 0,
"name": "hdfs-source-connector",
"status": "RUNNING",
"status": Object {
"state": "RUNNING",
},
"tasksCount": 2,
"topics": Array [
"test-topic",
@ -58,7 +60,41 @@ exports[`Connectors ListItem matches snapshot 1`] = `
<td
className="has-text-overflow-ellipsis"
>
hdfs-source-connector
<NavLink
activeClassName="is-active"
className="title is-6"
exact={true}
to="/ui/clusters/local/connects/first/connectors/hdfs-source-connector"
>
<Link
aria-current={null}
className="title is-6"
to={
Object {
"hash": "",
"pathname": "/ui/clusters/local/connects/first/connectors/hdfs-source-connector",
"search": "",
"state": null,
}
}
>
<LinkAnchor
aria-current={null}
className="title is-6"
href="/ui/clusters/local/connects/first/connectors/hdfs-source-connector"
navigate={[Function]}
>
<a
aria-current={null}
className="title is-6"
href="/ui/clusters/local/connects/first/connectors/hdfs-source-connector"
onClick={[Function]}
>
hdfs-source-connector
</a>
</LinkAnchor>
</Link>
</NavLink>
</td>
<td>
first

View file

@ -0,0 +1,179 @@
import React from 'react';
import { useHistory, useParams } from 'react-router-dom';
import { Controller, useForm } from 'react-hook-form';
import { ErrorMessage } from '@hookform/error-message';
import { yupResolver } from '@hookform/resolvers/yup';
import { Connect, Connector, NewConnector } from 'generated-sources';
import { ClusterName, ConnectName } from 'redux/interfaces';
import { clusterConnectConnectorPath } from 'lib/paths';
import yup from 'lib/yupExtended';
import JSONEditor from 'components/common/JSONEditor/JSONEditor';
import PageLoader from 'components/common/PageLoader/PageLoader';
const validationSchema = yup.object().shape({
name: yup.string().required(),
config: yup.string().required().isJsonObject(),
});
interface RouterParams {
clusterName: ClusterName;
}
export interface NewProps {
fetchConnects(clusterName: ClusterName): void;
areConnectsFetching: boolean;
connects: Connect[];
createConnector(
clusterName: ClusterName,
connectName: ConnectName,
newConnector: NewConnector
): Promise<Connector | undefined>;
}
interface FormValues {
connectName: ConnectName;
name: string;
config: string;
}
const New: React.FC<NewProps> = ({
fetchConnects,
areConnectsFetching,
connects,
createConnector,
}) => {
const { clusterName } = useParams<RouterParams>();
const history = useHistory();
const {
register,
errors,
handleSubmit,
control,
formState: { isDirty, isSubmitting, isValid },
getValues,
setValue,
} = useForm<FormValues>({
mode: 'onTouched',
resolver: yupResolver(validationSchema),
defaultValues: {
connectName: connects[0]?.name || '',
name: '',
config: '',
},
});
React.useEffect(() => {
fetchConnects(clusterName);
}, [fetchConnects, clusterName]);
React.useEffect(() => {
if (connects && connects.length > 0 && !getValues().connectName) {
setValue('connectName', connects[0].name);
}
}, [connects, getValues, setValue]);
const connectNameFieldClassName = React.useMemo(
() => (connects.length > 1 ? '' : 'is-hidden'),
[connects]
);
const onSubmit = React.useCallback(
async (values: FormValues) => {
const connector = await createConnector(clusterName, values.connectName, {
name: values.name,
config: JSON.parse(values.config),
});
if (connector) {
history.push(
clusterConnectConnectorPath(
clusterName,
connector.connect,
connector.name
)
);
}
},
[createConnector, clusterName]
);
if (areConnectsFetching) {
return <PageLoader />;
}
if (connects.length === 0) {
return null;
}
return (
<div className="box">
<form onSubmit={handleSubmit(onSubmit)}>
<div className={['field', connectNameFieldClassName].join(' ')}>
<label className="label">Connect *</label>
<div className="control select">
<select ref={register} name="connectName" disabled={isSubmitting}>
{connects.map(({ name }) => (
<option key={name} value={name}>
{name}
</option>
))}
</select>
</div>
<p className="help is-danger">
<ErrorMessage errors={errors} name="connectName" />
</p>
</div>
<div className="field">
<label className="label">Name *</label>
<div className="control">
<input
ref={register}
className="input"
placeholder="Connector Name"
name="name"
autoComplete="off"
disabled={isSubmitting}
/>
</div>
<p className="help is-danger">
<ErrorMessage errors={errors} name="name" />
</p>
</div>
<div className="field">
<label className="label">Config *</label>
<div className="control">
<Controller
control={control}
name="config"
render={({ name, onChange, onBlur }) => (
<JSONEditor
ref={register}
name={name}
onChange={onChange}
onBlur={onBlur}
readOnly={isSubmitting}
/>
)}
/>
</div>
<p className="help is-danger">
<ErrorMessage errors={errors} name="config" />
</p>
</div>
<div className="field">
<div className="control">
<input
type="submit"
className="button is-primary"
disabled={!isValid || isSubmitting || !isDirty}
/>
</div>
</div>
</form>
</div>
);
};
export default New;

View file

@ -0,0 +1,22 @@
import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
import { createConnector, fetchConnects } from 'redux/actions';
import { RootState } from 'redux/interfaces';
import {
getAreConnectsFetching,
getConnects,
} from 'redux/reducers/connect/selectors';
import New from './New';
const mapStateToProps = (state: RootState) => ({
areConnectsFetching: getAreConnectsFetching(state),
connects: getConnects(state),
});
const mapDispatchToProps = {
fetchConnects,
createConnector,
};
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(New));

View file

@ -0,0 +1,117 @@
import React from 'react';
import { create, act as rendererAct } from 'react-test-renderer';
import { mount, ReactWrapper } from 'enzyme';
import { act } from 'react-dom/test-utils';
import { containerRendersView, TestRouterWrapper } from 'lib/testHelpers';
import {
clusterConnectConnectorPath,
clusterConnectorNewPath,
} from 'lib/paths';
import NewContainer from 'components/Connect/New/NewContainer';
import New, { NewProps } from 'components/Connect/New/New';
import { connects, connector } from 'redux/reducers/connect/__test__/fixtures';
jest.mock('components/common/PageLoader/PageLoader', () => 'mock-PageLoader');
jest.mock('components/common/JSONEditor/JSONEditor', () => 'mock-JSONEditor');
const mockHistoryPush = jest.fn();
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useHistory: () => ({
push: mockHistoryPush,
}),
}));
describe('New', () => {
containerRendersView(<NewContainer />, New);
describe('view', () => {
const pathname = clusterConnectorNewPath(':clusterName');
const clusterName = 'my-cluster';
const simulateFormSubmit = (wrapper: ReactWrapper) =>
act(async () => {
const nameInput = wrapper
.find('input[name="name"]')
.getDOMNode<HTMLInputElement>();
nameInput.value = 'my-connector';
wrapper
.find('mock-JSONEditor')
.simulate('change', { target: { value: '{"class":"MyClass"}' } });
wrapper.find('input[type="submit"]').simulate('submit');
});
const setupWrapper = (props: Partial<NewProps> = {}) => (
<TestRouterWrapper pathname={pathname} urlParams={{ clusterName }}>
<New
fetchConnects={jest.fn()}
areConnectsFetching={false}
connects={connects}
createConnector={jest.fn()}
{...props}
/>
</TestRouterWrapper>
);
it('matches snapshot', async () => {
let wrapper = create(<div />);
await rendererAct(async () => {
wrapper = create(setupWrapper());
});
expect(wrapper.toJSON()).toMatchSnapshot();
});
it('matches snapshot when fetching connects', async () => {
let wrapper = create(<div />);
await rendererAct(async () => {
wrapper = create(setupWrapper({ areConnectsFetching: true }));
});
expect(wrapper.toJSON()).toMatchSnapshot();
});
it('fetches connects on mount', async () => {
const fetchConnects = jest.fn();
await act(async () => {
mount(setupWrapper({ fetchConnects }));
});
expect(fetchConnects).toHaveBeenCalledTimes(1);
expect(fetchConnects).toHaveBeenCalledWith(clusterName);
});
it('calls createConnector on form submit', async () => {
const createConnector = jest.fn();
const wrapper = mount(setupWrapper({ createConnector }));
await simulateFormSubmit(wrapper);
expect(createConnector).toHaveBeenCalledTimes(1);
expect(createConnector).toHaveBeenCalledWith(
clusterName,
connects[0].name,
{
name: 'my-connector',
config: { class: 'MyClass' },
}
);
});
it('redirects to connector details view on successful submit', async () => {
const createConnector = jest.fn().mockResolvedValue(connector);
const wrapper = mount(setupWrapper({ createConnector }));
await simulateFormSubmit(wrapper);
expect(mockHistoryPush).toHaveBeenCalledTimes(1);
expect(mockHistoryPush).toHaveBeenCalledWith(
clusterConnectConnectorPath(
clusterName,
connects[0].name,
connector.name
)
);
});
it('does not redirect to connector details view on unsuccessful submit', async () => {
const createConnector = jest.fn().mockResolvedValueOnce(undefined);
const wrapper = mount(setupWrapper({ createConnector }));
await simulateFormSubmit(wrapper);
expect(mockHistoryPush).not.toHaveBeenCalled();
});
});
});

View file

@ -0,0 +1,103 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`New view matches snapshot 1`] = `
<div
className="box"
>
<form
onSubmit={[Function]}
>
<div
className="field "
>
<label
className="label"
>
Connect *
</label>
<div
className="control select"
>
<select
disabled={false}
name="connectName"
>
<option
value="first"
>
first
</option>
<option
value="second"
>
second
</option>
</select>
</div>
<p
className="help is-danger"
/>
</div>
<div
className="field"
>
<label
className="label"
>
Name *
</label>
<div
className="control"
>
<input
autoComplete="off"
className="input"
disabled={false}
name="name"
placeholder="Connector Name"
/>
</div>
<p
className="help is-danger"
/>
</div>
<div
className="field"
>
<label
className="label"
>
Config *
</label>
<div
className="control"
>
<mock-JSONEditor
name="config"
onBlur={[Function]}
onChange={[Function]}
readOnly={false}
/>
</div>
<p
className="help is-danger"
/>
</div>
<div
className="field"
>
<div
className="control"
>
<input
className="button is-primary"
disabled={true}
type="submit"
/>
</div>
</div>
</form>
</div>
`;
exports[`New view matches snapshot when fetching connects 1`] = `<mock-PageLoader />`;

View file

@ -9,8 +9,8 @@ export interface StatusTagProps {
const StatusTag: React.FC<StatusTagProps> = ({ status }) => {
const classNames = cx('tag', {
'is-success': status === ConnectorTaskStatus.RUNNING,
'is-success is-light': status === ConnectorTaskStatus.PAUSED,
'is-light': status === ConnectorTaskStatus.UNASSIGNED,
'is-light': status === ConnectorTaskStatus.PAUSED,
'is-warning': status === ConnectorTaskStatus.UNASSIGNED,
'is-danger': status === ConnectorTaskStatus.FAILED,
});

View file

@ -0,0 +1,10 @@
import React from 'react';
import { shallow } from 'enzyme';
import Connect from 'components/Connect/Connect';
describe('Connect', () => {
it('matches snapshot', () => {
const wrapper = shallow(<Connect />);
expect(wrapper).toMatchSnapshot();
});
});

View file

@ -0,0 +1,38 @@
import React from 'react';
import { create } from 'react-test-renderer';
import StatusTag, { StatusTagProps } from 'components/Connect/StatusTag';
import { ConnectorTaskStatus } from 'generated-sources';
describe('StatusTag', () => {
const setupWrapper = (props: Partial<StatusTagProps> = {}) => (
<StatusTag status={ConnectorTaskStatus.RUNNING} {...props} />
);
it('matches snapshot for running status', () => {
const wrapper = create(
setupWrapper({ status: ConnectorTaskStatus.RUNNING })
);
expect(wrapper.toJSON()).toMatchSnapshot();
});
it('matches snapshot for failed status', () => {
const wrapper = create(
setupWrapper({ status: ConnectorTaskStatus.FAILED })
);
expect(wrapper.toJSON()).toMatchSnapshot();
});
it('matches snapshot for paused status', () => {
const wrapper = create(
setupWrapper({ status: ConnectorTaskStatus.PAUSED })
);
expect(wrapper.toJSON()).toMatchSnapshot();
});
it('matches snapshot for unassigned status', () => {
const wrapper = create(
setupWrapper({ status: ConnectorTaskStatus.UNASSIGNED })
);
expect(wrapper.toJSON()).toMatchSnapshot();
});
});

View file

@ -0,0 +1,46 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Connect matches snapshot 1`] = `
<div
className="section"
>
<Switch>
<Route
component={[Function]}
path="/ui/clusters/:clusterName/connects/:connectName/connectors/:connectorName"
/>
<Route
component={[Function]}
path="/ui/clusters/:clusterName/connectors"
/>
</Switch>
<Switch>
<Route
component={
Object {
"$$typeof": Symbol(react.memo),
"WrappedComponent": [Function],
"compare": null,
"type": [Function],
}
}
exact={true}
path="/ui/clusters/:clusterName/connectors"
/>
<Route
component={[Function]}
exact={true}
path="/ui/clusters/:clusterName/connectors/create_new"
/>
<Route
component={[Function]}
exact={true}
path="/ui/clusters/:clusterName/connects/:connectName/connectors/:connectorName/edit"
/>
<Route
component={[Function]}
path="/ui/clusters/:clusterName/connects/:connectName/connectors/:connectorName"
/>
</Switch>
</div>
`;

View file

@ -0,0 +1,33 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`StatusTag matches snapshot for failed status 1`] = `
<span
className="tag is-danger"
>
FAILED
</span>
`;
exports[`StatusTag matches snapshot for paused status 1`] = `
<span
className="tag is-light"
>
PAUSED
</span>
`;
exports[`StatusTag matches snapshot for running status 1`] = `
<span
className="tag is-success"
>
RUNNING
</span>
`;
exports[`StatusTag matches snapshot for unassigned status 1`] = `
<span
className="tag is-warning"
>
UNASSIGNED
</span>
`;

View file

@ -7,6 +7,7 @@ import {
clusterConsumerGroupsPath,
clusterSchemasPath,
clusterConnectorsPath,
clusterConnectsPath,
} from 'lib/paths';
import DefaultClusterIcon from './DefaultClusterIcon';
@ -82,6 +83,10 @@ const ClusterMenu: React.FC<Props> = ({
to={clusterConnectorsPath(name)}
activeClassName="is-active"
title="Kafka Connect"
isActive={(_, location) =>
location.pathname.startsWith(clusterConnectsPath(name)) ||
location.pathname.startsWith(clusterConnectorsPath(name))
}
>
Kafka Connect
</NavLink>

View file

@ -5,6 +5,7 @@ export interface ConfirmationModalProps {
title?: React.ReactNode;
onConfirm(): void;
onCancel(): void;
isConfirming?: boolean;
}
const ConfirmationModal: React.FC<ConfirmationModalProps> = ({
@ -13,20 +14,32 @@ const ConfirmationModal: React.FC<ConfirmationModalProps> = ({
title,
onCancel,
onConfirm,
isConfirming = false,
}) => {
if (!isOpen) return null;
const cancelHandler = React.useCallback(() => {
if (!isConfirming) {
onCancel();
}
}, [isConfirming, onCancel]);
return (
<div className="modal is-active">
<div className="modal-background" onClick={onCancel} aria-hidden="true" />
<div
className="modal-background"
onClick={cancelHandler}
aria-hidden="true"
/>
<div className="modal-card">
<header className="modal-card-head">
<p className="modal-card-title">{title || 'Confirm the action'}</p>
<button
onClick={onCancel}
onClick={cancelHandler}
type="button"
className="delete"
aria-label="close"
disabled={isConfirming}
/>
</header>
<section className="modal-card-body">{children}</section>
@ -35,10 +48,16 @@ const ConfirmationModal: React.FC<ConfirmationModalProps> = ({
onClick={onConfirm}
type="button"
className="button is-danger"
disabled={isConfirming}
>
Confirm
</button>
<button onClick={onCancel} type="button" className="button">
<button
onClick={cancelHandler}
type="button"
className="button"
disabled={isConfirming}
>
Cancel
</button>
</footer>

View file

@ -24,6 +24,7 @@ describe('ConfiramationModal', () => {
expect(wrapper.exists(ConfirmationModal)).toBeTruthy();
expect(wrapper.exists('.modal.is-active')).toBeTruthy();
expect(wrapper.find('.modal-card-body').text()).toEqual(body);
expect(wrapper.find('.modal-card-foot button').length).toEqual(2);
});
it('renders modal with default header', () => {
const wrapper = mount(setupWrapper({ isOpen: true }));
@ -38,31 +39,47 @@ describe('ConfiramationModal', () => {
});
it('handles onConfirm when user clicks confirm button', () => {
const wrapper = mount(setupWrapper({ isOpen: true }));
expect(wrapper.find('.modal-card-foot button').length).toEqual(2);
const cancelBtn = wrapper.find('.modal-card-foot button').at(0);
expect(cancelBtn.text()).toEqual('Confirm');
cancelBtn.simulate('click');
const confirmBtn = wrapper.find({ children: 'Confirm' });
confirmBtn.simulate('click');
expect(cancelMock).toHaveBeenCalledTimes(0);
expect(confirmMock).toHaveBeenCalledTimes(1);
});
describe('cancellation', () => {
let wrapper: ReactWrapper;
beforeEach(() => {
wrapper = mount(setupWrapper({ isOpen: true }));
describe('when not confirming', () => {
beforeEach(() => {
wrapper = mount(setupWrapper({ isOpen: true }));
});
it('handles onCancel when user clicks on modal-background', () => {
wrapper.find('.modal-background').simulate('click');
expect(cancelMock).toHaveBeenCalledTimes(1);
expect(confirmMock).toHaveBeenCalledTimes(0);
});
it('handles onCancel when user clicks on Cancel button', () => {
const cancelBtn = wrapper.find({ children: 'Cancel' });
cancelBtn.simulate('click');
expect(cancelMock).toHaveBeenCalledTimes(1);
expect(confirmMock).toHaveBeenCalledTimes(0);
});
});
it('handles onCancel when user clicks on modal-background', () => {
wrapper.find('.modal-background').simulate('click');
expect(cancelMock).toHaveBeenCalledTimes(1);
expect(confirmMock).toHaveBeenCalledTimes(0);
});
it('handles onCancel when user clicks on Cancel button', () => {
expect(wrapper.find('.modal-card-foot button').length).toEqual(2);
const cancelBtn = wrapper.find('.modal-card-foot button').at(1);
expect(cancelBtn.text()).toEqual('Cancel');
cancelBtn.simulate('click');
expect(cancelMock).toHaveBeenCalledTimes(1);
expect(confirmMock).toHaveBeenCalledTimes(0);
describe('when confirming', () => {
beforeEach(() => {
wrapper = mount(setupWrapper({ isOpen: true, isConfirming: true }));
});
it('does not call onCancel when user clicks on modal-background', () => {
wrapper.find('.modal-background').simulate('click');
expect(cancelMock).toHaveBeenCalledTimes(0);
expect(confirmMock).toHaveBeenCalledTimes(0);
});
it('does not call onCancel when user clicks on Cancel button', () => {
const cancelBtn = wrapper.find({ children: 'Cancel' });
cancelBtn.simulate('click');
expect(cancelMock).toHaveBeenCalledTimes(0);
expect(confirmMock).toHaveBeenCalledTimes(0);
});
});
});
});

View file

@ -3,26 +3,34 @@ import AceEditor, { IAceEditorProps } from 'react-ace';
import 'ace-builds/src-noconflict/mode-json5';
import 'ace-builds/src-noconflict/theme-textmate';
import React from 'react';
import ReactAce from 'react-ace/lib/ace';
interface JSONEditorProps extends IAceEditorProps {
isFixedHeight?: boolean;
}
const JSONEditor: React.FC<JSONEditorProps> = (props) => {
const { isFixedHeight, value } = props;
return (
<AceEditor
mode="json5"
theme="textmate"
tabSize={2}
width="100%"
height={
isFixedHeight ? `${(value?.split('\n').length || 32) * 16}px` : '500px'
}
wrapEnabled
{...props}
/>
);
};
const JSONEditor = React.forwardRef<ReactAce | null, JSONEditorProps>(
(props, ref) => {
const { isFixedHeight, ...rest } = props;
return (
<AceEditor
ref={ref}
mode="json5"
theme="textmate"
tabSize={2}
width="100%"
height={
isFixedHeight
? `${(props.value?.split('\n').length || 32) * 16}px`
: '500px'
}
wrapEnabled
{...rest}
/>
);
}
);
JSONEditor.displayName = 'JSONEditor';
export default JSONEditor;

View file

@ -7,4 +7,16 @@ describe('JSONEditor component', () => {
const component = shallow(<JSONEditor value="{}" name="name" />);
expect(component).toMatchSnapshot();
});
it('matches the snapshot with fixed height', () => {
const component = shallow(
<JSONEditor value="{}" name="name" isFixedHeight />
);
expect(component).toMatchSnapshot();
});
it('matches the snapshot with fixed height with no value', () => {
const component = shallow(<JSONEditor name="name" isFixedHeight />);
expect(component).toMatchSnapshot();
});
});

View file

@ -41,3 +41,86 @@ exports[`JSONEditor component matches the snapshot 1`] = `
wrapEnabled={true}
/>
`;
exports[`JSONEditor component matches the snapshot with fixed height 1`] = `
<ReactAce
cursorStart={1}
editorProps={Object {}}
enableBasicAutocompletion={false}
enableLiveAutocompletion={false}
enableSnippets={false}
focus={false}
fontSize={12}
height="16px"
highlightActiveLine={true}
maxLines={null}
minLines={null}
mode="json5"
name="name"
navigateToFileEnd={true}
onChange={null}
onLoad={null}
onPaste={null}
onScroll={null}
placeholder={null}
readOnly={false}
scrollMargin={
Array [
0,
0,
0,
0,
]
}
setOptions={Object {}}
showGutter={true}
showPrintMargin={true}
style={Object {}}
tabSize={2}
theme="textmate"
value="{}"
width="100%"
wrapEnabled={true}
/>
`;
exports[`JSONEditor component matches the snapshot with fixed height with no value 1`] = `
<ReactAce
cursorStart={1}
editorProps={Object {}}
enableBasicAutocompletion={false}
enableLiveAutocompletion={false}
enableSnippets={false}
focus={false}
fontSize={12}
height="512px"
highlightActiveLine={true}
maxLines={null}
minLines={null}
mode="json5"
name="name"
navigateToFileEnd={true}
onChange={null}
onLoad={null}
onPaste={null}
onScroll={null}
placeholder={null}
readOnly={false}
scrollMargin={
Array [
0,
0,
0,
0,
]
}
setOptions={Object {}}
showGutter={true}
showPrintMargin={true}
style={Object {}}
tabSize={2}
theme="textmate"
width="100%"
wrapEnabled={true}
/>
`;

View file

@ -0,0 +1,24 @@
import { isValidJsonObject } from 'lib/yupExtended';
describe('yup extended', () => {
describe('isValidJsonObject', () => {
it('returns false for no value', () => {
expect(isValidJsonObject()).toBeFalsy();
});
it('returns false for invalid string', () => {
expect(isValidJsonObject('foo: bar')).toBeFalsy();
});
it('returns false on parsing error', () => {
JSON.parse = jest.fn().mockImplementationOnce(() => {
throw new Error();
});
expect(isValidJsonObject('{ "foo": "bar" }')).toBeFalsy();
});
it('returns true for valid JSON object', () => {
expect(isValidJsonObject('{ "foo": "bar" }')).toBeTruthy();
});
});
});

View file

@ -1,4 +1,10 @@
import { ClusterName, SchemaName, TopicName } from 'redux/interfaces';
import {
ClusterName,
ConnectName,
ConnectorName,
SchemaName,
TopicName,
} from 'redux/interfaces';
import { GIT_REPO_LINK } from './constants';
@ -52,5 +58,48 @@ export const clusterTopicEditPath = (
) => `${clusterTopicsPath(clusterName)}/${topicName}/edit`;
// Kafka Connect
export const clusterConnectsPath = (clusterName: ClusterName) =>
`${clusterPath(clusterName)}/connects`;
export const clusterConnectorsPath = (clusterName: ClusterName) =>
`${clusterPath(clusterName)}/connectors`;
export const clusterConnectorNewPath = (clusterName: ClusterName) =>
`${clusterConnectorsPath(clusterName)}/create_new`;
const clusterConnectConnectorsPath = (
clusterName: ClusterName,
connectName: ConnectName
) => `${clusterConnectsPath(clusterName)}/${connectName}/connectors`;
export const clusterConnectConnectorPath = (
clusterName: ClusterName,
connectName: ConnectName,
connectorName: ConnectorName
) =>
`${clusterConnectConnectorsPath(clusterName, connectName)}/${connectorName}`;
export const clusterConnectConnectorEditPath = (
clusterName: ClusterName,
connectName: ConnectName,
connectorName: ConnectorName
) =>
`${clusterConnectConnectorsPath(
clusterName,
connectName
)}/${connectorName}/edit`;
export const clusterConnectConnectorTasksPath = (
clusterName: ClusterName,
connectName: ConnectName,
connectorName: ConnectorName
) =>
`${clusterConnectConnectorPath(
clusterName,
connectName,
connectorName
)}/tasks`;
export const clusterConnectConnectorConfigPath = (
clusterName: ClusterName,
connectName: ConnectName,
connectorName: ConnectorName
) =>
`${clusterConnectConnectorPath(
clusterName,
connectName,
connectorName
)}/config`;

View file

@ -0,0 +1,55 @@
import React from 'react';
import { MemoryRouter, Route, StaticRouter } from 'react-router-dom';
import { Provider } from 'react-redux';
import { mount } from 'enzyme';
import { act } from 'react-dom/test-utils';
import configureStore from 'redux/store/configureStore';
interface TestRouterWrapperProps {
pathname: string;
urlParams: {
[key: string]: string;
};
}
export const TestRouterWrapper: React.FC<TestRouterWrapperProps> = ({
children,
pathname,
urlParams,
}) => (
<MemoryRouter
initialEntries={[
{
key: 'test',
pathname: Object.keys(urlParams).reduce(
(acc, param) => acc.replace(`:${param}`, urlParams[param]),
pathname
),
},
]}
>
<Route path={pathname}>{children}</Route>
</MemoryRouter>
);
export const containerRendersView = (
container: React.ReactElement,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
view: React.FC<any>
) => {
describe('container', () => {
const store = configureStore();
it('renders view', async () => {
let wrapper = mount(<div />);
await act(async () => {
wrapper = mount(
<Provider store={store}>
<StaticRouter>{container}</StaticRouter>
</Provider>
);
});
expect(wrapper.exists(view)).toBeTruthy();
});
});
};

View file

@ -0,0 +1,41 @@
import * as yup from 'yup';
import { AnyObject, Maybe } from 'yup/lib/types';
declare module 'yup' {
interface StringSchema<
TType extends Maybe<string> = string | undefined,
TContext extends AnyObject = AnyObject,
TOut extends TType = TType
> extends yup.BaseSchema<TType, TContext, TOut> {
isJsonObject(): StringSchema<TType, TContext>;
}
}
export const isValidJsonObject = (value?: string) => {
try {
if (!value) return false;
if (
value.indexOf('{') === 0 &&
value.lastIndexOf('}') === value.length - 1
) {
JSON.parse(value);
return true;
}
} catch {
// do nothing
}
return false;
};
const isJsonObject = () => {
return yup.string().test(
'isJsonObject',
// eslint-disable-next-line no-template-curly-in-string
'${path} is not JSON object',
isValidJsonObject
);
};
yup.addMethod(yup.string, 'isJsonObject', isJsonObject);
export default yup;

View file

@ -1,15 +1,23 @@
import fetchMock from 'fetch-mock-jest';
import { ConnectorAction } from 'generated-sources';
import * as actions from 'redux/actions/actions';
import * as thunks from 'redux/actions/thunks';
import {
connectorsPayload,
connects,
connectorsServerPayload,
connectsPayload,
connectors,
connectorServerPayload,
connector,
tasksServerPayload,
tasks,
} from 'redux/reducers/connect/__test__/fixtures';
import mockStoreCreator from 'redux/store/configureStore/mockStoreCreator';
const store = mockStoreCreator;
const clusterName = 'local';
const connectName = 'first';
const connectorName = 'hdfs-source-connector';
const taskId = 10;
describe('Thunks', () => {
afterEach(() => {
@ -19,17 +27,11 @@ describe('Thunks', () => {
describe('fetchConnects', () => {
it('creates GET_CONNECTS__SUCCESS when fetching connects', async () => {
fetchMock.getOnce(
`/api/clusters/${clusterName}/connects`,
connectsPayload
);
fetchMock.getOnce(`/api/clusters/${clusterName}/connects`, connects);
await store.dispatch(thunks.fetchConnects(clusterName));
expect(store.getActions()).toEqual([
actions.fetchConnectsAction.request(),
actions.fetchConnectsAction.success({
...store.getState().connect,
connects: connectsPayload,
}),
actions.fetchConnectsAction.success({ connects }),
]);
});
@ -41,7 +43,7 @@ describe('Thunks', () => {
actions.fetchConnectsAction.failure({
alert: {
subject: 'connects',
title: `Kafka Connect`,
title: 'Kafka Connect',
response: {
status: 404,
statusText: 'Not Found',
@ -54,7 +56,7 @@ describe('Thunks', () => {
});
describe('fetchConnectors', () => {
it('creates GET_CONNECTORS__SUCCESS when fetching connects', async () => {
it('creates GET_CONNECTORS__SUCCESS when fetching connectors', async () => {
fetchMock.getOnce(
`/api/clusters/${clusterName}/connectors`,
connectorsServerPayload
@ -62,14 +64,11 @@ describe('Thunks', () => {
await store.dispatch(thunks.fetchConnectors(clusterName));
expect(store.getActions()).toEqual([
actions.fetchConnectorsAction.request(),
actions.fetchConnectorsAction.success({
...store.getState().connect,
connectors: connectorsPayload,
}),
actions.fetchConnectorsAction.success({ connectors }),
]);
});
it('creates GET_CONNECTORS__SUCCESS when fetching connects in silent mode', async () => {
it('creates GET_CONNECTORS__SUCCESS when fetching connectors in silent mode', async () => {
fetchMock.getOnce(
`/api/clusters/${clusterName}/connectors`,
connectorsServerPayload
@ -78,7 +77,7 @@ describe('Thunks', () => {
expect(store.getActions()).toEqual([
actions.fetchConnectorsAction.success({
...store.getState().connect,
connectors: connectorsPayload,
connectors,
}),
]);
});
@ -103,10 +102,105 @@ describe('Thunks', () => {
});
});
describe('deleteConnector', () => {
const connectName = 'first';
const connectorName = 'hdfs-source-connector';
describe('fetchConnector', () => {
it('creates GET_CONNECTOR__SUCCESS when fetching connects', async () => {
fetchMock.getOnce(
`/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}`,
connectorServerPayload
);
await store.dispatch(
thunks.fetchConnector(clusterName, connectName, connectorName)
);
expect(store.getActions()).toEqual([
actions.fetchConnectorAction.request(),
actions.fetchConnectorAction.success({ connector }),
]);
});
it('creates GET_CONNECTOR__FAILURE', async () => {
fetchMock.getOnce(
`/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}`,
404
);
await store.dispatch(
thunks.fetchConnector(clusterName, connectName, connectorName)
);
expect(store.getActions()).toEqual([
actions.fetchConnectorAction.request(),
actions.fetchConnectorAction.failure({
alert: {
subject: 'local-first-hdfs-source-connector',
title: 'Kafka Connect Connector',
response: {
status: 404,
statusText: 'Not Found',
body: undefined,
},
},
}),
]);
});
});
describe('createConnector', () => {
it('creates POST_CONNECTOR__SUCCESS when fetching connects', async () => {
fetchMock.postOnce(
{
url: `/api/clusters/${clusterName}/connects/${connectName}/connectors`,
body: {
name: connectorName,
config: connector.config,
},
},
connectorServerPayload
);
await store.dispatch(
thunks.createConnector(clusterName, connectName, {
name: connectorName,
config: connector.config,
})
);
expect(store.getActions()).toEqual([
actions.createConnectorAction.request(),
actions.createConnectorAction.success({ connector }),
]);
});
it('creates POST_CONNECTOR__FAILURE', async () => {
fetchMock.postOnce(
{
url: `/api/clusters/${clusterName}/connects/${connectName}/connectors`,
body: {
name: connectorName,
config: connector.config,
},
},
404
);
await store.dispatch(
thunks.createConnector(clusterName, connectName, {
name: connectorName,
config: connector.config,
})
);
expect(store.getActions()).toEqual([
actions.createConnectorAction.request(),
actions.createConnectorAction.failure({
alert: {
subject: 'local-first',
title: 'Kafka Connect Connector Create',
response: {
status: 404,
statusText: 'Not Found',
body: undefined,
},
},
}),
]);
});
});
describe('deleteConnector', () => {
it('creates DELETE_CONNECTOR__SUCCESS', async () => {
fetchMock.deleteOnce(
`/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}`,
@ -121,9 +215,7 @@ describe('Thunks', () => {
);
expect(store.getActions()).toEqual([
actions.deleteConnectorAction.request(),
actions.deleteConnectorAction.success({
...store.getState().connect,
}),
actions.deleteConnectorAction.success({ connectorName }),
]);
});
@ -132,15 +224,328 @@ describe('Thunks', () => {
`/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}`,
404
);
try {
await store.dispatch(
thunks.deleteConnector(clusterName, connectName, connectorName)
);
} catch {
expect(store.getActions()).toEqual([
actions.deleteConnectorAction.request(),
actions.deleteConnectorAction.failure({
alert: {
subject: 'local-first-hdfs-source-connector',
title: 'Kafka Connect Connector Delete',
response: {
status: 404,
statusText: 'Not Found',
body: undefined,
},
},
}),
]);
}
});
});
describe('fetchConnectorTasks', () => {
it('creates GET_CONNECTOR_TASKS__SUCCESS when fetching connects', async () => {
fetchMock.getOnce(
`/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}/tasks`,
tasksServerPayload
);
await store.dispatch(
thunks.deleteConnector(clusterName, connectName, connectorName)
thunks.fetchConnectorTasks(clusterName, connectName, connectorName)
);
expect(store.getActions()).toEqual([
actions.deleteConnectorAction.request(),
actions.deleteConnectorAction.failure({
actions.fetchConnectorTasksAction.request(),
actions.fetchConnectorTasksAction.success({ tasks }),
]);
});
it('creates GET_CONNECTOR_TASKS__FAILURE', async () => {
fetchMock.getOnce(
`/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}/tasks`,
404
);
await store.dispatch(
thunks.fetchConnectorTasks(clusterName, connectName, connectorName)
);
expect(store.getActions()).toEqual([
actions.fetchConnectorTasksAction.request(),
actions.fetchConnectorTasksAction.failure({
alert: {
subject: 'local-first-hdfs-source-connector',
title: 'Kafka Connect Connector Delete',
title: 'Kafka Connect Connector Tasks',
response: {
status: 404,
statusText: 'Not Found',
body: undefined,
},
},
}),
]);
});
});
describe('restartConnector', () => {
it('creates RESTART_CONNECTOR__SUCCESS when fetching connects', async () => {
fetchMock.postOnce(
`/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}/action/${ConnectorAction.RESTART}`,
{ message: 'success' }
);
fetchMock.getOnce(
`/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}/tasks`,
tasksServerPayload
);
await store.dispatch(
thunks.restartConnector(clusterName, connectName, connectorName)
);
expect(store.getActions()).toEqual([
actions.restartConnectorAction.request(),
actions.restartConnectorAction.success(),
]);
});
it('creates RESTART_CONNECTOR__FAILURE', async () => {
fetchMock.postOnce(
`/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}/action/${ConnectorAction.RESTART}`,
404
);
await store.dispatch(
thunks.restartConnector(clusterName, connectName, connectorName)
);
expect(store.getActions()).toEqual([
actions.restartConnectorAction.request(),
actions.restartConnectorAction.failure({
alert: {
subject: 'local-first-hdfs-source-connector',
title: 'Kafka Connect Connector Tasks Restart',
response: {
status: 404,
statusText: 'Not Found',
body: undefined,
},
},
}),
]);
});
});
describe('pauseConnector', () => {
it('creates PAUSE_CONNECTOR__SUCCESS when fetching connects', async () => {
fetchMock.postOnce(
`/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}/action/${ConnectorAction.PAUSE}`,
{ message: 'success' }
);
await store.dispatch(
thunks.pauseConnector(clusterName, connectName, connectorName)
);
expect(store.getActions()).toEqual([
actions.pauseConnectorAction.request(),
actions.pauseConnectorAction.success({ connectorName }),
]);
});
it('creates PAUSE_CONNECTOR__FAILURE', async () => {
fetchMock.postOnce(
`/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}/action/${ConnectorAction.PAUSE}`,
404
);
await store.dispatch(
thunks.pauseConnector(clusterName, connectName, connectorName)
);
expect(store.getActions()).toEqual([
actions.pauseConnectorAction.request(),
actions.pauseConnectorAction.failure({
alert: {
subject: 'local-first-hdfs-source-connector',
title: 'Kafka Connect Connector Pause',
response: {
status: 404,
statusText: 'Not Found',
body: undefined,
},
},
}),
]);
});
});
describe('resumeConnector', () => {
it('creates RESUME_CONNECTOR__SUCCESS when fetching connects', async () => {
fetchMock.postOnce(
`/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}/action/${ConnectorAction.RESUME}`,
{ message: 'success' }
);
await store.dispatch(
thunks.resumeConnector(clusterName, connectName, connectorName)
);
expect(store.getActions()).toEqual([
actions.resumeConnectorAction.request(),
actions.resumeConnectorAction.success({ connectorName }),
]);
});
it('creates RESUME_CONNECTOR__FAILURE', async () => {
fetchMock.postOnce(
`/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}/action/${ConnectorAction.RESUME}`,
404
);
await store.dispatch(
thunks.resumeConnector(clusterName, connectName, connectorName)
);
expect(store.getActions()).toEqual([
actions.resumeConnectorAction.request(),
actions.resumeConnectorAction.failure({
alert: {
subject: 'local-first-hdfs-source-connector',
title: 'Kafka Connect Connector Resume',
response: {
status: 404,
statusText: 'Not Found',
body: undefined,
},
},
}),
]);
});
});
describe('restartConnectorTask', () => {
it('creates RESTART_CONNECTOR_TASK__SUCCESS when fetching connects', async () => {
fetchMock.postOnce(
`/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}/tasks/${taskId}/action/restart`,
{ message: 'success' }
);
fetchMock.getOnce(
`/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}/tasks`,
tasksServerPayload
);
await store.dispatch(
thunks.restartConnectorTask(
clusterName,
connectName,
connectorName,
taskId
)
);
expect(store.getActions()).toEqual([
actions.restartConnectorTaskAction.request(),
actions.restartConnectorTaskAction.success(),
]);
});
it('creates RESTART_CONNECTOR_TASK__FAILURE', async () => {
fetchMock.postOnce(
`/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}/tasks/${taskId}/action/restart`,
404
);
await store.dispatch(
thunks.restartConnectorTask(
clusterName,
connectName,
connectorName,
taskId
)
);
expect(store.getActions()).toEqual([
actions.restartConnectorTaskAction.request(),
actions.restartConnectorTaskAction.failure({
alert: {
subject: 'local-first-hdfs-source-connector-10',
title: 'Kafka Connect Connector Task Restart',
response: {
status: 404,
statusText: 'Not Found',
body: undefined,
},
},
}),
]);
});
});
describe('fetchConnectorConfig', () => {
it('creates GET_CONNECTOR_CONFIG__SUCCESS when fetching connects', async () => {
fetchMock.getOnce(
`/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}/config`,
connector.config
);
await store.dispatch(
thunks.fetchConnectorConfig(clusterName, connectName, connectorName)
);
expect(store.getActions()).toEqual([
actions.fetchConnectorConfigAction.request(),
actions.fetchConnectorConfigAction.success({
config: connector.config,
}),
]);
});
it('creates GET_CONNECTOR_CONFIG__FAILURE', async () => {
fetchMock.getOnce(
`/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}/config`,
404
);
await store.dispatch(
thunks.fetchConnectorConfig(clusterName, connectName, connectorName)
);
expect(store.getActions()).toEqual([
actions.fetchConnectorConfigAction.request(),
actions.fetchConnectorConfigAction.failure({
alert: {
subject: 'local-first-hdfs-source-connector',
title: 'Kafka Connect Connector Config',
response: {
status: 404,
statusText: 'Not Found',
body: undefined,
},
},
}),
]);
});
});
describe('updateConnectorConfig', () => {
it('creates PATCH_CONNECTOR_CONFIG__SUCCESS when fetching connects', async () => {
fetchMock.putOnce(
`/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}/config`,
connectorServerPayload
);
await store.dispatch(
thunks.updateConnectorConfig(
clusterName,
connectName,
connectorName,
connector.config
)
);
expect(store.getActions()).toEqual([
actions.updateConnectorConfigAction.request(),
actions.updateConnectorConfigAction.success({ connector }),
]);
});
it('creates PATCH_CONNECTOR_CONFIG__FAILURE', async () => {
fetchMock.putOnce(
`/api/clusters/${clusterName}/connects/${connectName}/connectors/${connectorName}/config`,
404
);
await store.dispatch(
thunks.updateConnectorConfig(
clusterName,
connectName,
connectorName,
connector.config
)
);
expect(store.getActions()).toEqual([
actions.updateConnectorConfigAction.request(),
actions.updateConnectorConfigAction.failure({
alert: {
subject: 'local-first-hdfs-source-connector',
title: 'Kafka Connect Connector Config Update',
response: {
status: 404,
statusText: 'Not Found',

View file

@ -4,7 +4,8 @@ import {
FailurePayload,
TopicName,
TopicsState,
ConnectState,
ConnectorName,
ConnectorConfig,
} from 'redux/interfaces';
import {
Cluster,
@ -17,6 +18,10 @@ import {
ConsumerGroupDetails,
SchemaSubject,
CompatibilityLevelCompatibilityEnum,
Connector,
FullConnectorInfo,
Connect,
Task,
} from 'generated-sources';
export const fetchClusterStatsAction = createAsyncAction(
@ -161,22 +166,70 @@ export const fetchConnectsAction = createAsyncAction(
'GET_CONNECTS__REQUEST',
'GET_CONNECTS__SUCCESS',
'GET_CONNECTS__FAILURE'
)<undefined, ConnectState, { alert?: FailurePayload }>();
)<undefined, { connects: Connect[] }, { alert?: FailurePayload }>();
export const fetchConnectorsAction = createAsyncAction(
'GET_CONNECTORS__REQUEST',
'GET_CONNECTORS__SUCCESS',
'GET_CONNECTORS__FAILURE'
)<undefined, ConnectState, { alert?: FailurePayload }>();
)<undefined, { connectors: FullConnectorInfo[] }, { alert?: FailurePayload }>();
export const fetchConnectorAction = createAsyncAction(
'GET_CONNECTOR__REQUEST',
'GET_CONNECTOR__SUCCESS',
'GET_CONNECTOR__FAILURE'
)<undefined, ConnectState, { alert?: FailurePayload }>();
)<undefined, { connector: Connector }, { alert?: FailurePayload }>();
export const createConnectorAction = createAsyncAction(
'POST_CONNECTOR__REQUEST',
'POST_CONNECTOR__SUCCESS',
'POST_CONNECTOR__FAILURE'
)<undefined, { connector: Connector }, { alert?: FailurePayload }>();
export const deleteConnectorAction = createAsyncAction(
'DELETE_CONNECTOR__REQUEST',
'DELETE_CONNECTOR__SUCCESS',
'DELETE_CONNECTOR__FAILURE'
)<undefined, ConnectState, { alert?: FailurePayload }>();
)<undefined, { connectorName: ConnectorName }, { alert?: FailurePayload }>();
export const restartConnectorAction = createAsyncAction(
'RESTART_CONNECTOR__REQUEST',
'RESTART_CONNECTOR__SUCCESS',
'RESTART_CONNECTOR__FAILURE'
)<undefined, undefined, { alert?: FailurePayload }>();
export const pauseConnectorAction = createAsyncAction(
'PAUSE_CONNECTOR__REQUEST',
'PAUSE_CONNECTOR__SUCCESS',
'PAUSE_CONNECTOR__FAILURE'
)<undefined, { connectorName: ConnectorName }, { alert?: FailurePayload }>();
export const resumeConnectorAction = createAsyncAction(
'RESUME_CONNECTOR__REQUEST',
'RESUME_CONNECTOR__SUCCESS',
'RESUME_CONNECTOR__FAILURE'
)<undefined, { connectorName: ConnectorName }, { alert?: FailurePayload }>();
export const fetchConnectorTasksAction = createAsyncAction(
'GET_CONNECTOR_TASKS__REQUEST',
'GET_CONNECTOR_TASKS__SUCCESS',
'GET_CONNECTOR_TASKS__FAILURE'
)<undefined, { tasks: Task[] }, { alert?: FailurePayload }>();
export const restartConnectorTaskAction = createAsyncAction(
'RESTART_CONNECTOR_TASK__REQUEST',
'RESTART_CONNECTOR_TASK__SUCCESS',
'RESTART_CONNECTOR_TASK__FAILURE'
)<undefined, undefined, { alert?: FailurePayload }>();
export const fetchConnectorConfigAction = createAsyncAction(
'GET_CONNECTOR_CONFIG__REQUEST',
'GET_CONNECTOR_CONFIG__SUCCESS',
'GET_CONNECTOR_CONFIG__FAILURE'
)<undefined, { config: ConnectorConfig }, { alert?: FailurePayload }>();
export const updateConnectorConfigAction = createAsyncAction(
'PATCH_CONNECTOR_CONFIG__REQUEST',
'PATCH_CONNECTOR_CONFIG__SUCCESS',
'PATCH_CONNECTOR_CONFIG__FAILURE'
)<undefined, { connector: Connector }, { alert?: FailurePayload }>();

View file

@ -1,7 +1,17 @@
import { KafkaConnectApi, Configuration } from 'generated-sources';
import {
KafkaConnectApi,
Configuration,
NewConnector,
Connector,
ConnectorAction,
TaskId,
} from 'generated-sources';
import { BASE_PARAMS } from 'lib/constants';
import {
ClusterName,
ConnectName,
ConnectorConfig,
ConnectorName,
FailurePayload,
PromiseThunkResult,
} from 'redux/interfaces';
@ -11,77 +21,302 @@ import { getResponse } from 'lib/errorHandling';
const apiClientConf = new Configuration(BASE_PARAMS);
export const kafkaConnectApiClient = new KafkaConnectApi(apiClientConf);
export const fetchConnects =
(clusterName: ClusterName): PromiseThunkResult<void> =>
async (dispatch, getState) => {
dispatch(actions.fetchConnectsAction.request());
try {
const connects = await kafkaConnectApiClient.getConnects({ clusterName });
const state = getState().connect;
dispatch(actions.fetchConnectsAction.success({ ...state, connects }));
} catch (error) {
const response = await getResponse(error);
const alert: FailurePayload = {
subject: 'connects',
title: `Kafka Connect`,
response,
};
dispatch(actions.fetchConnectsAction.failure({ alert }));
}
};
export const fetchConnects = (
clusterName: ClusterName
): PromiseThunkResult<void> => async (dispatch) => {
dispatch(actions.fetchConnectsAction.request());
try {
const connects = await kafkaConnectApiClient.getConnects({ clusterName });
dispatch(actions.fetchConnectsAction.success({ connects }));
} catch (error) {
const response = await getResponse(error);
const alert: FailurePayload = {
subject: 'connects',
title: `Kafka Connect`,
response,
};
dispatch(actions.fetchConnectsAction.failure({ alert }));
}
};
export const fetchConnectors =
(clusterName: ClusterName, silent = false): PromiseThunkResult<void> =>
async (dispatch, getState) => {
if (!silent) dispatch(actions.fetchConnectorsAction.request());
try {
const connectors = await kafkaConnectApiClient.getAllConnectors({
clusterName,
});
const state = getState().connect;
dispatch(actions.fetchConnectorsAction.success({ ...state, connectors }));
} catch (error) {
const response = await getResponse(error);
const alert: FailurePayload = {
subject: [clusterName, 'connectors'].join('-'),
title: `Kafka Connect Connectors`,
response,
};
dispatch(actions.fetchConnectorsAction.failure({ alert }));
}
};
export const fetchConnectors = (
clusterName: ClusterName,
silent = false
): PromiseThunkResult<void> => async (dispatch) => {
if (!silent) dispatch(actions.fetchConnectorsAction.request());
try {
const connectors = await kafkaConnectApiClient.getAllConnectors({
clusterName,
});
dispatch(actions.fetchConnectorsAction.success({ connectors }));
} catch (error) {
const response = await getResponse(error);
const alert: FailurePayload = {
subject: [clusterName, 'connectors'].join('-'),
title: `Kafka Connect Connectors`,
response,
};
dispatch(actions.fetchConnectorsAction.failure({ alert }));
}
};
export const deleteConnector =
(
clusterName: ClusterName,
connectName: string,
connectorName: string
): PromiseThunkResult<void> =>
async (dispatch, getState) => {
dispatch(actions.deleteConnectorAction.request());
try {
await kafkaConnectApiClient.deleteConnector({
clusterName,
connectName,
connectorName,
});
const state = getState().connect;
dispatch(
actions.deleteConnectorAction.success({
...state,
connectors: state?.connectors.filter(
({ name }) => name !== connectorName
),
})
);
dispatch(fetchConnectors(clusterName, true));
} catch (error) {
const response = await getResponse(error);
const alert: FailurePayload = {
subject: [clusterName, connectName, connectorName].join('-'),
title: `Kafka Connect Connector Delete`,
response,
};
dispatch(actions.deleteConnectorAction.failure({ alert }));
}
};
export const fetchConnector = (
clusterName: ClusterName,
connectName: ConnectName,
connectorName: ConnectorName
): PromiseThunkResult<void> => async (dispatch) => {
dispatch(actions.fetchConnectorAction.request());
try {
const connector = await kafkaConnectApiClient.getConnector({
clusterName,
connectName,
connectorName,
});
dispatch(actions.fetchConnectorAction.success({ connector }));
} catch (error) {
const response = await getResponse(error);
const alert: FailurePayload = {
subject: [clusterName, connectName, connectorName].join('-'),
title: `Kafka Connect Connector`,
response,
};
dispatch(actions.fetchConnectorAction.failure({ alert }));
}
};
export const createConnector = (
clusterName: ClusterName,
connectName: ConnectName,
newConnector: NewConnector
): PromiseThunkResult<Connector | undefined> => async (dispatch) => {
dispatch(actions.createConnectorAction.request());
try {
const connector = await kafkaConnectApiClient.createConnector({
clusterName,
connectName,
newConnector,
});
dispatch(actions.createConnectorAction.success({ connector }));
return connector;
} catch (error) {
const response = await getResponse(error);
const alert: FailurePayload = {
subject: [clusterName, connectName].join('-'),
title: `Kafka Connect Connector Create`,
response,
};
dispatch(actions.createConnectorAction.failure({ alert }));
}
return undefined;
};
export const deleteConnector = (
clusterName: ClusterName,
connectName: ConnectName,
connectorName: ConnectorName
): PromiseThunkResult<void> => async (dispatch) => {
dispatch(actions.deleteConnectorAction.request());
try {
await kafkaConnectApiClient.deleteConnector({
clusterName,
connectName,
connectorName,
});
dispatch(actions.deleteConnectorAction.success({ connectorName }));
dispatch(fetchConnectors(clusterName, true));
} catch (error) {
const response = await getResponse(error);
const alert: FailurePayload = {
subject: [clusterName, connectName, connectorName].join('-'),
title: `Kafka Connect Connector Delete`,
response,
};
dispatch(actions.deleteConnectorAction.failure({ alert }));
throw error;
}
};
export const fetchConnectorTasks = (
clusterName: ClusterName,
connectName: ConnectName,
connectorName: ConnectorName,
silent = false
): PromiseThunkResult<void> => async (dispatch) => {
if (!silent) dispatch(actions.fetchConnectorTasksAction.request());
try {
const tasks = await kafkaConnectApiClient.getConnectorTasks({
clusterName,
connectName,
connectorName,
});
dispatch(actions.fetchConnectorTasksAction.success({ tasks }));
} catch (error) {
const response = await getResponse(error);
const alert: FailurePayload = {
subject: [clusterName, connectName, connectorName].join('-'),
title: `Kafka Connect Connector Tasks`,
response,
};
dispatch(actions.fetchConnectorTasksAction.failure({ alert }));
}
};
export const restartConnector = (
clusterName: ClusterName,
connectName: ConnectName,
connectorName: ConnectorName
): PromiseThunkResult<void> => async (dispatch) => {
dispatch(actions.restartConnectorAction.request());
try {
await kafkaConnectApiClient.updateConnectorState({
clusterName,
connectName,
connectorName,
action: ConnectorAction.RESTART,
});
dispatch(actions.restartConnectorAction.success());
dispatch(
fetchConnectorTasks(clusterName, connectName, connectorName, true)
);
} catch (error) {
const response = await getResponse(error);
const alert: FailurePayload = {
subject: [clusterName, connectName, connectorName].join('-'),
title: `Kafka Connect Connector Tasks Restart`,
response,
};
dispatch(actions.restartConnectorAction.failure({ alert }));
}
};
export const pauseConnector = (
clusterName: ClusterName,
connectName: ConnectName,
connectorName: ConnectorName
): PromiseThunkResult<void> => async (dispatch) => {
dispatch(actions.pauseConnectorAction.request());
try {
await kafkaConnectApiClient.updateConnectorState({
clusterName,
connectName,
connectorName,
action: ConnectorAction.PAUSE,
});
dispatch(actions.pauseConnectorAction.success({ connectorName }));
} catch (error) {
const response = await getResponse(error);
const alert: FailurePayload = {
subject: [clusterName, connectName, connectorName].join('-'),
title: `Kafka Connect Connector Pause`,
response,
};
dispatch(actions.pauseConnectorAction.failure({ alert }));
}
};
export const resumeConnector = (
clusterName: ClusterName,
connectName: ConnectName,
connectorName: ConnectorName
): PromiseThunkResult<void> => async (dispatch) => {
dispatch(actions.resumeConnectorAction.request());
try {
await kafkaConnectApiClient.updateConnectorState({
clusterName,
connectName,
connectorName,
action: ConnectorAction.RESUME,
});
dispatch(actions.resumeConnectorAction.success({ connectorName }));
} catch (error) {
const response = await getResponse(error);
const alert: FailurePayload = {
subject: [clusterName, connectName, connectorName].join('-'),
title: `Kafka Connect Connector Resume`,
response,
};
dispatch(actions.resumeConnectorAction.failure({ alert }));
}
};
export const restartConnectorTask = (
clusterName: ClusterName,
connectName: ConnectName,
connectorName: ConnectorName,
taskId: TaskId['task']
): PromiseThunkResult<void> => async (dispatch) => {
dispatch(actions.restartConnectorTaskAction.request());
try {
await kafkaConnectApiClient.restartConnectorTask({
clusterName,
connectName,
connectorName,
taskId: Number(taskId),
});
dispatch(actions.restartConnectorTaskAction.success());
dispatch(
fetchConnectorTasks(clusterName, connectName, connectorName, true)
);
} catch (error) {
const response = await getResponse(error);
const alert: FailurePayload = {
subject: [clusterName, connectName, connectorName, taskId].join('-'),
title: `Kafka Connect Connector Task Restart`,
response,
};
dispatch(actions.restartConnectorTaskAction.failure({ alert }));
}
};
export const fetchConnectorConfig = (
clusterName: ClusterName,
connectName: ConnectName,
connectorName: ConnectorName,
silent = false
): PromiseThunkResult<void> => async (dispatch) => {
if (!silent) dispatch(actions.fetchConnectorConfigAction.request());
try {
const config = await kafkaConnectApiClient.getConnectorConfig({
clusterName,
connectName,
connectorName,
});
dispatch(actions.fetchConnectorConfigAction.success({ config }));
} catch (error) {
const response = await getResponse(error);
const alert: FailurePayload = {
subject: [clusterName, connectName, connectorName].join('-'),
title: `Kafka Connect Connector Config`,
response,
};
dispatch(actions.fetchConnectorConfigAction.failure({ alert }));
}
};
export const updateConnectorConfig = (
clusterName: ClusterName,
connectName: ConnectName,
connectorName: ConnectorName,
connectorConfig: ConnectorConfig
): PromiseThunkResult<Connector | undefined> => async (dispatch) => {
dispatch(actions.updateConnectorConfigAction.request());
try {
const connector = await kafkaConnectApiClient.setConnectorConfig({
clusterName,
connectName,
connectorName,
requestBody: connectorConfig,
});
dispatch(actions.updateConnectorConfigAction.success({ connector }));
return connector;
} catch (error) {
const response = await getResponse(error);
const alert: FailurePayload = {
subject: [clusterName, connectName, connectorName].join('-'),
title: `Kafka Connect Connector Config Update`,
response,
};
dispatch(actions.updateConnectorConfigAction.failure({ alert }));
}
return undefined;
};

View file

@ -1,6 +1,15 @@
import { Connect, FullConnectorInfo } from 'generated-sources';
import { Connect, Connector, FullConnectorInfo, Task } from 'generated-sources';
export type ConnectName = Connect['name'];
export type ConnectorName = Connector['name'];
export type ConnectorConfig = Connector['config'];
export interface ConnectState {
connects: Connect[];
connectors: FullConnectorInfo[];
currentConnector: {
connector: Connector | null;
tasks: Task[];
config: ConnectorConfig | null;
};
}

View file

@ -1,10 +1,13 @@
import {
Connect,
Connector,
ConnectorTaskStatus,
ConnectorType,
FullConnectorInfo,
Task,
} from 'generated-sources';
export const connectsPayload = [
export const connects: Connect[] = [
{ name: 'first', address: 'localhost:8083' },
{ name: 'second', address: 'localhost:8084' },
];
@ -16,7 +19,10 @@ export const connectorsServerPayload = [
connector_class: 'FileStreamSource',
type: ConnectorType.SOURCE,
topics: ['test-topic'],
status: ConnectorTaskStatus.RUNNING,
status: {
state: ConnectorTaskStatus.RUNNING,
workerId: 1,
},
tasks_count: 2,
failed_tasks_count: 0,
},
@ -26,20 +32,25 @@ export const connectorsServerPayload = [
connector_class: 'FileStreamSource',
type: ConnectorType.SINK,
topics: ['test-topic'],
status: ConnectorTaskStatus.FAILED,
status: {
state: ConnectorTaskStatus.FAILED,
workerId: 1,
},
tasks_count: 3,
failed_tasks_count: 1,
},
];
export const connectorsPayload: FullConnectorInfo[] = [
export const connectors: FullConnectorInfo[] = [
{
connect: 'first',
name: 'hdfs-source-connector',
connectorClass: 'FileStreamSource',
type: ConnectorType.SOURCE,
topics: ['test-topic'],
status: ConnectorTaskStatus.RUNNING,
status: {
state: ConnectorTaskStatus.RUNNING,
},
tasksCount: 2,
failedTasksCount: 0,
},
@ -49,8 +60,136 @@ export const connectorsPayload: FullConnectorInfo[] = [
connectorClass: 'FileStreamSource',
type: ConnectorType.SINK,
topics: ['test-topic'],
status: ConnectorTaskStatus.FAILED,
status: {
state: ConnectorTaskStatus.FAILED,
},
tasksCount: 3,
failedTasksCount: 1,
},
];
export const connectorServerPayload = {
connect: 'first',
name: 'hdfs-source-connector',
type: ConnectorType.SOURCE,
status: {
state: ConnectorTaskStatus.RUNNING,
worker_id: 'kafka-connect0:8083',
},
config: {
'connector.class': 'FileStreamSource',
'tasks.max': '10',
topic: 'test-topic',
file: '/some/file',
},
tasks: [{ connector: 'first', task: 1 }],
};
export const connector: Connector = {
connect: 'first',
name: 'hdfs-source-connector',
type: ConnectorType.SOURCE,
status: {
state: ConnectorTaskStatus.RUNNING,
workerId: 'kafka-connect0:8083',
},
config: {
'connector.class': 'FileStreamSource',
'tasks.max': '10',
topic: 'test-topic',
file: '/some/file',
},
tasks: [{ connector: 'first', task: 1 }],
};
export const tasksServerPayload = [
{
id: { connector: 'first', task: 1 },
status: {
id: 1,
state: ConnectorTaskStatus.RUNNING,
worker_id: 'kafka-connect0:8083',
},
config: {
'batch.size': '2000',
file: '/some/file',
'task.class': 'org.apache.kafka.connect.file.FileStreamSourceTask',
topic: 'test-topic',
},
},
{
id: { connector: 'first', task: 2 },
status: {
id: 2,
state: ConnectorTaskStatus.FAILED,
trace: 'Failure 1',
worker_id: 'kafka-connect0:8083',
},
config: {
'batch.size': '1000',
file: '/some/file2',
'task.class': 'org.apache.kafka.connect.file.FileStreamSourceTask',
topic: 'test-topic',
},
},
{
id: { connector: 'first', task: 3 },
status: {
id: 3,
state: ConnectorTaskStatus.RUNNING,
worker_id: 'kafka-connect0:8083',
},
config: {
'batch.size': '3000',
file: '/some/file3',
'task.class': 'org.apache.kafka.connect.file.FileStreamSourceTask',
topic: 'test-topic',
},
},
];
export const tasks: Task[] = [
{
id: { connector: 'first', task: 1 },
status: {
id: 1,
state: ConnectorTaskStatus.RUNNING,
workerId: 'kafka-connect0:8083',
},
config: {
'batch.size': '2000',
file: '/some/file',
'task.class': 'org.apache.kafka.connect.file.FileStreamSourceTask',
topic: 'test-topic',
},
},
{
id: { connector: 'first', task: 2 },
status: {
id: 2,
state: ConnectorTaskStatus.FAILED,
trace: 'Failure 1',
workerId: 'kafka-connect0:8083',
},
config: {
'batch.size': '1000',
file: '/some/file2',
'task.class': 'org.apache.kafka.connect.file.FileStreamSourceTask',
topic: 'test-topic',
},
},
{
id: { connector: 'first', task: 3 },
status: {
id: 3,
state: ConnectorTaskStatus.RUNNING,
workerId: 'kafka-connect0:8083',
},
config: {
'batch.size': '3000',
file: '/some/file3',
'task.class': 'org.apache.kafka.connect.file.FileStreamSourceTask',
topic: 'test-topic',
},
},
];

View file

@ -1,24 +1,229 @@
import { ConnectorTaskStatus } from 'generated-sources';
import {
fetchConnectorsAction,
fetchConnectorAction,
fetchConnectsAction,
fetchConnectorTasksAction,
fetchConnectorConfigAction,
createConnectorAction,
deleteConnectorAction,
pauseConnectorAction,
resumeConnectorAction,
updateConnectorConfigAction,
} from 'redux/actions';
import reducer, { initialState } from 'redux/reducers/connect/reducer';
import { connects, connectors, connector, tasks } from './fixtures';
const runningConnectorState = {
...initialState,
currentConnector: {
...initialState.currentConnector,
connector: {
...connector,
status: {
...connector.status,
state: ConnectorTaskStatus.RUNNING,
},
},
tasks: tasks.map((task) => ({
...task,
status: {
...task.status,
state: ConnectorTaskStatus.RUNNING,
},
})),
},
};
const pausedConnectorState = {
...initialState,
currentConnector: {
...initialState.currentConnector,
connector: {
...connector,
status: {
...connector.status,
state: ConnectorTaskStatus.PAUSED,
},
},
tasks: tasks.map((task) => ({
...task,
status: {
...task.status,
state: ConnectorTaskStatus.PAUSED,
},
})),
},
};
describe('Clusters reducer', () => {
it('reacts on GET_CONNECTS__SUCCESS and returns payload', () => {
it('reacts on GET_CONNECTS__SUCCESS', () => {
expect(
reducer(undefined, fetchConnectsAction.success(initialState))
).toEqual(initialState);
reducer(initialState, fetchConnectsAction.success({ connects }))
).toEqual({
...initialState,
connects,
});
});
it('reacts on GET_CONNECTORS__SUCCESS and returns payload', () => {
it('reacts on GET_CONNECTORS__SUCCESS', () => {
expect(
reducer(undefined, fetchConnectorsAction.success(initialState))
).toEqual(initialState);
reducer(initialState, fetchConnectorsAction.success({ connectors }))
).toEqual({
...initialState,
connectors,
});
});
it('reacts on GET_CONNECTOR__SUCCESS and returns payload', () => {
it('reacts on GET_CONNECTOR__SUCCESS', () => {
expect(
reducer(undefined, fetchConnectorAction.success(initialState))
).toEqual(initialState);
reducer(initialState, fetchConnectorAction.success({ connector }))
).toEqual({
...initialState,
currentConnector: {
...initialState.currentConnector,
connector,
},
});
});
it('reacts on POST_CONNECTOR__SUCCESS', () => {
expect(
reducer(initialState, createConnectorAction.success({ connector }))
).toEqual({
...initialState,
currentConnector: {
...initialState.currentConnector,
connector,
},
});
});
it('reacts on DELETE_CONNECTOR__SUCCESS', () => {
expect(
reducer(
{
...initialState,
connectors,
},
deleteConnectorAction.success({ connectorName: connectors[0].name })
)
).toEqual({
...initialState,
connectors: connectors.slice(1),
});
});
it('reacts on PAUSE_CONNECTOR__SUCCESS', () => {
expect(
reducer(
runningConnectorState,
pauseConnectorAction.success({ connectorName: connector.name })
)
).toEqual(pausedConnectorState);
});
it('reacts on PAUSE_CONNECTOR__SUCCESS when current connector is null', () => {
expect(
reducer(
{
...initialState,
currentConnector: {
...initialState.currentConnector,
connector: null,
},
},
pauseConnectorAction.success({ connectorName: connector.name })
)
).toEqual({
...initialState,
currentConnector: {
...initialState.currentConnector,
connector: null,
},
});
});
it('reacts on RESUME_CONNECTOR__SUCCESS', () => {
expect(
reducer(
pausedConnectorState,
resumeConnectorAction.success({ connectorName: connector.name })
)
).toEqual(runningConnectorState);
});
it('reacts on RESUME_CONNECTOR__SUCCESS when current connector is null', () => {
expect(
reducer(
{
...initialState,
currentConnector: {
...initialState.currentConnector,
connector: null,
},
},
resumeConnectorAction.success({ connectorName: connector.name })
)
).toEqual({
...initialState,
currentConnector: {
...initialState.currentConnector,
connector: null,
},
});
});
it('reacts on GET_CONNECTOR_TASKS__SUCCESS', () => {
expect(
reducer(initialState, fetchConnectorTasksAction.success({ tasks }))
).toEqual({
...initialState,
currentConnector: {
...initialState.currentConnector,
tasks,
},
});
});
it('reacts on GET_CONNECTOR_CONFIG__SUCCESS', () => {
expect(
reducer(
initialState,
fetchConnectorConfigAction.success({ config: connector.config })
)
).toEqual({
...initialState,
currentConnector: {
...initialState.currentConnector,
config: connector.config,
},
});
});
it('reacts on PATCH_CONNECTOR_CONFIG__SUCCESS', () => {
expect(
reducer(
{
...initialState,
currentConnector: {
...initialState.currentConnector,
config: {
...connector.config,
fieldToRemove: 'Fake',
},
},
},
updateConnectorConfigAction.success({ connector })
)
).toEqual({
...initialState,
currentConnector: {
...initialState.currentConnector,
connector,
config: connector.config,
},
});
});
});

View file

@ -0,0 +1,92 @@
import {
fetchConnectorAction,
fetchConnectorConfigAction,
fetchConnectorsAction,
fetchConnectorTasksAction,
fetchConnectsAction,
} from 'redux/actions';
import configureStore from 'redux/store/configureStore';
import * as selectors from 'redux/reducers/connect/selectors';
import { connects, connectors, connector, tasks } from './fixtures';
const store = configureStore();
describe('Connect selectors', () => {
describe('Initial State', () => {
it('returns initial values', () => {
expect(selectors.getAreConnectsFetching(store.getState())).toEqual(false);
expect(selectors.getConnects(store.getState())).toEqual([]);
expect(selectors.getAreConnectorsFetching(store.getState())).toEqual(
false
);
expect(selectors.getConnectors(store.getState())).toEqual([]);
expect(selectors.getIsConnectorFetching(store.getState())).toEqual(false);
expect(selectors.getConnector(store.getState())).toEqual(null);
expect(selectors.getConnectorStatus(store.getState())).toEqual(undefined);
expect(selectors.getIsConnectorDeleting(store.getState())).toEqual(false);
expect(selectors.getIsConnectorRestarting(store.getState())).toEqual(
false
);
expect(selectors.getIsConnectorPausing(store.getState())).toEqual(false);
expect(selectors.getIsConnectorResuming(store.getState())).toEqual(false);
expect(selectors.getIsConnectorActionRunning(store.getState())).toEqual(
false
);
expect(selectors.getAreConnectorTasksFetching(store.getState())).toEqual(
false
);
expect(selectors.getConnectorTasks(store.getState())).toEqual([]);
expect(selectors.getConnectorRunningTasksCount(store.getState())).toEqual(
0
);
expect(selectors.getConnectorFailedTasksCount(store.getState())).toEqual(
0
);
expect(selectors.getIsConnectorConfigFetching(store.getState())).toEqual(
false
);
expect(selectors.getConnectorConfig(store.getState())).toEqual(null);
});
});
describe('state', () => {
it('returns connects', () => {
store.dispatch(fetchConnectsAction.success({ connects }));
expect(selectors.getConnects(store.getState())).toEqual(connects);
});
it('returns connectors', () => {
store.dispatch(fetchConnectorsAction.success({ connectors }));
expect(selectors.getConnectors(store.getState())).toEqual(connectors);
});
it('returns connector', () => {
store.dispatch(fetchConnectorAction.success({ connector }));
expect(selectors.getConnector(store.getState())).toEqual(connector);
expect(selectors.getConnectorStatus(store.getState())).toEqual(
connector.status.state
);
});
it('returns connector tasks', () => {
store.dispatch(fetchConnectorTasksAction.success({ tasks }));
expect(selectors.getConnectorTasks(store.getState())).toEqual(tasks);
expect(selectors.getConnectorRunningTasksCount(store.getState())).toEqual(
2
);
expect(selectors.getConnectorFailedTasksCount(store.getState())).toEqual(
1
);
});
it('returns connector config', () => {
store.dispatch(
fetchConnectorConfigAction.success({ config: connector.config })
);
expect(selectors.getConnectorConfig(store.getState())).toEqual(
connector.config
);
});
});
});

View file

@ -2,19 +2,117 @@ import { getType } from 'typesafe-actions';
import * as actions from 'redux/actions';
import { ConnectState } from 'redux/interfaces/connect';
import { Action } from 'redux/interfaces';
import { ConnectorTaskStatus } from 'generated-sources';
export const initialState: ConnectState = {
connects: [],
connectors: [],
currentConnector: {
connector: null,
tasks: [],
config: null,
},
};
const reducer = (state = initialState, action: Action): ConnectState => {
switch (action.type) {
case getType(actions.fetchConnectsAction.success):
case getType(actions.fetchConnectorAction.success):
return {
...state,
connects: action.payload.connects,
};
case getType(actions.fetchConnectorsAction.success):
return {
...state,
connectors: action.payload.connectors,
};
case getType(actions.fetchConnectorAction.success):
case getType(actions.createConnectorAction.success):
return {
...state,
currentConnector: {
...state.currentConnector,
connector: action.payload.connector,
},
};
case getType(actions.deleteConnectorAction.success):
return action.payload;
return {
...state,
connectors: state?.connectors.filter(
({ name }) => name !== action.payload.connectorName
),
};
case getType(actions.pauseConnectorAction.success):
return {
...state,
currentConnector: {
...state.currentConnector,
connector: state.currentConnector.connector
? {
...state.currentConnector.connector,
status: {
...state.currentConnector.connector?.status,
state: ConnectorTaskStatus.PAUSED,
},
}
: null,
tasks: state.currentConnector.tasks.map((task) => ({
...task,
status: {
...task.status,
state: ConnectorTaskStatus.PAUSED,
},
})),
},
};
case getType(actions.resumeConnectorAction.success):
return {
...state,
currentConnector: {
...state.currentConnector,
connector: state.currentConnector.connector
? {
...state.currentConnector.connector,
status: {
...state.currentConnector.connector?.status,
state: ConnectorTaskStatus.RUNNING,
},
}
: null,
tasks: state.currentConnector.tasks.map((task) => ({
...task,
status: {
...task.status,
state: ConnectorTaskStatus.RUNNING,
},
})),
},
};
case getType(actions.fetchConnectorTasksAction.success):
return {
...state,
currentConnector: {
...state.currentConnector,
tasks: action.payload.tasks,
},
};
case getType(actions.fetchConnectorConfigAction.success):
return {
...state,
currentConnector: {
...state.currentConnector,
config: action.payload.config,
},
};
case getType(actions.updateConnectorConfigAction.success):
return {
...state,
currentConnector: {
...state.currentConnector,
connector: action.payload.connector,
config: action.payload.connector.config,
},
};
default:
return state;
}

View file

@ -1,27 +1,14 @@
import { createSelector } from 'reselect';
import { ConnectState, RootState } from 'redux/interfaces';
import { createFetchingSelector } from 'redux/reducers/loader/selectors';
import { ConnectorTaskStatus } from 'generated-sources';
const connectState = ({ connect }: RootState): ConnectState => connect;
const getConnectorsFetchingStatus = createFetchingSelector('GET_CONNECTORS');
export const getAreConnectorsFetching = createSelector(
getConnectorsFetchingStatus,
(status) => status === 'fetching'
);
export const getAreConnectorsFetched = createSelector(
getConnectorsFetchingStatus,
(status) => status === 'fetched'
);
const getConnectsFetchingStatus = createFetchingSelector('GET_CONNECTS');
export const getAreConnectsFetching = createSelector(
getConnectsFetchingStatus,
(status) => status === 'fetching' || status === 'notFetched'
);
export const getAreConnectsFetched = createSelector(
getConnectsFetchingStatus,
(status) => status === 'fetched'
(status) => status === 'fetching'
);
export const getConnects = createSelector(
@ -29,7 +16,107 @@ export const getConnects = createSelector(
({ connects }) => connects
);
const getConnectorsFetchingStatus = createFetchingSelector('GET_CONNECTORS');
export const getAreConnectorsFetching = createSelector(
getConnectorsFetchingStatus,
(status) => status === 'fetching'
);
export const getConnectors = createSelector(
connectState,
({ connectors }) => connectors
);
const getConnectorFetchingStatus = createFetchingSelector('GET_CONNECTOR');
export const getIsConnectorFetching = createSelector(
getConnectorFetchingStatus,
(status) => status === 'fetching'
);
const getCurrentConnector = createSelector(
connectState,
({ currentConnector }) => currentConnector
);
export const getConnector = createSelector(
getCurrentConnector,
({ connector }) => connector
);
export const getConnectorStatus = createSelector(
getConnector,
(connector) => connector?.status?.state
);
const getConnectorDeletingStatus = createFetchingSelector('DELETE_CONNECTOR');
export const getIsConnectorDeleting = createSelector(
getConnectorDeletingStatus,
(status) => status === 'fetching'
);
const getConnectorRestartingStatus = createFetchingSelector(
'RESTART_CONNECTOR'
);
export const getIsConnectorRestarting = createSelector(
getConnectorRestartingStatus,
(status) => status === 'fetching'
);
const getConnectorPausingStatus = createFetchingSelector('PAUSE_CONNECTOR');
export const getIsConnectorPausing = createSelector(
getConnectorPausingStatus,
(status) => status === 'fetching'
);
const getConnectorResumingStatus = createFetchingSelector('RESUME_CONNECTOR');
export const getIsConnectorResuming = createSelector(
getConnectorResumingStatus,
(status) => status === 'fetching'
);
export const getIsConnectorActionRunning = createSelector(
getIsConnectorRestarting,
getIsConnectorPausing,
getIsConnectorResuming,
(restarting, pausing, resuming) => restarting || pausing || resuming
);
const getConnectorTasksFetchingStatus = createFetchingSelector(
'GET_CONNECTOR_TASKS'
);
export const getAreConnectorTasksFetching = createSelector(
getConnectorTasksFetchingStatus,
(status) => status === 'fetching'
);
export const getConnectorTasks = createSelector(
getCurrentConnector,
({ tasks }) => tasks
);
export const getConnectorRunningTasksCount = createSelector(
getConnectorTasks,
(tasks) =>
tasks.filter((task) => task.status?.state === ConnectorTaskStatus.RUNNING)
.length
);
export const getConnectorFailedTasksCount = createSelector(
getConnectorTasks,
(tasks) =>
tasks.filter((task) => task.status?.state === ConnectorTaskStatus.FAILED)
.length
);
const getConnectorConfigFetchingStatus = createFetchingSelector(
'GET_CONNECTOR_CONFIG'
);
export const getIsConnectorConfigFetching = createSelector(
getConnectorConfigFetchingStatus,
(status) => status === 'fetching'
);
export const getConnectorConfig = createSelector(
getCurrentConnector,
({ config }) => config
);

3
package-lock.json generated Normal file
View file

@ -0,0 +1,3 @@
{
"lockfileVersion": 1
}