Merge pull request #403 from provectus/feature/connect-views
Add connect views
This commit is contained in:
commit
8b3b51fa31
72 changed files with 5365 additions and 277 deletions
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
217
kafka-ui-react-app/package-lock.json
generated
217
kafka-ui-react-app/package-lock.json
generated
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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')}
|
||||
|
|
|
@ -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;
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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>
|
||||
`;
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
|
@ -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)
|
||||
);
|
|
@ -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
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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>
|
||||
`;
|
|
@ -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;
|
|
@ -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));
|
|
@ -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
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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 />`;
|
141
kafka-ui-react-app/src/components/Connect/Details/Details.tsx
Normal file
141
kafka-ui-react-app/src/components/Connect/Details/Details.tsx
Normal 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;
|
|
@ -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)
|
||||
);
|
|
@ -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;
|
|
@ -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));
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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>
|
||||
`;
|
|
@ -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;
|
|
@ -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)
|
||||
);
|
|
@ -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
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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>
|
||||
`;
|
|
@ -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;
|
|
@ -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));
|
|
@ -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
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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>
|
||||
`;
|
|
@ -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
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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 />`;
|
142
kafka-ui-react-app/src/components/Connect/Edit/Edit.tsx
Normal file
142
kafka-ui-react-app/src/components/Connect/Edit/Edit.tsx
Normal 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;
|
|
@ -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));
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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 />`;
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
|
|
179
kafka-ui-react-app/src/components/Connect/New/New.tsx
Normal file
179
kafka-ui-react-app/src/components/Connect/New/New.tsx
Normal 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;
|
|
@ -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));
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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 />`;
|
|
@ -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,
|
||||
});
|
||||
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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>
|
||||
`;
|
|
@ -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>
|
||||
`;
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
`;
|
||||
|
|
24
kafka-ui-react-app/src/lib/__test__/yupExtended.spec.ts
Normal file
24
kafka-ui-react-app/src/lib/__test__/yupExtended.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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`;
|
||||
|
|
55
kafka-ui-react-app/src/lib/testHelpers.tsx
Normal file
55
kafka-ui-react-app/src/lib/testHelpers.tsx
Normal 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();
|
||||
});
|
||||
});
|
||||
};
|
41
kafka-ui-react-app/src/lib/yupExtended.ts
Normal file
41
kafka-ui-react-app/src/lib/yupExtended.ts
Normal 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;
|
|
@ -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',
|
||||
|
|
|
@ -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 }>();
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
3
package-lock.json
generated
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"lockfileVersion": 1
|
||||
}
|
Loading…
Add table
Reference in a new issue