Redesign (#1045)
* Refactor topic creation
* Remove unused thunk
* Remove excess interface
* Add New page snapshot test
* Refactor new component tests
* Remove excess function
* Add typography variables and classes
* Add font families
* Implement custom button
* Get rid of enums
* Add theme
* Separate styles from logic
* Feature/layout redesign (#862)
* Refactor pages general layout
* Refactor breadcrumbs
* Refactor brokers metrics
* Fix toggle position
Co-authored-by: azat.belgibayev <azat.belgibayev@almatech.dev>
* add redesigned new menu item
* remove styles from theme
* update tests
* fix local and app wide styles
* add tests
* Add theme
* Add types to the styles
* update menu item prop prefixes, minor fixes
* add theme styles, move interface, update test, snapshot
* add optional styling
* add isActive props, propagate component, update tests
* remove button
* Revert "remove button"
This reverts commit 4a9c87d8d8
.
* add tests for styled button
* remove ternary operator from style
* import styled from lib/
* Custom Inputs (#890)
* Implement and test custom input
* Custom select (#896)
* Implement custom select
* Fix Metrics component (#914)
* Add styled table header cell component (#901)
* Redesign menu (#918)
* Finish styling menu
* Styled Table
* Fix styled table
* Allow custom buttons work as links
* Restyle Breadcrumb
* Topics list (#946)
* Redesign pagination
* Fix styled components usage
* Topic messages (#959)
* Topic Consumer Groups
* Message settings
* Finish styling indicators
* Style the dashboard
* Finish with the topics page
* Style consumer groups list
* Restyle the consumer group details
* Style alerts
* Style confirmation modal
* Update DangerZone.spec.tsx
* redesign schema registry
* Add Topic details snapshot
* Style Page Loader
* Style connectors list
* Style KSQL
* Remove all the classes from the styled components (#1049)
* Redesign topic form (#1051)
* Redesign connect details (#1053)
* Update types for styled-components (#1054)
* Redesign some minor forms in the app (#1062)
* Fix alert styles
* Get rid of bulma/layout styles
* Fix form styling
* Custom Switch component
* fix border-radius property of metrick widgets
* get rid of warnings in tests
* use jest-styled-components
* cleanup
* get rid of some bulma modules
* refactor metrics component
* get rid of JSON-tree. Json Editor redesign
* update proxy config
* Refactor Alerts component (#1124)
* Refactor tests (#1129)
* App layout update (#1127)
* ‘App-layout-update’
* toBeNull changed to toBeInDocument
* scss file removed
* App navbar layout update
* navbar test
* code smells local refactoring
* StyledMenuItem code smells refactoring
* StyledClusterTab code smells refactoring
* ConfirmationModalWrapper code smells refactoring
* input icon and label code smells refactoring
* navburger displaying fixed
* Get rid of classes
* fix code smells
* refactor styles
* refactor styles
Co-authored-by: Oleg Shuralev <workshur@gmail.com>
* Refactored Cluster nav (#1147)
* Update caniuse
* refactor Nav component
* Update sonars config
* refactor Nav component + specs
* Specs
* Feature/code smells removing (#1148)
* StyledSelect code smell refactoring
* SecondaryTabs code smell refactoring
* TextareaStyled code smell refactoring
* TableStyled code smell refactoring
* StyledTableHeaderCell code smell refactoring
* Add custom render with theme provider wrapper for testing lib (#1152)
* Added cleanupPolicy to topic details. Closes #999 (#1067)
* Rename "latest first" to "oldest first"
* Switch to redux-toolkit. Refactoring (#1171)
* Switch to redux-toolkit
* Fix #1207 (cherry-pick)
* refactor metrics (#1253)
Co-authored-by: Azat Belgibayev <belg.azat@gmail.com>
Co-authored-by: Alexander <mr.afigitelniychuvak@gmail.com>
Co-authored-by: azat.belgibayev <azat.belgibayev@almatech.dev>
Co-authored-by: sergei <scheremnov@provectus.com>
Co-authored-by: Alexander Krivonosov <31561808+GneyHabub@users.noreply.github.com>
Co-authored-by: Alina Miryuk <alinamiryuk@mail.ru>
Co-authored-by: Roman Zabaluev <rzabaluev@provectus.com>
This commit is contained in:
parent
f5d421d9f0
commit
7e5e8d9268
332 changed files with 14161 additions and 11785 deletions
|
@ -1699,12 +1699,7 @@ components:
|
|||
underReplicatedPartitions:
|
||||
type: integer
|
||||
cleanUpPolicy:
|
||||
type: string
|
||||
enum:
|
||||
- DELETE
|
||||
- COMPACT
|
||||
- COMPACT_DELETE
|
||||
- UNKNOWN
|
||||
$ref: '#/components/schemas/CleanUpPolicy'
|
||||
partitions:
|
||||
type: array
|
||||
items:
|
||||
|
@ -1752,6 +1747,8 @@ components:
|
|||
type: integer
|
||||
underReplicatedPartitions:
|
||||
type: integer
|
||||
cleanUpPolicy:
|
||||
$ref: '#/components/schemas/CleanUpPolicy'
|
||||
required:
|
||||
- name
|
||||
|
||||
|
@ -2630,4 +2627,12 @@ components:
|
|||
value:
|
||||
type: string
|
||||
source:
|
||||
$ref: '#/components/schemas/ConfigSource'
|
||||
$ref: '#/components/schemas/ConfigSource'
|
||||
|
||||
CleanUpPolicy:
|
||||
type: string
|
||||
enum:
|
||||
- DELETE
|
||||
- COMPACT
|
||||
- COMPACT_DELETE
|
||||
- UNKNOWN
|
|
@ -24,6 +24,7 @@
|
|||
"extends": [
|
||||
"airbnb-typescript",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:jest-dom/recommended",
|
||||
"plugin:prettier/recommended",
|
||||
"prettier"
|
||||
],
|
||||
|
@ -41,7 +42,8 @@
|
|||
}],
|
||||
"import/no-relative-parent-imports": "error",
|
||||
"no-debugger": "warn",
|
||||
"react/jsx-props-no-spreading": "off"
|
||||
"react/jsx-props-no-spreading": "off",
|
||||
"no-param-reassign": ["error", { "props": true, "ignorePropertyModificationsFor": ["state"] }]
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
|
|
400
kafka-ui-react-app/package-lock.json
generated
400
kafka-ui-react-app/package-lock.json
generated
|
@ -15,14 +15,12 @@
|
|||
"@babel/compat-data": {
|
||||
"version": "7.14.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.14.5.tgz",
|
||||
"integrity": "sha512-kixrYn4JwfAVPa0f2yfzc2AWti6WRRyO3XjWW5PJAvtE11qhSayrrcrEnee05KAtNaPC+EwehE8Qt1UedEVB8w==",
|
||||
"dev": true
|
||||
"integrity": "sha512-kixrYn4JwfAVPa0f2yfzc2AWti6WRRyO3XjWW5PJAvtE11qhSayrrcrEnee05KAtNaPC+EwehE8Qt1UedEVB8w=="
|
||||
},
|
||||
"@babel/core": {
|
||||
"version": "7.14.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.14.6.tgz",
|
||||
"integrity": "sha512-gJnOEWSqTk96qG5BoIrl5bVtc23DCycmIePPYnamY9RboYdI4nFy5vAQMSl81O5K/W0sLDWfGysnOECC+KUUCA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/code-frame": "^7.14.5",
|
||||
"@babel/generator": "^7.14.5",
|
||||
|
@ -45,7 +43,6 @@
|
|||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
|
||||
"integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ms": "2.1.2"
|
||||
}
|
||||
|
@ -54,7 +51,6 @@
|
|||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz",
|
||||
"integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"minimist": "^1.2.5"
|
||||
}
|
||||
|
@ -62,20 +58,17 @@
|
|||
"ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
|
||||
"dev": true
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
},
|
||||
"semver": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
|
||||
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
|
||||
"dev": true
|
||||
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
|
||||
},
|
||||
"source-map": {
|
||||
"version": "0.5.7",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
|
||||
"integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
|
||||
"dev": true
|
||||
"integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w="
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -83,7 +76,6 @@
|
|||
"version": "7.14.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.5.tgz",
|
||||
"integrity": "sha512-y3rlP+/G25OIX3mYKKIOlQRcqj7YgrvHxOLbVmyLJ9bPmi5ttvUmpydVjcFjZphOktWuA7ovbx91ECloWTfjIA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/types": "^7.14.5",
|
||||
"jsesc": "^2.5.1",
|
||||
|
@ -93,8 +85,7 @@
|
|||
"source-map": {
|
||||
"version": "0.5.7",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
|
||||
"integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
|
||||
"dev": true
|
||||
"integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w="
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -102,7 +93,6 @@
|
|||
"version": "7.14.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.14.5.tgz",
|
||||
"integrity": "sha512-EivH9EgBIb+G8ij1B2jAwSH36WnGvkQSEC6CkX/6v6ZFlw5fVOHvsgGF4uiEHO2GzMvunZb6tDLQEQSdrdocrA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/types": "^7.14.5"
|
||||
}
|
||||
|
@ -121,7 +111,6 @@
|
|||
"version": "7.14.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.14.5.tgz",
|
||||
"integrity": "sha512-v+QtZqXEiOnpO6EYvlImB6zCD2Lel06RzOPzmkz/D/XgQiUu3C/Jb1LOqSt/AIA34TYi/Q+KlT8vTQrgdxkbLw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/compat-data": "^7.14.5",
|
||||
"@babel/helper-validator-option": "^7.14.5",
|
||||
|
@ -132,8 +121,7 @@
|
|||
"semver": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
|
||||
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
|
||||
"dev": true
|
||||
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -213,7 +201,6 @@
|
|||
"version": "7.14.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.14.5.tgz",
|
||||
"integrity": "sha512-Gjna0AsXWfFvrAuX+VKcN/aNNWonizBj39yGwUzVDVTlMYJMK2Wp6xdpy72mfArFq5uK+NOuexfzZlzI1z9+AQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-get-function-arity": "^7.14.5",
|
||||
"@babel/template": "^7.14.5",
|
||||
|
@ -224,7 +211,6 @@
|
|||
"version": "7.14.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.14.5.tgz",
|
||||
"integrity": "sha512-I1Db4Shst5lewOM4V+ZKJzQ0JGGaZ6VY1jYvMghRjqs6DWgxLCIyFt30GlnKkfUeFLpJt2vzbMVEXVSXlIFYUg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/types": "^7.14.5"
|
||||
}
|
||||
|
@ -233,7 +219,6 @@
|
|||
"version": "7.14.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.14.5.tgz",
|
||||
"integrity": "sha512-R1PXiz31Uc0Vxy4OEOm07x0oSjKAdPPCh3tPivn/Eo8cvz6gveAeuyUUPB21Hoiif0uoPQSSdhIPS3352nvdyQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/types": "^7.14.5"
|
||||
}
|
||||
|
@ -242,7 +227,6 @@
|
|||
"version": "7.14.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.14.5.tgz",
|
||||
"integrity": "sha512-UxUeEYPrqH1Q/k0yRku1JE7dyfyehNwT6SVkMHvYvPDv4+uu627VXBckVj891BO8ruKBkiDoGnZf4qPDD8abDQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/types": "^7.14.5"
|
||||
}
|
||||
|
@ -251,7 +235,6 @@
|
|||
"version": "7.14.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.14.5.tgz",
|
||||
"integrity": "sha512-SwrNHu5QWS84XlHwGYPDtCxcA0hrSlL2yhWYLgeOc0w7ccOl2qv4s/nARI0aYZW+bSwAL5CukeXA47B/1NKcnQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/types": "^7.14.5"
|
||||
}
|
||||
|
@ -260,7 +243,6 @@
|
|||
"version": "7.14.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.14.5.tgz",
|
||||
"integrity": "sha512-iXpX4KW8LVODuAieD7MzhNjmM6dzYY5tfRqT+R9HDXWl0jPn/djKmA+G9s/2C2T9zggw5tK1QNqZ70USfedOwA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-module-imports": "^7.14.5",
|
||||
"@babel/helper-replace-supers": "^7.14.5",
|
||||
|
@ -276,7 +258,6 @@
|
|||
"version": "7.14.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.14.5.tgz",
|
||||
"integrity": "sha512-IqiLIrODUOdnPU9/F8ib1Fx2ohlgDhxnIDU7OEVi+kAbEZcyiF7BLU8W6PfvPi9LzztjS7kcbzbmL7oG8kD6VA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/types": "^7.14.5"
|
||||
}
|
||||
|
@ -302,7 +283,6 @@
|
|||
"version": "7.14.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.14.5.tgz",
|
||||
"integrity": "sha512-3i1Qe9/8x/hCHINujn+iuHy+mMRLoc77b2nI9TB0zjH1hvn9qGlXjWlggdwUcju36PkPCy/lpM7LLUdcTyH4Ow==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-member-expression-to-functions": "^7.14.5",
|
||||
"@babel/helper-optimise-call-expression": "^7.14.5",
|
||||
|
@ -314,7 +294,6 @@
|
|||
"version": "7.14.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.14.5.tgz",
|
||||
"integrity": "sha512-nfBN9xvmCt6nrMZjfhkl7i0oTV3yxR4/FztsbOASyTvVcoYd0TRHh7eMLdlEcCqobydC0LAF3LtC92Iwxo0wyw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/types": "^7.14.5"
|
||||
}
|
||||
|
@ -332,7 +311,6 @@
|
|||
"version": "7.14.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.14.5.tgz",
|
||||
"integrity": "sha512-hprxVPu6e5Kdp2puZUmvOGjaLv9TCe58E/Fl6hRq4YiVQxIcNvuq6uTM2r1mT/oPskuS9CgR+I94sqAYv0NGKA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/types": "^7.14.5"
|
||||
}
|
||||
|
@ -345,8 +323,7 @@
|
|||
"@babel/helper-validator-option": {
|
||||
"version": "7.14.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.14.5.tgz",
|
||||
"integrity": "sha512-OX8D5eeX4XwcroVW45NMvoYaIuFI+GQpA2a8Gi+X/U/cDUIRsV37qQfF905F0htTRCREQIB4KqPeaveRJUl3Ow==",
|
||||
"dev": true
|
||||
"integrity": "sha512-OX8D5eeX4XwcroVW45NMvoYaIuFI+GQpA2a8Gi+X/U/cDUIRsV37qQfF905F0htTRCREQIB4KqPeaveRJUl3Ow=="
|
||||
},
|
||||
"@babel/helper-wrap-function": {
|
||||
"version": "7.14.5",
|
||||
|
@ -364,7 +341,6 @@
|
|||
"version": "7.14.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.14.6.tgz",
|
||||
"integrity": "sha512-yesp1ENQBiLI+iYHSJdoZKUtRpfTlL1grDIX9NRlAVppljLw/4tTyYupIB7uIYmC3stW/imAv8EqaKaS/ibmeA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/template": "^7.14.5",
|
||||
"@babel/traverse": "^7.14.5",
|
||||
|
@ -417,8 +393,7 @@
|
|||
"@babel/parser": {
|
||||
"version": "7.14.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.6.tgz",
|
||||
"integrity": "sha512-oG0ej7efjEXxb4UgE+klVx+3j4MVo+A2vCzm7OUN4CLo6WhQ+vSOD2yJ8m7B+DghObxtLxt3EfgMWpq+AsWehQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-oG0ej7efjEXxb4UgE+klVx+3j4MVo+A2vCzm7OUN4CLo6WhQ+vSOD2yJ8m7B+DghObxtLxt3EfgMWpq+AsWehQ=="
|
||||
},
|
||||
"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": {
|
||||
"version": "7.14.5",
|
||||
|
@ -1349,7 +1324,6 @@
|
|||
"version": "7.14.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.14.5.tgz",
|
||||
"integrity": "sha512-6Z3Po85sfxRGachLULUhOmvAaOo7xCvqGQtxINai2mEGPFm6pQ4z5QInFnUrRpfoSV60BnjyF5F3c+15fxFV1g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/code-frame": "^7.14.5",
|
||||
"@babel/parser": "^7.14.5",
|
||||
|
@ -1360,7 +1334,6 @@
|
|||
"version": "7.14.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.14.5.tgz",
|
||||
"integrity": "sha512-G3BiS15vevepdmFqmUc9X+64y0viZYygubAMO8SvBmKARuF6CPSZtH4Ng9vi/lrWlZFGe3FWdXNy835akH8Glg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/code-frame": "^7.14.5",
|
||||
"@babel/generator": "^7.14.5",
|
||||
|
@ -1377,7 +1350,6 @@
|
|||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
|
||||
"integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ms": "2.1.2"
|
||||
}
|
||||
|
@ -1385,14 +1357,12 @@
|
|||
"globals": {
|
||||
"version": "11.12.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
|
||||
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
|
||||
"dev": true
|
||||
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
|
||||
"dev": true
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -1400,7 +1370,6 @@
|
|||
"version": "7.14.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.5.tgz",
|
||||
"integrity": "sha512-M/NzBpEL95I5Hh4dwhin5JlE7EzO5PHMAuzjxss3tiOBD46KfQvVedN/3jEPZvdRvtsK2222XfdHogNIttFgcg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-validator-identifier": "^7.14.5",
|
||||
"to-fast-properties": "^2.0.0"
|
||||
|
@ -1449,6 +1418,29 @@
|
|||
"integrity": "sha512-ij4wRiunFfaJxjB0BdrYHIH8FxBJpOwNPhhAcunlmPdXudL1WQV1qoP9un6JsEBAgQH+7UXyyjh0g7jTxXK6tg==",
|
||||
"dev": true
|
||||
},
|
||||
"@emotion/is-prop-valid": {
|
||||
"version": "0.8.8",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz",
|
||||
"integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==",
|
||||
"requires": {
|
||||
"@emotion/memoize": "0.7.4"
|
||||
}
|
||||
},
|
||||
"@emotion/memoize": {
|
||||
"version": "0.7.4",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz",
|
||||
"integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw=="
|
||||
},
|
||||
"@emotion/stylis": {
|
||||
"version": "0.8.5",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.5.tgz",
|
||||
"integrity": "sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ=="
|
||||
},
|
||||
"@emotion/unitless": {
|
||||
"version": "0.7.5",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz",
|
||||
"integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg=="
|
||||
},
|
||||
"@eslint/eslintrc": {
|
||||
"version": "0.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz",
|
||||
|
@ -2144,6 +2136,7 @@
|
|||
"version": "27.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@jest/types/-/types-27.4.2.tgz",
|
||||
"integrity": "sha512-j35yw0PMTPpZsUoOBiuHzr1zTYoad1cVIE0ajEjcrJONxxrko/IRGKkXx3os0Nsi4Hu3+5VmDbVfq5WhG/pWAg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/istanbul-lib-coverage": "^2.0.0",
|
||||
"@types/istanbul-reports": "^3.0.0",
|
||||
|
@ -2156,6 +2149,7 @@
|
|||
"version": "16.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz",
|
||||
"integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/yargs-parser": "*"
|
||||
}
|
||||
|
@ -2348,6 +2342,24 @@
|
|||
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.9.2.tgz",
|
||||
"integrity": "sha512-VZMYa7+fXHdwIq1TDhSXoVmSPEGM/aa+6Aiq3nVVJ9bXr24zScr+NlKFKC3iPljA7ho/GAZr+d2jOf5GIRC30Q=="
|
||||
},
|
||||
"@reduxjs/toolkit": {
|
||||
"version": "1.6.2",
|
||||
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.6.2.tgz",
|
||||
"integrity": "sha512-HbfI/hOVrAcMGAYsMWxw3UJyIoAS9JTdwddsjlr5w3S50tXhWb+EMyhIw+IAvCVCLETkzdjgH91RjDSYZekVBA==",
|
||||
"requires": {
|
||||
"immer": "^9.0.6",
|
||||
"redux": "^4.1.0",
|
||||
"redux-thunk": "^2.3.0",
|
||||
"reselect": "^4.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"immer": {
|
||||
"version": "9.0.7",
|
||||
"resolved": "https://registry.npmjs.org/immer/-/immer-9.0.7.tgz",
|
||||
"integrity": "sha512-KGllzpbamZDvOIxnmJ0jI840g7Oikx58lBPWV0hUh7dtAyZpFqqrBZdKka5GlTwMTZ1Tjc/bKKW4VSFAt6BqMA=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@rollup/plugin-node-resolve": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-7.1.3.tgz",
|
||||
|
@ -2569,6 +2581,26 @@
|
|||
"pretty-format": "^27.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@jest/types": {
|
||||
"version": "27.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@jest/types/-/types-27.2.5.tgz",
|
||||
"integrity": "sha512-nmuM4VuDtCZcY+eTpw+0nvstwReMsjPoj7ZR80/BbixulhLaiX+fbv8oeLW8WZlJMcsGQsTmMKT/iTZu1Uy/lQ==",
|
||||
"requires": {
|
||||
"@types/istanbul-lib-coverage": "^2.0.0",
|
||||
"@types/istanbul-reports": "^3.0.0",
|
||||
"@types/node": "*",
|
||||
"@types/yargs": "^16.0.0",
|
||||
"chalk": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"@types/yargs": {
|
||||
"version": "16.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz",
|
||||
"integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==",
|
||||
"requires": {
|
||||
"@types/yargs-parser": "*"
|
||||
}
|
||||
},
|
||||
"ansi-styles": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
|
||||
|
@ -2593,6 +2625,20 @@
|
|||
"ansi-regex": "^5.0.1",
|
||||
"ansi-styles": "^5.0.0",
|
||||
"react-is": "^17.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@jest/types": {
|
||||
"version": "27.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@jest/types/-/types-27.4.2.tgz",
|
||||
"integrity": "sha512-j35yw0PMTPpZsUoOBiuHzr1zTYoad1cVIE0ajEjcrJONxxrko/IRGKkXx3os0Nsi4Hu3+5VmDbVfq5WhG/pWAg==",
|
||||
"requires": {
|
||||
"@types/istanbul-lib-coverage": "^2.0.0",
|
||||
"@types/istanbul-reports": "^3.0.0",
|
||||
"@types/node": "*",
|
||||
"@types/yargs": "^16.0.0",
|
||||
"chalk": "^4.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"react-is": {
|
||||
|
@ -2640,6 +2686,15 @@
|
|||
"@testing-library/dom": "^8.0.0"
|
||||
}
|
||||
},
|
||||
"@testing-library/user-event": {
|
||||
"version": "13.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-13.5.0.tgz",
|
||||
"integrity": "sha512-5Kwtbo3Y/NowpkbRuSepbyMFkZmHgD+vPzYB/RJ4oxt5Gj/avFFBYjhw27cqSVPVw/3a67NK1PbiIr9k4Gwmdg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.12.5"
|
||||
}
|
||||
},
|
||||
"@tootallnate/once": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz",
|
||||
|
@ -2716,11 +2771,6 @@
|
|||
"@babel/types": "^7.3.0"
|
||||
}
|
||||
},
|
||||
"@types/base16": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/base16/-/base16-1.0.2.tgz",
|
||||
"integrity": "sha512-oYO/U4VD1DavwrKuCSQWdLG+5K22SLPem2OQaHmFcQuwHoVeGC+JGVRji2MUqZUAIQZHEonOeVfAX09hYiLsdg=="
|
||||
},
|
||||
"@types/cheerio": {
|
||||
"version": "0.22.29",
|
||||
"resolved": "https://registry.npmjs.org/@types/cheerio/-/cheerio-0.22.29.tgz",
|
||||
|
@ -2934,15 +2984,8 @@
|
|||
"@types/lodash": {
|
||||
"version": "4.14.177",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.177.tgz",
|
||||
"integrity": "sha512-0fDwydE2clKe9MNfvXHBHF9WEahRuj+msTuQqOmAApNORFvhMYZKNGGJdCzuhheVjMps/ti0Ak/iJPACMaevvw=="
|
||||
},
|
||||
"@types/lodash.curry": {
|
||||
"version": "4.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash.curry/-/lodash.curry-4.1.6.tgz",
|
||||
"integrity": "sha512-x3ctCcmOYqRrihNNnQJW6fe/yZFCgnrIa6p80AiPQRO8Jis29bBdy1dEw1FwngoF/mCZa3Bx+33fUZvOEE635Q==",
|
||||
"requires": {
|
||||
"@types/lodash": "*"
|
||||
}
|
||||
"integrity": "sha512-0fDwydE2clKe9MNfvXHBHF9WEahRuj+msTuQqOmAApNORFvhMYZKNGGJdCzuhheVjMps/ti0Ak/iJPACMaevvw==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/minimatch": {
|
||||
"version": "3.0.4",
|
||||
|
@ -3112,6 +3155,16 @@
|
|||
"integrity": "sha512-RJJrrySY7A8havqpGObOB4W92QXKJo63/jFLLgpvOtsGUqbQZ9Sbgl35KMm1DjC6j7AvmmU2bIno+3IyEaemaw==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/styled-components": {
|
||||
"version": "5.1.14",
|
||||
"resolved": "https://registry.npmjs.org/@types/styled-components/-/styled-components-5.1.14.tgz",
|
||||
"integrity": "sha512-d6P1/tyNytqKwam3cQXq7a9uPtovc/mdAs7dBiz1YbDdNIT3X4WmuFU78YdSYh84TXVuhOwezZ3EeKuNBhwsHQ==",
|
||||
"requires": {
|
||||
"@types/hoist-non-react-statics": "*",
|
||||
"@types/react": "*",
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"@types/tapable": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.7.tgz",
|
||||
|
@ -4491,6 +4544,22 @@
|
|||
"@babel/helper-define-polyfill-provider": "^0.2.2"
|
||||
}
|
||||
},
|
||||
"babel-plugin-styled-components": {
|
||||
"version": "1.13.2",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-1.13.2.tgz",
|
||||
"integrity": "sha512-Vb1R3d4g+MUfPQPVDMCGjm3cDocJEUTR7Xq7QS95JWWeksN1wdFRYpD2kulDgI3Huuaf1CZd+NK4KQmqUFh5dA==",
|
||||
"requires": {
|
||||
"@babel/helper-annotate-as-pure": "^7.0.0",
|
||||
"@babel/helper-module-imports": "^7.0.0",
|
||||
"babel-plugin-syntax-jsx": "^6.18.0",
|
||||
"lodash": "^4.17.11"
|
||||
}
|
||||
},
|
||||
"babel-plugin-syntax-jsx": {
|
||||
"version": "6.18.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz",
|
||||
"integrity": "sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY="
|
||||
},
|
||||
"babel-plugin-syntax-object-rest-spread": {
|
||||
"version": "6.13.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz",
|
||||
|
@ -4860,11 +4929,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"base16": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/base16/-/base16-1.0.0.tgz",
|
||||
"integrity": "sha1-4pf2DX7BAUp6lxo568ipjAtoHnA="
|
||||
},
|
||||
"base64-js": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||
|
@ -5091,7 +5155,6 @@
|
|||
"version": "4.16.6",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.6.tgz",
|
||||
"integrity": "sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"caniuse-lite": "^1.0.30001219",
|
||||
"colorette": "^1.2.2",
|
||||
|
@ -5163,11 +5226,6 @@
|
|||
"resolved": "https://registry.npmjs.org/bulma/-/bulma-0.9.3.tgz",
|
||||
"integrity": "sha512-0d7GNW1PY4ud8TWxdNcP6Cc8Bu7MxcntD/RRLGWuiw/s0a9P+XlH/6QoOIrmbj6o8WWJzJYhytiu9nFjTszk1g=="
|
||||
},
|
||||
"bulma-switch": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/bulma-switch/-/bulma-switch-2.0.0.tgz",
|
||||
"integrity": "sha512-myD38zeUfjmdduq+pXabhJEe3x2hQP48l/OI+Y0fO3HdDynZUY/VJygucvEAJKRjr4HxD5DnEm4yx+oDOBXpAA=="
|
||||
},
|
||||
"bytes": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
|
||||
|
@ -5289,6 +5347,11 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"camelize": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.0.tgz",
|
||||
"integrity": "sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs="
|
||||
},
|
||||
"caniuse-api": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz",
|
||||
|
@ -5302,10 +5365,9 @@
|
|||
}
|
||||
},
|
||||
"caniuse-lite": {
|
||||
"version": "1.0.30001237",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001237.tgz",
|
||||
"integrity": "sha512-pDHgRndit6p1NR2GhzMbQ6CkRrp4VKuSsqbcLeOQppYPKOYkKT/6ZvZDvKJUqcmtyWIAHuZq3SVS2vc1egCZzw==",
|
||||
"dev": true
|
||||
"version": "1.0.30001283",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001283.tgz",
|
||||
"integrity": "sha512-9RoKo841j1GQFSJz/nCXOj0sD7tHBtlowjYlrqIUS812x9/emfBLBt6IyMz1zIaYc/eRL8Cs6HPUVi2Hzq4sIg=="
|
||||
},
|
||||
"capture-exit": {
|
||||
"version": "2.0.0",
|
||||
|
@ -5628,6 +5690,7 @@
|
|||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/color/-/color-3.1.3.tgz",
|
||||
"integrity": "sha512-xgXAcTHa2HeFCGLE9Xs/R82hujGtu9Jd9x4NW3T34+OMs7VoPsjwzRczKHvTAHeJwWFwX5j15+MgAppE8ztObQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"color-convert": "^1.9.1",
|
||||
"color-string": "^1.5.4"
|
||||
|
@ -5650,6 +5713,7 @@
|
|||
"version": "1.5.5",
|
||||
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.5.tgz",
|
||||
"integrity": "sha512-jgIoum0OfQfq9Whcfc2z/VhCNcmQjWbey6qBX0vqt7YICflUmBCh9E9CiQD5GSJ+Uehixm3NUwHVhqUAWRivZg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"color-name": "^1.0.0",
|
||||
"simple-swizzle": "^0.2.2"
|
||||
|
@ -5658,8 +5722,7 @@
|
|||
"colorette": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz",
|
||||
"integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==",
|
||||
"dev": true
|
||||
"integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w=="
|
||||
},
|
||||
"combined-stream": {
|
||||
"version": "1.0.8",
|
||||
|
@ -5898,7 +5961,6 @@
|
|||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz",
|
||||
"integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"safe-buffer": "~5.1.1"
|
||||
},
|
||||
|
@ -5906,8 +5968,7 @@
|
|||
"safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||
"dev": true
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -5957,8 +6018,7 @@
|
|||
"core-js": {
|
||||
"version": "3.14.0",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.14.0.tgz",
|
||||
"integrity": "sha512-3s+ed8er9ahK+zJpp9ZtuVcDoFzHNiZsPbNAAE4KXgrRHbjSqqNN6xGSXq6bq7TZIbKj4NLrLb6bJ5i+vSVjHA==",
|
||||
"dev": true
|
||||
"integrity": "sha512-3s+ed8er9ahK+zJpp9ZtuVcDoFzHNiZsPbNAAE4KXgrRHbjSqqNN6xGSXq6bq7TZIbKj4NLrLb6bJ5i+vSVjHA=="
|
||||
},
|
||||
"core-js-compat": {
|
||||
"version": "3.14.0",
|
||||
|
@ -6110,6 +6170,11 @@
|
|||
"postcss": "^7.0.5"
|
||||
}
|
||||
},
|
||||
"css-color-keywords": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz",
|
||||
"integrity": "sha1-/qJhbcZ2spYmhrOvjb2+GAskTgU="
|
||||
},
|
||||
"css-color-names": {
|
||||
"version": "0.0.4",
|
||||
"resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz",
|
||||
|
@ -6220,6 +6285,16 @@
|
|||
"integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==",
|
||||
"dev": true
|
||||
},
|
||||
"css-to-react-native": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.0.0.tgz",
|
||||
"integrity": "sha512-Ro1yETZA813eoyUp2GDBhG2j+YggidUmzO1/v9eYBKR2EHVEniE2MI/NqpTQ954BMpTPZFsGNPm46qFB9dpaPQ==",
|
||||
"requires": {
|
||||
"camelize": "^1.0.0",
|
||||
"css-color-keywords": "^1.0.0",
|
||||
"postcss-value-parser": "^4.0.2"
|
||||
}
|
||||
},
|
||||
"css-tree": {
|
||||
"version": "1.0.0-alpha.37",
|
||||
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.37.tgz",
|
||||
|
@ -7037,8 +7112,7 @@
|
|||
"electron-to-chromium": {
|
||||
"version": "1.3.752",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.752.tgz",
|
||||
"integrity": "sha512-2Tg+7jSl3oPxgsBsWKh5H83QazTkmWG/cnNwJplmyZc7KcN61+I10oUgaXSVk/NwfvN3BdkKDR4FYuRBQQ2v0A==",
|
||||
"dev": true
|
||||
"integrity": "sha512-2Tg+7jSl3oPxgsBsWKh5H83QazTkmWG/cnNwJplmyZc7KcN61+I10oUgaXSVk/NwfvN3BdkKDR4FYuRBQQ2v0A=="
|
||||
},
|
||||
"elliptic": {
|
||||
"version": "6.5.4",
|
||||
|
@ -7347,8 +7421,7 @@
|
|||
"escalade": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
|
||||
"integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==",
|
||||
"dev": true
|
||||
"integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw=="
|
||||
},
|
||||
"escape-html": {
|
||||
"version": "1.0.3",
|
||||
|
@ -7880,6 +7953,35 @@
|
|||
"@typescript-eslint/experimental-utils": "^4.0.1"
|
||||
}
|
||||
},
|
||||
"eslint-plugin-jest-dom": {
|
||||
"version": "3.9.2",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-jest-dom/-/eslint-plugin-jest-dom-3.9.2.tgz",
|
||||
"integrity": "sha512-DKNW6nxYkBvwv36WcYFxapCalGjOGSWUu5PREpDVuXGbEns3S5jhr+mZ5W2N6MxbOWw/2U61C1JVLH31gwVjOQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.9.6",
|
||||
"@testing-library/dom": "^7.28.1",
|
||||
"requireindex": "^1.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@testing-library/dom": {
|
||||
"version": "7.31.2",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-7.31.2.tgz",
|
||||
"integrity": "sha512-3UqjCpey6HiTZT92vODYLPxTBWlM8ZOOjr3LX5F37/VRipW2M1kX6I/Cm4VXzteZqfGfagg8yXywpcOgQBlNsQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/code-frame": "^7.10.4",
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@types/aria-query": "^4.2.0",
|
||||
"aria-query": "^4.2.2",
|
||||
"chalk": "^4.1.0",
|
||||
"dom-accessibility-api": "^0.5.6",
|
||||
"lz-string": "^1.4.4",
|
||||
"pretty-format": "^26.6.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"eslint-plugin-jsx-a11y": {
|
||||
"version": "6.4.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.4.1.tgz",
|
||||
|
@ -8693,7 +8795,6 @@
|
|||
"version": "9.11.0",
|
||||
"resolved": "https://registry.npmjs.org/fetch-mock/-/fetch-mock-9.11.0.tgz",
|
||||
"integrity": "sha512-PG1XUv+x7iag5p/iNHD4/jdpxL9FtVSqRMUQhPab4hVDt80T1MH5ehzVrL2IdXO9Q2iBggArFvPqjUbHFuI58Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/core": "^7.0.0",
|
||||
"@babel/runtime": "^7.0.0",
|
||||
|
@ -8708,10 +8809,9 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"debug": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
|
||||
"integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
|
||||
"dev": true,
|
||||
"version": "4.3.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz",
|
||||
"integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==",
|
||||
"requires": {
|
||||
"ms": "2.1.2"
|
||||
}
|
||||
|
@ -8719,14 +8819,12 @@
|
|||
"ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
|
||||
"dev": true
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
},
|
||||
"path-to-regexp": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-2.4.0.tgz",
|
||||
"integrity": "sha512-G6zHoVqC6GGTQkZwF4lkuEyMbVOjoBKAEybQUypI1WTkqinCOrq2x6U2+phkJ1XsEMTy4LjtwPI7HW+NVrRR2w==",
|
||||
"dev": true
|
||||
"integrity": "sha512-G6zHoVqC6GGTQkZwF4lkuEyMbVOjoBKAEybQUypI1WTkqinCOrq2x6U2+phkJ1XsEMTy4LjtwPI7HW+NVrRR2w=="
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -9351,8 +9449,7 @@
|
|||
"gensync": {
|
||||
"version": "1.0.0-beta.2",
|
||||
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
||||
"integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
|
||||
"dev": true
|
||||
"integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="
|
||||
},
|
||||
"get-caller-file": {
|
||||
"version": "2.0.5",
|
||||
|
@ -9432,8 +9529,7 @@
|
|||
"glob-to-regexp": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz",
|
||||
"integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==",
|
||||
"dev": true
|
||||
"integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw=="
|
||||
},
|
||||
"global-modules": {
|
||||
"version": "2.0.0",
|
||||
|
@ -10310,7 +10406,8 @@
|
|||
"is-arrayish": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
|
||||
"integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="
|
||||
"integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==",
|
||||
"dev": true
|
||||
},
|
||||
"is-bigint": {
|
||||
"version": "1.0.2",
|
||||
|
@ -10595,8 +10692,7 @@
|
|||
"is-subset": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-subset/-/is-subset-0.1.1.tgz",
|
||||
"integrity": "sha1-ilkRfZMt4d4A8kX83TnOQ/HpOaY=",
|
||||
"dev": true
|
||||
"integrity": "sha1-ilkRfZMt4d4A8kX83TnOQ/HpOaY="
|
||||
},
|
||||
"is-symbol": {
|
||||
"version": "1.0.4",
|
||||
|
@ -12105,6 +12201,15 @@
|
|||
"xml": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"jest-styled-components": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/jest-styled-components/-/jest-styled-components-7.0.6.tgz",
|
||||
"integrity": "sha512-asCitkJfaO1TX60nbIBXDUglblDFvKl9aKA5IV09AmO2zL6DC89MgrK73/e7zXzMOpzyVmuXQLLYlsDUSYIb7g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"css": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"jest-util": {
|
||||
"version": "26.6.2",
|
||||
"resolved": "https://registry.npmjs.org/jest-util/-/jest-util-26.6.2.tgz",
|
||||
|
@ -12328,8 +12433,7 @@
|
|||
"jsesc": {
|
||||
"version": "2.5.2",
|
||||
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
|
||||
"integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==",
|
||||
"dev": true
|
||||
"integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA=="
|
||||
},
|
||||
"json-parse-better-errors": {
|
||||
"version": "1.0.2",
|
||||
|
@ -12670,11 +12774,6 @@
|
|||
"integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=",
|
||||
"dev": true
|
||||
},
|
||||
"lodash.curry": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash.curry/-/lodash.curry-4.1.1.tgz",
|
||||
"integrity": "sha1-JI42By7ekGUB11lmIAqG2riyMXA="
|
||||
},
|
||||
"lodash.debounce": {
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
|
||||
|
@ -12724,8 +12823,7 @@
|
|||
"lodash.sortby": {
|
||||
"version": "4.7.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz",
|
||||
"integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=",
|
||||
"dev": true
|
||||
"integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg="
|
||||
},
|
||||
"lodash.template": {
|
||||
"version": "4.5.0",
|
||||
|
@ -13565,8 +13663,7 @@
|
|||
"node-releases": {
|
||||
"version": "1.1.73",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.73.tgz",
|
||||
"integrity": "sha512-uW7fodD6pyW2FZNZnp/Z3hvWKeEW1Y8R1+1CnErE8cXFXzl5blBOoVB41CvMer6P6Q0S5FXDwcHgFd1Wj0U9zg==",
|
||||
"dev": true
|
||||
"integrity": "sha512-uW7fodD6pyW2FZNZnp/Z3hvWKeEW1Y8R1+1CnErE8cXFXzl5blBOoVB41CvMer6P6Q0S5FXDwcHgFd1Wj0U9zg=="
|
||||
},
|
||||
"normalize-package-data": {
|
||||
"version": "2.5.0",
|
||||
|
@ -15416,8 +15513,7 @@
|
|||
"postcss-value-parser": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz",
|
||||
"integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ=="
|
||||
},
|
||||
"postcss-values-parser": {
|
||||
"version": "2.0.1",
|
||||
|
@ -15673,8 +15769,7 @@
|
|||
"querystring": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.1.tgz",
|
||||
"integrity": "sha512-wkvS7mL/JMugcup3/rMitHmd9ecIGd2lhFhK9N3UUQ450h66d1r3Y9nvXzQAW1Lq+wyx61k/1pfKS5KuKiyEbg==",
|
||||
"dev": true
|
||||
"integrity": "sha512-wkvS7mL/JMugcup3/rMitHmd9ecIGd2lhFhK9N3UUQ450h66d1r3Y9nvXzQAW1Lq+wyx61k/1pfKS5KuKiyEbg=="
|
||||
},
|
||||
"querystring-es3": {
|
||||
"version": "0.2.1",
|
||||
|
@ -15789,19 +15884,6 @@
|
|||
"whatwg-fetch": "^3.4.1"
|
||||
}
|
||||
},
|
||||
"react-base16-styling": {
|
||||
"version": "0.8.0",
|
||||
"resolved": "https://registry.npmjs.org/react-base16-styling/-/react-base16-styling-0.8.0.tgz",
|
||||
"integrity": "sha512-ElvciPaL4xpWh7ISX7ugkNS/dvoh7DpVMp4t93ngnEsS2LkMd8Gu+cDDOLis2rj4889CNK662UdjOfv3wvZg9w==",
|
||||
"requires": {
|
||||
"@types/base16": "^1.0.2",
|
||||
"@types/lodash.curry": "^4.1.6",
|
||||
"base16": "^1.0.0",
|
||||
"color": "^3.1.2",
|
||||
"csstype": "^3.0.2",
|
||||
"lodash.curry": "^4.1.1"
|
||||
}
|
||||
},
|
||||
"react-datepicker": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-4.5.0.tgz",
|
||||
|
@ -16065,16 +16147,6 @@
|
|||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
||||
},
|
||||
"react-json-tree": {
|
||||
"version": "0.15.0",
|
||||
"resolved": "https://registry.npmjs.org/react-json-tree/-/react-json-tree-0.15.0.tgz",
|
||||
"integrity": "sha512-/bEFXZBfLFiep6ReuzatR8mz9G7sRmejElRDgcAuqY0Jsx7llouax2DM03rlQifrUJgmvTGmPA+olyWYyGagqA==",
|
||||
"requires": {
|
||||
"@types/prop-types": "^15.7.3",
|
||||
"prop-types": "^15.7.2",
|
||||
"react-base16-styling": "^0.8.0"
|
||||
}
|
||||
},
|
||||
"react-multi-select-component": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/react-multi-select-component/-/react-multi-select-component-4.0.6.tgz",
|
||||
|
@ -16443,9 +16515,9 @@
|
|||
}
|
||||
},
|
||||
"redux-thunk": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.3.0.tgz",
|
||||
"integrity": "sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw=="
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.1.tgz",
|
||||
"integrity": "sha512-OOYGNY5Jy2TWvTL1KgAlVy6dcx3siPJ1wTq741EPyUKfn6W6nChdICjZwCd0p8AZBs5kWpZlbkXW2nE/zjUa+Q=="
|
||||
},
|
||||
"reflect-metadata": {
|
||||
"version": "0.1.13",
|
||||
|
@ -16622,6 +16694,12 @@
|
|||
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
|
||||
"dev": true
|
||||
},
|
||||
"requireindex": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.2.0.tgz",
|
||||
"integrity": "sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==",
|
||||
"dev": true
|
||||
},
|
||||
"requires-port": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
|
||||
|
@ -17407,6 +17485,11 @@
|
|||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"shallowequal": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz",
|
||||
"integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ=="
|
||||
},
|
||||
"shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
|
@ -17456,6 +17539,7 @@
|
|||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
|
||||
"integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"is-arrayish": "^0.3.1"
|
||||
}
|
||||
|
@ -18178,6 +18262,38 @@
|
|||
"schema-utils": "^2.7.0"
|
||||
}
|
||||
},
|
||||
"styled-components": {
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.1.tgz",
|
||||
"integrity": "sha512-JThv2JRzyH0NOIURrk9iskdxMSAAtCfj/b2Sf1WJaCUsloQkblepy1jaCLX/bYE+mhYo3unmwVSI9I5d9ncSiQ==",
|
||||
"requires": {
|
||||
"@babel/helper-module-imports": "^7.0.0",
|
||||
"@babel/traverse": "^7.4.5",
|
||||
"@emotion/is-prop-valid": "^0.8.8",
|
||||
"@emotion/stylis": "^0.8.4",
|
||||
"@emotion/unitless": "^0.7.4",
|
||||
"babel-plugin-styled-components": ">= 1.12.0",
|
||||
"css-to-react-native": "^3.0.0",
|
||||
"hoist-non-react-statics": "^3.0.0",
|
||||
"shallowequal": "^1.1.0",
|
||||
"supports-color": "^5.5.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"has-flag": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
|
||||
"integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0="
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
|
||||
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
|
||||
"requires": {
|
||||
"has-flag": "^3.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"stylehacks": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-4.0.3.tgz",
|
||||
|
@ -18769,8 +18885,7 @@
|
|||
"to-fast-properties": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
|
||||
"integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=",
|
||||
"dev": true
|
||||
"integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4="
|
||||
},
|
||||
"to-object-path": {
|
||||
"version": "0.3.0",
|
||||
|
@ -18827,7 +18942,6 @@
|
|||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz",
|
||||
"integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"punycode": "^2.1.0"
|
||||
}
|
||||
|
@ -19733,8 +19847,7 @@
|
|||
"webidl-conversions": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz",
|
||||
"integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==",
|
||||
"dev": true
|
||||
"integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg=="
|
||||
},
|
||||
"webpack": {
|
||||
"version": "4.44.2",
|
||||
|
@ -20774,7 +20887,6 @@
|
|||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-6.5.0.tgz",
|
||||
"integrity": "sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"lodash.sortby": "^4.7.0",
|
||||
"tr46": "^1.0.1",
|
||||
|
|
|
@ -7,18 +7,20 @@
|
|||
"@fortawesome/fontawesome-free": "^5.15.4",
|
||||
"@hookform/error-message": "^2.0.0",
|
||||
"@hookform/resolvers": "^2.7.1",
|
||||
"@reduxjs/toolkit": "^1.6.2",
|
||||
"@rooks/use-outside-click-ref": "^4.10.1",
|
||||
"@testing-library/react": "^12.0.0",
|
||||
"@types/eventsource": "^1.1.6",
|
||||
"@types/styled-components": "^5.1.14",
|
||||
"@types/yup": "^0.29.13",
|
||||
"ace-builds": "^1.4.12",
|
||||
"ajv": "^8.6.3",
|
||||
"bulma": "^0.9.3",
|
||||
"bulma-switch": "^2.0.0",
|
||||
"classnames": "^2.2.6",
|
||||
"dayjs": "^1.10.6",
|
||||
"eslint-import-resolver-node": "^0.3.5",
|
||||
"eslint-import-resolver-typescript": "^2.4.0",
|
||||
"fetch-mock": "^9.11.0",
|
||||
"json-schema-faker": "^0.5.0-rcv.39",
|
||||
"lodash": "^4.17.21",
|
||||
"node-fetch": "^2.6.1",
|
||||
|
@ -28,16 +30,15 @@
|
|||
"react-datepicker": "^4.2.0",
|
||||
"react-dom": "^17.0.1",
|
||||
"react-hook-form": "7.6.9",
|
||||
"react-json-tree": "^0.15.0",
|
||||
"react-multi-select-component": "^4.0.6",
|
||||
"react-redux": "^7.2.2",
|
||||
"react-router": "^5.2.0",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"redux": "^4.1.1",
|
||||
"redux-thunk": "^2.3.0",
|
||||
"reselect": "^4.0.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"sass": "^1.43.4",
|
||||
"styled-components": "^5.3.1",
|
||||
"typesafe-actions": "^5.1.0",
|
||||
"use-debounce": "^7.0.0",
|
||||
"uuid": "^8.3.1",
|
||||
|
@ -80,7 +81,9 @@
|
|||
"devDependencies": {
|
||||
"@jest/types": "^27.0.6",
|
||||
"@openapitools/openapi-generator-cli": "^2.4.15",
|
||||
"@testing-library/dom": "^8.11.1",
|
||||
"@testing-library/jest-dom": "^5.14.1",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@types/classnames": "^2.2.11",
|
||||
"@types/enzyme": "^3.10.9",
|
||||
"@types/jest": "^27.0.2",
|
||||
|
@ -94,6 +97,7 @@
|
|||
"@types/react-router-dom": "^5.1.8",
|
||||
"@types/react-test-renderer": "^17.0.1",
|
||||
"@types/redux-mock-store": "^1.0.3",
|
||||
"@types/styled-components": "^5.1.13",
|
||||
"@types/uuid": "^8.3.1",
|
||||
"@typescript-eslint/eslint-plugin": "^4.29.1",
|
||||
"@typescript-eslint/parser": "^4.29.1",
|
||||
|
@ -106,6 +110,7 @@
|
|||
"eslint-config-airbnb-typescript": "^12.3.1",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-import": "^2.24.0",
|
||||
"eslint-plugin-jest-dom": "^3.9.2",
|
||||
"eslint-plugin-jsx-a11y": "^6.4.1",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"eslint-plugin-react": "^7.21.5",
|
||||
|
@ -116,6 +121,7 @@
|
|||
"http-proxy-middleware": "^2.0.1",
|
||||
"husky": "^7.0.1",
|
||||
"jest-sonar-reporter": "^2.0.0",
|
||||
"jest-styled-components": "^7.0.6",
|
||||
"lint-staged": "^11.1.2",
|
||||
"prettier": "^2.3.1",
|
||||
"react-scripts": "4.0.3",
|
||||
|
|
|
@ -2,7 +2,7 @@ sonar.projectKey=provectus_kafka-ui_frontend
|
|||
sonar.organization=provectus
|
||||
|
||||
sonar.sources=.
|
||||
sonar.exclusions="**/__test?__/**,src/setupWorker.ts,src/setupTests.ts,**/fixtures.ts"
|
||||
sonar.exclusions="**/__test?__/**,src/setupWorker.ts,src/setupTests.ts,**/fixtures.ts,src/lib/testHelpers.tsx"
|
||||
|
||||
sonar.typescript.lcov.reportPaths=./coverage/lcov.info
|
||||
sonar.testExecutionReportPaths=./test-report.xml
|
||||
|
|
|
@ -1,48 +0,0 @@
|
|||
import React from 'react';
|
||||
import cx from 'classnames';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { dismissAlert } from 'redux/actions';
|
||||
import { Alert as AlertProps } from 'redux/interfaces';
|
||||
|
||||
const Alert: React.FC<AlertProps> = ({
|
||||
id,
|
||||
type,
|
||||
title,
|
||||
message,
|
||||
response,
|
||||
}) => {
|
||||
const classNames = React.useMemo(
|
||||
() =>
|
||||
cx('notification', {
|
||||
'is-danger': type === 'error',
|
||||
'is-success': type === 'success',
|
||||
'is-info': type === 'info',
|
||||
'is-warning': type === 'warning',
|
||||
}),
|
||||
[type]
|
||||
);
|
||||
const dispatch = useDispatch();
|
||||
const dismiss = React.useCallback(() => {
|
||||
dispatch(dismissAlert(id));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={classNames}>
|
||||
<button className="delete" type="button" onClick={dismiss}>
|
||||
x
|
||||
</button>
|
||||
<div>
|
||||
<h6 className="title is-6">{title}</h6>
|
||||
<p className="subtitle is-6">{message}</p>
|
||||
{response && (
|
||||
<div className="is-flex">
|
||||
<div className="mr-3">{response.status}</div>
|
||||
<div>{response.body?.message || response.statusText}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Alert;
|
|
@ -1,119 +0,0 @@
|
|||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { Alert as AlertProps } from 'redux/interfaces';
|
||||
import * as actions from 'redux/actions/actions';
|
||||
import Alert from 'components/Alert/Alert';
|
||||
|
||||
const id = 'test-id';
|
||||
const title = 'My Alert Title';
|
||||
const message = 'My Alert Message';
|
||||
const statusCode = 123;
|
||||
const serverSideMessage = 'Server Side Message';
|
||||
const httpStatusText = 'My Status Text';
|
||||
const dismiss = jest.fn();
|
||||
|
||||
describe('Alert', () => {
|
||||
const setupComponent = (props: Partial<AlertProps> = {}) => (
|
||||
<Alert
|
||||
id={id}
|
||||
type="error"
|
||||
title={title}
|
||||
message={message}
|
||||
createdAt={1234567}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
it('renders with initial props', () => {
|
||||
const wrapper = mount(setupComponent());
|
||||
expect(wrapper.exists('.title.is-6')).toBeTruthy();
|
||||
expect(wrapper.find('.title.is-6').text()).toEqual(title);
|
||||
expect(wrapper.exists('.subtitle.is-6')).toBeTruthy();
|
||||
expect(wrapper.find('.subtitle.is-6').text()).toEqual(message);
|
||||
expect(wrapper.exists('button')).toBeTruthy();
|
||||
expect(wrapper.exists('.is-flex')).toBeFalsy();
|
||||
});
|
||||
|
||||
it('renders alert with server side message', () => {
|
||||
const wrapper = mount(
|
||||
setupComponent({
|
||||
type: 'info',
|
||||
response: {
|
||||
status: statusCode,
|
||||
statusText: 'My Status Text',
|
||||
body: {
|
||||
message: serverSideMessage,
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(wrapper.exists('.is-flex')).toBeTruthy();
|
||||
expect(wrapper.find('.is-flex').text()).toEqual(
|
||||
`${statusCode}${serverSideMessage}`
|
||||
);
|
||||
});
|
||||
|
||||
it('renders alert with http status text', () => {
|
||||
const wrapper = mount(
|
||||
setupComponent({
|
||||
type: 'info',
|
||||
response: {
|
||||
status: statusCode,
|
||||
statusText: httpStatusText,
|
||||
body: {},
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(wrapper.exists('.is-flex')).toBeTruthy();
|
||||
expect(wrapper.find('.is-flex').text()).toEqual(
|
||||
`${statusCode}${httpStatusText}`
|
||||
);
|
||||
});
|
||||
|
||||
it('matches snapshot', () => {
|
||||
expect(mount(setupComponent())).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe('types', () => {
|
||||
it('renders error', () => {
|
||||
const wrapper = mount(setupComponent({ type: 'error' }));
|
||||
expect(wrapper.exists('.notification.is-danger')).toBeTruthy();
|
||||
expect(wrapper.exists('.notification.is-warning')).toBeFalsy();
|
||||
expect(wrapper.exists('.notification.is-info')).toBeFalsy();
|
||||
expect(wrapper.exists('.notification.is-success')).toBeFalsy();
|
||||
});
|
||||
|
||||
it('renders warning', () => {
|
||||
const wrapper = mount(setupComponent({ type: 'warning' }));
|
||||
expect(wrapper.exists('.notification.is-warning')).toBeTruthy();
|
||||
expect(wrapper.exists('.notification.is-danger')).toBeFalsy();
|
||||
expect(wrapper.exists('.notification.is-info')).toBeFalsy();
|
||||
expect(wrapper.exists('.notification.is-success')).toBeFalsy();
|
||||
});
|
||||
|
||||
it('renders info', () => {
|
||||
const wrapper = mount(setupComponent({ type: 'info' }));
|
||||
expect(wrapper.exists('.notification.is-info')).toBeTruthy();
|
||||
expect(wrapper.exists('.notification.is-warning')).toBeFalsy();
|
||||
expect(wrapper.exists('.notification.is-danger')).toBeFalsy();
|
||||
expect(wrapper.exists('.notification.is-success')).toBeFalsy();
|
||||
});
|
||||
|
||||
it('renders success', () => {
|
||||
const wrapper = mount(setupComponent({ type: 'success' }));
|
||||
expect(wrapper.exists('.notification.is-success')).toBeTruthy();
|
||||
expect(wrapper.exists('.notification.is-warning')).toBeFalsy();
|
||||
expect(wrapper.exists('.notification.is-info')).toBeFalsy();
|
||||
expect(wrapper.exists('.notification.is-danger')).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('dismiss', () => {
|
||||
it('handles dismiss callback', () => {
|
||||
jest.spyOn(actions, 'dismissAlert').mockImplementation(dismiss);
|
||||
const wrapper = mount(setupComponent());
|
||||
wrapper.find('button').simulate('click');
|
||||
expect(dismiss).toHaveBeenCalledWith(id);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,35 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Alert matches snapshot 1`] = `
|
||||
<Alert
|
||||
createdAt={1234567}
|
||||
id="test-id"
|
||||
message="My Alert Message"
|
||||
title="My Alert Title"
|
||||
type="error"
|
||||
>
|
||||
<div
|
||||
className="notification is-danger"
|
||||
>
|
||||
<button
|
||||
className="delete"
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
x
|
||||
</button>
|
||||
<div>
|
||||
<h6
|
||||
className="title is-6"
|
||||
>
|
||||
My Alert Title
|
||||
</h6>
|
||||
<p
|
||||
className="subtitle is-6"
|
||||
>
|
||||
My Alert Message
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Alert>
|
||||
`;
|
27
kafka-ui-react-app/src/components/Alerts/Alert.styled.ts
Normal file
27
kafka-ui-react-app/src/components/Alerts/Alert.styled.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
import { AlertType } from 'redux/interfaces';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Alert = styled.div<{ $type: AlertType }>`
|
||||
background-color: ${({ $type, theme }) => theme.alert.color[$type]};
|
||||
width: 400px;
|
||||
min-height: 64px;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
filter: drop-shadow(0px 4px 16px rgba(0, 0, 0, 0.1));
|
||||
margin-top: 10px;
|
||||
line-height: 20px;
|
||||
`;
|
||||
|
||||
export const Title = styled.div`
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
`;
|
||||
|
||||
export const Message = styled.p`
|
||||
font-weight: normal;
|
||||
font-size: 14px;
|
||||
margin: 3px 0;
|
||||
`;
|
28
kafka-ui-react-app/src/components/Alerts/Alert.tsx
Normal file
28
kafka-ui-react-app/src/components/Alerts/Alert.tsx
Normal file
|
@ -0,0 +1,28 @@
|
|||
import React from 'react';
|
||||
import CloseIcon from 'components/common/Icons/CloseIcon';
|
||||
import IconButtonWrapper from 'components/common/Icons/IconButtonWrapper';
|
||||
import { Alert as AlertType } from 'redux/interfaces';
|
||||
|
||||
import * as S from './Alert.styled';
|
||||
|
||||
export interface AlertProps {
|
||||
title: AlertType['title'];
|
||||
type: AlertType['type'];
|
||||
message: AlertType['message'];
|
||||
onDissmiss(): void;
|
||||
}
|
||||
|
||||
const Alert: React.FC<AlertProps> = ({ title, type, message, onDissmiss }) => (
|
||||
<S.Alert $type={type} role="alert">
|
||||
<div>
|
||||
<S.Title role="heading">{title}</S.Title>
|
||||
<S.Message role="contentinfo">{message}</S.Message>
|
||||
</div>
|
||||
|
||||
<IconButtonWrapper role="button" onClick={onDissmiss}>
|
||||
<CloseIcon />
|
||||
</IconButtonWrapper>
|
||||
</S.Alert>
|
||||
);
|
||||
|
||||
export default Alert;
|
44
kafka-ui-react-app/src/components/Alerts/Alerts.tsx
Normal file
44
kafka-ui-react-app/src/components/Alerts/Alerts.tsx
Normal file
|
@ -0,0 +1,44 @@
|
|||
import React from 'react';
|
||||
import { dismissAlert } from 'redux/actions';
|
||||
import { getAlerts } from 'redux/reducers/alerts/selectors';
|
||||
import { alertDissmissed, selectAll } from 'redux/reducers/alerts/alertsSlice';
|
||||
import { useAppSelector, useAppDispatch } from 'lib/hooks/redux';
|
||||
import Alert from 'components/Alerts/Alert';
|
||||
|
||||
const Alerts: React.FC = () => {
|
||||
const alerts = useAppSelector(selectAll);
|
||||
const dispatch = useAppDispatch();
|
||||
const dismiss = React.useCallback((id: string) => {
|
||||
dispatch(alertDissmissed(id));
|
||||
}, []);
|
||||
|
||||
const legacyAlerts = useAppSelector(getAlerts);
|
||||
const dismissLegacy = React.useCallback((id: string) => {
|
||||
dispatch(dismissAlert(id));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
{alerts.map(({ id, type, title, message }) => (
|
||||
<Alert
|
||||
key={id}
|
||||
type={type}
|
||||
title={title}
|
||||
message={message}
|
||||
onDissmiss={() => dismiss(id)}
|
||||
/>
|
||||
))}
|
||||
{legacyAlerts.map(({ id, type, title, message }) => (
|
||||
<Alert
|
||||
key={id}
|
||||
type={type}
|
||||
title={title}
|
||||
message={message}
|
||||
onDissmiss={() => dismissLegacy(id)}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Alerts;
|
|
@ -0,0 +1,36 @@
|
|||
import React from 'react';
|
||||
import { screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { Alert as AlertProps } from 'redux/interfaces';
|
||||
import Alert from 'components/Alerts/Alert';
|
||||
import { render } from 'lib/testHelpers';
|
||||
|
||||
const id = 'test-id';
|
||||
const title = 'My Alert Title';
|
||||
const message = 'My Alert Message';
|
||||
const dismiss = jest.fn();
|
||||
|
||||
describe('Alert', () => {
|
||||
const setupComponent = (props: Partial<AlertProps> = {}) =>
|
||||
render(
|
||||
<Alert
|
||||
id={id}
|
||||
type="error"
|
||||
title={title}
|
||||
message={message}
|
||||
onDissmiss={dismiss}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
it('renders with initial props', () => {
|
||||
setupComponent();
|
||||
expect(screen.getByRole('heading')).toHaveTextContent(title);
|
||||
expect(screen.getByRole('contentinfo')).toHaveTextContent(message);
|
||||
expect(screen.getByRole('button')).toBeInTheDocument();
|
||||
});
|
||||
it('handles dismiss callback', () => {
|
||||
setupComponent();
|
||||
userEvent.click(screen.getByRole('button'));
|
||||
expect(dismiss).toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,59 @@
|
|||
import React from 'react';
|
||||
import { Action, FailurePayload, ServerResponse } from 'redux/interfaces';
|
||||
import { screen } from '@testing-library/react';
|
||||
import Alerts from 'components/Alerts/Alerts';
|
||||
import { render } from 'lib/testHelpers';
|
||||
import { store } from 'redux/store';
|
||||
import { UnknownAsyncThunkRejectedWithValueAction } from '@reduxjs/toolkit/dist/matchers';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
describe('Alerts', () => {
|
||||
beforeEach(() => render(<Alerts />));
|
||||
|
||||
it('renders alerts', async () => {
|
||||
const payload: ServerResponse = {
|
||||
status: 422,
|
||||
statusText: 'Unprocessable Entity',
|
||||
message: 'Unprocessable Entity',
|
||||
url: 'https://test.com/clusters',
|
||||
};
|
||||
const action: UnknownAsyncThunkRejectedWithValueAction = {
|
||||
type: 'any/action/rejected',
|
||||
payload,
|
||||
meta: {
|
||||
arg: 'test',
|
||||
requestId: 'test-request-id',
|
||||
requestStatus: 'rejected',
|
||||
aborted: false,
|
||||
condition: false,
|
||||
rejectedWithValue: true,
|
||||
},
|
||||
error: { message: 'Rejected' },
|
||||
};
|
||||
store.dispatch(action);
|
||||
|
||||
const alert: FailurePayload = {
|
||||
title: '404 - Not Found',
|
||||
message: 'Item is not found',
|
||||
subject: 'subject',
|
||||
};
|
||||
const legacyAction: Action = {
|
||||
type: 'CLEAR_TOPIC_MESSAGES__FAILURE',
|
||||
payload: { alert },
|
||||
};
|
||||
store.dispatch(legacyAction);
|
||||
|
||||
expect(screen.getAllByRole('alert').length).toEqual(2);
|
||||
|
||||
const dissmissAlertButtons = screen.getAllByRole('button');
|
||||
expect(dissmissAlertButtons.length).toEqual(2);
|
||||
|
||||
const dissmissButton = dissmissAlertButtons[0];
|
||||
const dissmissLegacyButton = dissmissAlertButtons[1];
|
||||
|
||||
userEvent.click(dissmissButton);
|
||||
userEvent.click(dissmissLegacyButton);
|
||||
|
||||
expect(screen.queryAllByRole('alert').length).toEqual(0);
|
||||
});
|
||||
});
|
|
@ -1,98 +0,0 @@
|
|||
$header-height: 52px;
|
||||
$navbar-width: 250px;
|
||||
|
||||
.Layout {
|
||||
min-width: 1200px;
|
||||
|
||||
&__header {
|
||||
box-shadow: 0 0.46875rem 2.1875rem rgba(4,9,20,0.03),
|
||||
0 0.9375rem 1.40625rem rgba(4,9,20,0.03),
|
||||
0 0.25rem 0.53125rem rgba(4,9,20,0.05),
|
||||
0 0.125rem 0.1875rem rgba(4,9,20,0.03);
|
||||
z-index: 31;
|
||||
}
|
||||
|
||||
&__container {
|
||||
margin-top: $header-height;
|
||||
margin-left: $navbar-width;
|
||||
position: relative;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
&__sidebar{
|
||||
width: $navbar-width;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 7px 0 60px rgba(0,0,0,0.05);
|
||||
position: fixed;
|
||||
top: $header-height;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
padding: 20px 20px;
|
||||
overflow-y: scroll;
|
||||
transition: width .25s,opacity .25s,transform .25s,-webkit-transform .25s;
|
||||
|
||||
&Overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
height: 120vh;
|
||||
z-index: 99;
|
||||
display: block;
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
-webkit-transition: all .5s ease;
|
||||
transition: all .5s ease;
|
||||
}
|
||||
}
|
||||
|
||||
&__alerts {
|
||||
max-width: 40%;
|
||||
width: 500px;
|
||||
position: fixed;
|
||||
bottom: 15px;
|
||||
right: 15px;
|
||||
z-index: 1000;
|
||||
}
|
||||
}
|
||||
|
||||
.react-datepicker-wrapper {
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
.react-datepicker-popper {
|
||||
z-index: 30 !important;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1023px) {
|
||||
.Layout {
|
||||
min-width: initial;
|
||||
|
||||
&__container {
|
||||
margin-left: initial;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
&__sidebar {
|
||||
left: -$navbar-width;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
&__alerts {
|
||||
max-width: initial;
|
||||
}
|
||||
|
||||
&--sidebarVisible {
|
||||
.Layout__sidebar {
|
||||
transform: translate3d($navbar-width,0,0);
|
||||
|
||||
&Overlay {
|
||||
background-color: rgba(34,41,47,.5);
|
||||
left: 0;
|
||||
right: 0;
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
177
kafka-ui-react-app/src/components/App.styled.ts
Normal file
177
kafka-ui-react-app/src/components/App.styled.ts
Normal file
|
@ -0,0 +1,177 @@
|
|||
import styled, { css } from 'styled-components';
|
||||
|
||||
export const Layout = styled.div`
|
||||
min-width: 1200px;
|
||||
|
||||
@media screen and (max-width: 1023px) {
|
||||
min-width: initial;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Container = styled.main(
|
||||
({ theme }) => css`
|
||||
margin-top: ${theme.layout.navBarHeight};
|
||||
margin-left: ${theme.layout.navBarWidth};
|
||||
position: relative;
|
||||
z-index: 20;
|
||||
|
||||
@media screen and (max-width: 1023px) {
|
||||
margin-left: initial;
|
||||
}
|
||||
`
|
||||
);
|
||||
|
||||
export const Sidebar = styled.div<{ $visible: boolean }>(
|
||||
({ theme, $visible }) => css`
|
||||
width: ${theme.layout.navBarWidth};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-right: 1px solid #e7e7e7;
|
||||
position: fixed;
|
||||
top: ${theme.layout.navBarHeight};
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
padding: 8px 16px;
|
||||
overflow-y: scroll;
|
||||
transition: width 0.25s, opacity 0.25s, transform 0.25s,
|
||||
-webkit-transform 0.25s;
|
||||
background: ${theme.menuStyles.backgroundColor.normal};
|
||||
@media screen and (max-width: 1023px) {
|
||||
${$visible &&
|
||||
`transform: translate3d(${theme.layout.navBarWidth}, 0, 0)`};
|
||||
left: -${theme.layout.navBarWidth};
|
||||
z-index: 100;
|
||||
}
|
||||
`
|
||||
);
|
||||
|
||||
export const Overlay = styled.div<{ $visible: boolean }>(
|
||||
({ theme, $visible }) => css`
|
||||
height: calc(100vh - ${theme.layout.navBarHeight});
|
||||
z-index: 99;
|
||||
visibility: 'hidden';
|
||||
opacity: 0;
|
||||
-webkit-transition: all 0.5s ease;
|
||||
transition: all 0.5s ease;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
${$visible &&
|
||||
css`
|
||||
@media screen and (max-width: 1023px) {
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
visibility: 'visible';
|
||||
opacity: 1;
|
||||
background-color: rgba(34, 41, 47, 0.5);
|
||||
}
|
||||
`}
|
||||
`
|
||||
);
|
||||
|
||||
export const Navbar = styled.nav(
|
||||
({ theme }) => css`
|
||||
border-bottom: 1px solid #e7e7e7;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 30;
|
||||
background-color: ${theme.menuStyles.backgroundColor.normal};
|
||||
min-height: 3.25rem;
|
||||
`
|
||||
);
|
||||
|
||||
export const NavbarBrand = styled.div`
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: stretch;
|
||||
min-height: 3.25rem;
|
||||
`;
|
||||
|
||||
export const NavbarItem = styled.div`
|
||||
display: flex;
|
||||
position: relative;
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
line-height: 1.5;
|
||||
padding: 0.5rem 0.75rem;
|
||||
`;
|
||||
|
||||
export const NavbarBurger = styled.div(
|
||||
({ theme }) => css`
|
||||
display: block;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
height: 3.25rem;
|
||||
width: 3.25rem;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
&:hover {
|
||||
background-color: ${theme.menuStyles.backgroundColor.hover};
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1024px) {
|
||||
display: none;
|
||||
}
|
||||
`
|
||||
);
|
||||
|
||||
export const Span = styled.span(
|
||||
({ theme }) => css`
|
||||
display: block;
|
||||
position: absolute;
|
||||
background: ${theme.menuStyles.color.active};
|
||||
height: 1px;
|
||||
left: calc(50% - 8px);
|
||||
transform-origin: center;
|
||||
transition-duration: 86ms;
|
||||
transition-property: background-color, opacity, transform, -webkit-transform;
|
||||
transition-timing-function: ease-out;
|
||||
width: 16px;
|
||||
|
||||
&:first-child {
|
||||
top: calc(50% - 6px);
|
||||
}
|
||||
&:nth-child(2) {
|
||||
top: calc(50% - 1px);
|
||||
}
|
||||
&:nth-child(3) {
|
||||
top: calc(50% + 4px);
|
||||
}
|
||||
`
|
||||
);
|
||||
|
||||
export const Hyperlink = styled.a(
|
||||
({ theme }) => css`
|
||||
display: flex;
|
||||
position: relative;
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
color: ${theme.menuStyles.color.active};
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
line-height: 1.5;
|
||||
padding: 0.5rem 0.75rem;
|
||||
text-decoration: none;
|
||||
word-break: break-word;
|
||||
`
|
||||
);
|
||||
|
||||
export const AlertsContainer = styled.div`
|
||||
max-width: 40%;
|
||||
width: 500px;
|
||||
position: fixed;
|
||||
bottom: 15px;
|
||||
left: 15px;
|
||||
z-index: 1000;
|
||||
|
||||
@media screen and (max-width: 1023px) {
|
||||
max-width: initial;
|
||||
}
|
||||
`;
|
|
@ -1,30 +1,28 @@
|
|||
import React from 'react';
|
||||
import cx from 'classnames';
|
||||
import { Cluster } from 'generated-sources';
|
||||
import { Switch, Route, useLocation } from 'react-router-dom';
|
||||
import { GIT_TAG, GIT_COMMIT } from 'lib/constants';
|
||||
import { Alerts } from 'redux/interfaces';
|
||||
import Nav from 'components/Nav/Nav';
|
||||
import PageLoader from 'components/common/PageLoader/PageLoader';
|
||||
import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb';
|
||||
import Dashboard from 'components/Dashboard/Dashboard';
|
||||
import ClusterPage from 'components/Cluster/Cluster';
|
||||
import Version from 'components/Version/Version';
|
||||
import Alert from 'components/Alert/Alert';
|
||||
import 'components/App.scss';
|
||||
import Alerts from 'components/Alerts/Alerts';
|
||||
import { ThemeProvider } from 'styled-components';
|
||||
import theme from 'theme/theme';
|
||||
import { useAppDispatch, useAppSelector } from 'lib/hooks/redux';
|
||||
import {
|
||||
fetchClusters,
|
||||
getClusterList,
|
||||
getAreClustersFulfilled,
|
||||
} from 'redux/reducers/clusters/clustersSlice';
|
||||
|
||||
export interface AppProps {
|
||||
isClusterListFetched?: boolean;
|
||||
alerts: Alerts;
|
||||
clusters: Cluster[];
|
||||
fetchClustersList: () => void;
|
||||
}
|
||||
import * as S from './App.styled';
|
||||
|
||||
const App: React.FC<AppProps> = ({
|
||||
isClusterListFetched,
|
||||
alerts,
|
||||
clusters,
|
||||
fetchClustersList,
|
||||
}) => {
|
||||
const App: React.FC = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const areClustersFulfilled = useAppSelector(getAreClustersFulfilled);
|
||||
const clusters = useAppSelector(getClusterList);
|
||||
const [isSidebarVisible, setIsSidebarVisible] = React.useState(false);
|
||||
|
||||
const onBurgerClick = React.useCallback(
|
||||
|
@ -41,85 +39,72 @@ const App: React.FC<AppProps> = ({
|
|||
}, [location]);
|
||||
|
||||
React.useEffect(() => {
|
||||
fetchClustersList();
|
||||
}, [fetchClustersList]);
|
||||
dispatch(fetchClusters());
|
||||
}, [fetchClusters]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx('Layout', { 'Layout--sidebarVisible': isSidebarVisible })}
|
||||
>
|
||||
<nav
|
||||
className="navbar is-fixed-top is-white Layout__header"
|
||||
role="navigation"
|
||||
aria-label="main navigation"
|
||||
>
|
||||
<div className="navbar-brand">
|
||||
<div
|
||||
className={cx('navbar-burger', 'ml-0', {
|
||||
'is-active': isSidebarVisible,
|
||||
})}
|
||||
onClick={onBurgerClick}
|
||||
onKeyDown={onBurgerClick}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
<span />
|
||||
<span />
|
||||
<span />
|
||||
</div>
|
||||
<ThemeProvider theme={theme}>
|
||||
<S.Layout>
|
||||
<S.Navbar role="navigation" aria-label="Page Header">
|
||||
<S.NavbarBrand>
|
||||
<S.NavbarBurger
|
||||
onClick={onBurgerClick}
|
||||
onKeyDown={onBurgerClick}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
<S.Span role="separator" />
|
||||
<S.Span role="separator" />
|
||||
<S.Span role="separator" />
|
||||
</S.NavbarBurger>
|
||||
|
||||
<a className="navbar-item title is-5 is-marginless" href="/ui">
|
||||
UI for Apache Kafka
|
||||
</a>
|
||||
<S.Hyperlink href="/ui">UI for Apache Kafka</S.Hyperlink>
|
||||
|
||||
<div className="navbar-item">
|
||||
<Version tag={GIT_TAG} commit={GIT_COMMIT} />
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<S.NavbarItem>
|
||||
<Version tag={GIT_TAG} commit={GIT_COMMIT} />
|
||||
</S.NavbarItem>
|
||||
</S.NavbarBrand>
|
||||
</S.Navbar>
|
||||
|
||||
<main className="Layout__container">
|
||||
<div className="Layout__sidebar has-shadow has-background-white">
|
||||
<Nav
|
||||
clusters={clusters}
|
||||
isClusterListFetched={isClusterListFetched}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="Layout__sidebarOverlay is-overlay"
|
||||
onClick={closeSidebar}
|
||||
onKeyDown={closeSidebar}
|
||||
tabIndex={-1}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{isClusterListFetched ? (
|
||||
<Switch>
|
||||
<Route
|
||||
exact
|
||||
path={['/', '/ui', '/ui/clusters']}
|
||||
component={Dashboard}
|
||||
<S.Container>
|
||||
<S.Sidebar aria-label="Sidebar" $visible={isSidebarVisible}>
|
||||
<Nav
|
||||
clusters={clusters}
|
||||
areClustersFulfilled={areClustersFulfilled}
|
||||
/>
|
||||
<Route path="/ui/clusters/:clusterName" component={ClusterPage} />
|
||||
</Switch>
|
||||
) : (
|
||||
<PageLoader fullHeight />
|
||||
)}
|
||||
</main>
|
||||
|
||||
<div className="Layout__alerts">
|
||||
{alerts.map(({ id, type, title, message, response, createdAt }) => (
|
||||
<Alert
|
||||
key={id}
|
||||
id={id}
|
||||
type={type}
|
||||
title={title}
|
||||
message={message}
|
||||
response={response}
|
||||
createdAt={createdAt}
|
||||
</S.Sidebar>
|
||||
<S.Overlay
|
||||
$visible={isSidebarVisible}
|
||||
onClick={closeSidebar}
|
||||
onKeyDown={closeSidebar}
|
||||
tabIndex={-1}
|
||||
aria-hidden="true"
|
||||
aria-label="Overlay"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{areClustersFulfilled ? (
|
||||
<>
|
||||
<Breadcrumb />
|
||||
<Switch>
|
||||
<Route
|
||||
exact
|
||||
path={['/', '/ui', '/ui/clusters']}
|
||||
component={Dashboard}
|
||||
/>
|
||||
<Route
|
||||
path="/ui/clusters/:clusterName"
|
||||
component={ClusterPage}
|
||||
/>
|
||||
</Switch>
|
||||
</>
|
||||
) : (
|
||||
<PageLoader />
|
||||
)}
|
||||
</S.Container>
|
||||
<S.AlertsContainer role="toolbar">
|
||||
<Alerts />
|
||||
</S.AlertsContainer>
|
||||
</S.Layout>
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { fetchClustersList } from 'redux/actions';
|
||||
import {
|
||||
getClusterList,
|
||||
getIsClusterListFetched,
|
||||
} from 'redux/reducers/clusters/selectors';
|
||||
import { getAlerts } from 'redux/reducers/alerts/selectors';
|
||||
import { RootState } from 'redux/interfaces';
|
||||
import App from 'components/App';
|
||||
|
||||
const mapStateToProps = (state: RootState) => ({
|
||||
isClusterListFetched: getIsClusterListFetched(state),
|
||||
alerts: getAlerts(state),
|
||||
clusters: getClusterList(state),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchClustersList,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(App);
|
|
@ -1,40 +1,38 @@
|
|||
import React from 'react';
|
||||
import { ClusterName, ZooKeeperStatus } from 'redux/interfaces';
|
||||
import { ClusterStats } from 'generated-sources';
|
||||
import useInterval from 'lib/hooks/useInterval';
|
||||
import cx from 'classnames';
|
||||
import MetricsWrapper from 'components/common/Dashboard/MetricsWrapper';
|
||||
import Indicator from 'components/common/Dashboard/Indicator';
|
||||
import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb';
|
||||
import BytesFormatted from 'components/common/BytesFormatted/BytesFormatted';
|
||||
import { useParams } from 'react-router';
|
||||
|
||||
interface Props extends ClusterStats {
|
||||
isFetched: boolean;
|
||||
fetchClusterStats: (clusterName: ClusterName) => void;
|
||||
fetchBrokers: (clusterName: ClusterName) => void;
|
||||
}
|
||||
|
||||
const Brokers: React.FC<Props> = ({
|
||||
brokerCount,
|
||||
activeControllers,
|
||||
zooKeeperStatus,
|
||||
onlinePartitionCount,
|
||||
offlinePartitionCount,
|
||||
inSyncReplicasCount,
|
||||
outOfSyncReplicasCount,
|
||||
underReplicatedPartitionCount,
|
||||
diskUsage,
|
||||
import TagStyled from 'components/common/Tag/Tag.styled';
|
||||
import TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeaderCell';
|
||||
import { Table } from 'components/common/table/Table/Table.styled';
|
||||
import PageHeading from 'components/common/PageHeading/PageHeading';
|
||||
import * as Metrics from 'components/common/Metrics';
|
||||
import { useAppDispatch, useAppSelector } from 'lib/hooks/redux';
|
||||
import {
|
||||
fetchClusterStats,
|
||||
fetchBrokers,
|
||||
version,
|
||||
}) => {
|
||||
selectStats,
|
||||
} from 'redux/reducers/brokers/brokersSlice';
|
||||
|
||||
const Brokers: React.FC = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { clusterName } = useParams<{ clusterName: ClusterName }>();
|
||||
const {
|
||||
brokerCount,
|
||||
activeControllers,
|
||||
zooKeeperStatus,
|
||||
onlinePartitionCount,
|
||||
offlinePartitionCount,
|
||||
inSyncReplicasCount,
|
||||
outOfSyncReplicasCount,
|
||||
underReplicatedPartitionCount,
|
||||
diskUsage,
|
||||
version,
|
||||
} = useAppSelector(selectStats);
|
||||
|
||||
React.useEffect(() => {
|
||||
fetchClusterStats(clusterName);
|
||||
fetchBrokers(clusterName);
|
||||
}, [fetchClusterStats, fetchBrokers, clusterName]);
|
||||
dispatch(fetchClusterStats(clusterName));
|
||||
}, [fetchClusterStats, clusterName]);
|
||||
|
||||
useInterval(() => {
|
||||
fetchClusterStats(clusterName);
|
||||
|
@ -43,61 +41,67 @@ const Brokers: React.FC<Props> = ({
|
|||
const zkOnline = zooKeeperStatus === ZooKeeperStatus.online;
|
||||
|
||||
return (
|
||||
<div className="section">
|
||||
<Breadcrumb>Brokers overview</Breadcrumb>
|
||||
<MetricsWrapper title="Uptime">
|
||||
<Indicator className="is-one-third" label="Total Brokers">
|
||||
{brokerCount}
|
||||
</Indicator>
|
||||
<Indicator className="is-one-third" label="Active Controllers">
|
||||
{activeControllers}
|
||||
</Indicator>
|
||||
<Indicator className="is-one-third" label="Zookeeper Status">
|
||||
<span className={cx('tag', zkOnline ? 'is-success' : 'is-danger')}>
|
||||
{zkOnline ? 'Online' : 'Offline'}
|
||||
</span>
|
||||
</Indicator>
|
||||
<Indicator className="is-one-third" label="Version">
|
||||
{version}
|
||||
</Indicator>
|
||||
</MetricsWrapper>
|
||||
<MetricsWrapper title="Partitions">
|
||||
<Indicator label="Online">
|
||||
<span
|
||||
className={cx({ 'has-text-danger': offlinePartitionCount !== 0 })}
|
||||
>
|
||||
{onlinePartitionCount}
|
||||
</span>
|
||||
<span className="subtitle has-text-weight-light">
|
||||
{' '}
|
||||
of
|
||||
{(onlinePartitionCount || 0) + (offlinePartitionCount || 0)}
|
||||
</span>
|
||||
</Indicator>
|
||||
<Indicator label="URP" title="Under replicated partitions">
|
||||
{underReplicatedPartitionCount}
|
||||
</Indicator>
|
||||
<Indicator label="In Sync Replicas">{inSyncReplicasCount}</Indicator>
|
||||
<Indicator label="Out of Sync Replicas">
|
||||
{outOfSyncReplicasCount}
|
||||
</Indicator>
|
||||
</MetricsWrapper>
|
||||
<MetricsWrapper multiline title="Disk Usage">
|
||||
{diskUsage?.map((brokerDiskUsage) => (
|
||||
<React.Fragment key={brokerDiskUsage.brokerId}>
|
||||
<Indicator className="is-one-third" label="Broker">
|
||||
{brokerDiskUsage.brokerId}
|
||||
</Indicator>
|
||||
<Indicator className="is-one-third" label="Segment Size" title="">
|
||||
<BytesFormatted value={brokerDiskUsage.segmentSize} />
|
||||
</Indicator>
|
||||
<Indicator className="is-one-third" label="Segment count">
|
||||
{brokerDiskUsage.segmentCount}
|
||||
</Indicator>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</MetricsWrapper>
|
||||
</div>
|
||||
<>
|
||||
<PageHeading text="Brokers" />
|
||||
<Metrics.Wrapper>
|
||||
<Metrics.Section title="Uptime">
|
||||
<Metrics.Indicator label="Total Brokers">
|
||||
{brokerCount}
|
||||
</Metrics.Indicator>
|
||||
<Metrics.Indicator label="Active Controllers">
|
||||
{activeControllers}
|
||||
</Metrics.Indicator>
|
||||
<Metrics.Indicator label="Zookeeper Status">
|
||||
<TagStyled color={zkOnline ? 'green' : 'gray'}>
|
||||
{zkOnline ? 'online' : 'offline'}
|
||||
</TagStyled>
|
||||
</Metrics.Indicator>
|
||||
<Metrics.Indicator label="Version">{version}</Metrics.Indicator>
|
||||
</Metrics.Section>
|
||||
<Metrics.Section title="Partitions">
|
||||
<Metrics.Indicator label="Online" isAlert>
|
||||
{offlinePartitionCount && offlinePartitionCount > 0 ? (
|
||||
<Metrics.RedText>{onlinePartitionCount}</Metrics.RedText>
|
||||
) : (
|
||||
onlinePartitionCount
|
||||
)}
|
||||
<Metrics.LightText>
|
||||
{' '}
|
||||
of {(onlinePartitionCount || 0) + (offlinePartitionCount || 0)}
|
||||
</Metrics.LightText>
|
||||
</Metrics.Indicator>
|
||||
<Metrics.Indicator label="URP" title="Under replicated partitions">
|
||||
{underReplicatedPartitionCount}
|
||||
</Metrics.Indicator>
|
||||
<Metrics.Indicator label="In Sync Replicas">
|
||||
{inSyncReplicasCount}
|
||||
</Metrics.Indicator>
|
||||
<Metrics.Indicator label="Out of Sync Replicas">
|
||||
{outOfSyncReplicasCount}
|
||||
</Metrics.Indicator>
|
||||
</Metrics.Section>
|
||||
</Metrics.Wrapper>
|
||||
<Table isFullwidth>
|
||||
<thead>
|
||||
<tr>
|
||||
<TableHeaderCell title="Broker" />
|
||||
<TableHeaderCell title="Segment size (Mb)" />
|
||||
<TableHeaderCell title="Segment Count" />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{diskUsage?.map(({ brokerId, segmentSize, segmentCount }) => (
|
||||
<tr key={brokerId}>
|
||||
<td>{brokerId}</td>
|
||||
<td>
|
||||
<BytesFormatted value={segmentSize} />
|
||||
</td>
|
||||
<td>{segmentCount}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -1,38 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { fetchClusterStats, fetchBrokers } from 'redux/actions';
|
||||
import { RootState } from 'redux/interfaces';
|
||||
import {
|
||||
getIsBrokerListFetched,
|
||||
getBrokerCount,
|
||||
getZooKeeperStatus,
|
||||
getActiveControllers,
|
||||
getOnlinePartitionCount,
|
||||
getOfflinePartitionCount,
|
||||
getInSyncReplicasCount,
|
||||
getOutOfSyncReplicasCount,
|
||||
getUnderReplicatedPartitionCount,
|
||||
getDiskUsage,
|
||||
getVersion,
|
||||
} from 'redux/reducers/brokers/selectors';
|
||||
import Brokers from 'components/Brokers/Brokers';
|
||||
|
||||
const mapStateToProps = (state: RootState) => ({
|
||||
isFetched: getIsBrokerListFetched(state),
|
||||
brokerCount: getBrokerCount(state),
|
||||
zooKeeperStatus: getZooKeeperStatus(state),
|
||||
activeControllers: getActiveControllers(state),
|
||||
onlinePartitionCount: getOnlinePartitionCount(state),
|
||||
offlinePartitionCount: getOfflinePartitionCount(state),
|
||||
inSyncReplicasCount: getInSyncReplicasCount(state),
|
||||
outOfSyncReplicasCount: getOutOfSyncReplicasCount(state),
|
||||
underReplicatedPartitionCount: getUnderReplicatedPartitionCount(state),
|
||||
diskUsage: getDiskUsage(state),
|
||||
version: getVersion(state),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchClusterStats,
|
||||
fetchBrokers,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Brokers);
|
|
@ -1,90 +1,54 @@
|
|||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import Brokers from 'components/Brokers/Brokers';
|
||||
import { ClusterName } from 'redux/interfaces';
|
||||
import { StaticRouter } from 'react-router';
|
||||
import { ClusterStats } from 'generated-sources';
|
||||
|
||||
interface Props extends ClusterStats {
|
||||
isFetched: boolean;
|
||||
fetchClusterStats: (clusterName: ClusterName) => void;
|
||||
fetchBrokers: (clusterName: ClusterName) => void;
|
||||
}
|
||||
import { render } from 'lib/testHelpers';
|
||||
import { screen, waitFor } from '@testing-library/dom';
|
||||
import { Route, StaticRouter } from 'react-router';
|
||||
import { clusterBrokersPath } from 'lib/paths';
|
||||
import fetchMock from 'fetch-mock';
|
||||
import { clusterStatsPayload } from 'redux/reducers/brokers/__test__/fixtures';
|
||||
|
||||
describe('Brokers Component', () => {
|
||||
const pathname = `ui/clusters/local/brokers`;
|
||||
afterEach(() => fetchMock.reset());
|
||||
|
||||
describe('Brokers Empty', () => {
|
||||
const setupEmptyComponent = (props: Partial<Props> = {}) => (
|
||||
<StaticRouter location={{ pathname }} context={{}}>
|
||||
<Brokers
|
||||
brokerCount={0}
|
||||
activeControllers={0}
|
||||
zooKeeperStatus={0}
|
||||
onlinePartitionCount={0}
|
||||
offlinePartitionCount={0}
|
||||
inSyncReplicasCount={0}
|
||||
outOfSyncReplicasCount={0}
|
||||
underReplicatedPartitionCount={0}
|
||||
version="1"
|
||||
fetchClusterStats={jest.fn()}
|
||||
fetchBrokers={jest.fn()}
|
||||
diskUsage={undefined}
|
||||
isFetched={false}
|
||||
{...props}
|
||||
/>
|
||||
const clusterName = 'local';
|
||||
const renderComponent = () =>
|
||||
render(
|
||||
<StaticRouter
|
||||
location={{
|
||||
pathname: clusterBrokersPath(clusterName),
|
||||
}}
|
||||
>
|
||||
<Route path={clusterBrokersPath(':clusterName')}>
|
||||
<Brokers />
|
||||
</Route>
|
||||
</StaticRouter>
|
||||
);
|
||||
it('renders section', () => {
|
||||
const component = mount(setupEmptyComponent());
|
||||
expect(component.exists('.section')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('renders section with is-danger selector', () => {
|
||||
const component = mount(setupEmptyComponent());
|
||||
expect(component.exists('.is-danger')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('matches Brokers Empty snapshot', () => {
|
||||
expect(mount(setupEmptyComponent())).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Brokers', () => {
|
||||
const setupComponent = (props: Partial<Props> = {}) => (
|
||||
<StaticRouter location={{ pathname }} context={{}}>
|
||||
<Brokers
|
||||
brokerCount={1}
|
||||
activeControllers={1}
|
||||
zooKeeperStatus={1}
|
||||
onlinePartitionCount={64}
|
||||
offlinePartitionCount={0}
|
||||
inSyncReplicasCount={64}
|
||||
outOfSyncReplicasCount={0}
|
||||
underReplicatedPartitionCount={0}
|
||||
version="1"
|
||||
fetchClusterStats={jest.fn()}
|
||||
fetchBrokers={jest.fn()}
|
||||
diskUsage={[
|
||||
{
|
||||
brokerId: 1,
|
||||
segmentCount: 64,
|
||||
segmentSize: 60718,
|
||||
},
|
||||
]}
|
||||
isFetched
|
||||
{...props}
|
||||
/>
|
||||
</StaticRouter>
|
||||
);
|
||||
|
||||
it('renders section with is-success selector', () => {
|
||||
const component = mount(setupComponent());
|
||||
expect(component.exists('.is-success')).toBeTruthy();
|
||||
it('renders', async () => {
|
||||
const mock = fetchMock.getOnce(
|
||||
`/api/clusters/${clusterName}/stats`,
|
||||
clusterStatsPayload
|
||||
);
|
||||
renderComponent();
|
||||
await waitFor(() => expect(mock.called()).toBeTruthy());
|
||||
expect(screen.getByRole('table')).toBeInTheDocument();
|
||||
const rows = screen.getAllByRole('row');
|
||||
expect(rows.length).toEqual(3);
|
||||
});
|
||||
|
||||
it('matches snapshot', () => {
|
||||
expect(mount(setupComponent())).toMatchSnapshot();
|
||||
it('shows warning when offlinePartitionCount > 0', async () => {
|
||||
const mock = fetchMock.getOnce(`/api/clusters/${clusterName}/stats`, {
|
||||
...clusterStatsPayload,
|
||||
offlinePartitionCount: 1345,
|
||||
});
|
||||
renderComponent();
|
||||
await waitFor(() => expect(mock.called()).toBeTruthy());
|
||||
const onlineWidget = screen.getByText(
|
||||
clusterStatsPayload.onlinePartitionCount
|
||||
);
|
||||
expect(onlineWidget).toBeInTheDocument();
|
||||
expect(onlineWidget).toHaveStyle({ color: '#E51A1A' });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
import React from 'react';
|
||||
import { containerRendersView } from 'lib/testHelpers';
|
||||
import Brokers from 'components/Brokers/Brokers';
|
||||
import BrokersContainer from 'components/Brokers/BrokersContainer';
|
||||
|
||||
describe('BrokersContainer', () => {
|
||||
containerRendersView(<BrokersContainer />, Brokers);
|
||||
});
|
|
@ -1,779 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Brokers Component Brokers Empty matches Brokers Empty snapshot 1`] = `
|
||||
<StaticRouter
|
||||
context={Object {}}
|
||||
location={
|
||||
Object {
|
||||
"pathname": "ui/clusters/local/brokers",
|
||||
}
|
||||
}
|
||||
>
|
||||
<Router
|
||||
history={
|
||||
Object {
|
||||
"action": "POP",
|
||||
"block": [Function],
|
||||
"createHref": [Function],
|
||||
"go": [Function],
|
||||
"goBack": [Function],
|
||||
"goForward": [Function],
|
||||
"listen": [Function],
|
||||
"location": Object {
|
||||
"hash": "",
|
||||
"pathname": "ui/clusters/local/brokers",
|
||||
"search": "",
|
||||
},
|
||||
"push": [Function],
|
||||
"replace": [Function],
|
||||
}
|
||||
}
|
||||
staticContext={Object {}}
|
||||
>
|
||||
<Brokers
|
||||
activeControllers={0}
|
||||
brokerCount={0}
|
||||
fetchBrokers={
|
||||
[MockFunction] {
|
||||
"calls": Array [
|
||||
Array [
|
||||
undefined,
|
||||
],
|
||||
],
|
||||
"results": Array [
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": undefined,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
fetchClusterStats={
|
||||
[MockFunction] {
|
||||
"calls": Array [
|
||||
Array [
|
||||
undefined,
|
||||
],
|
||||
],
|
||||
"results": Array [
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": undefined,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
inSyncReplicasCount={0}
|
||||
isFetched={false}
|
||||
offlinePartitionCount={0}
|
||||
onlinePartitionCount={0}
|
||||
outOfSyncReplicasCount={0}
|
||||
underReplicatedPartitionCount={0}
|
||||
version="1"
|
||||
zooKeeperStatus={0}
|
||||
>
|
||||
<div
|
||||
className="section"
|
||||
>
|
||||
<Breadcrumb>
|
||||
<nav
|
||||
aria-label="breadcrumbs"
|
||||
className="breadcrumb"
|
||||
>
|
||||
<ul>
|
||||
<li
|
||||
className="is-active"
|
||||
>
|
||||
<span
|
||||
className=""
|
||||
>
|
||||
Brokers overview
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</Breadcrumb>
|
||||
<MetricsWrapper
|
||||
title="Uptime"
|
||||
>
|
||||
<div
|
||||
className="box"
|
||||
>
|
||||
<h5
|
||||
className="subtitle is-6"
|
||||
>
|
||||
Uptime
|
||||
</h5>
|
||||
<div
|
||||
className="level"
|
||||
>
|
||||
<Indicator
|
||||
className="is-one-third"
|
||||
label="Total Brokers"
|
||||
>
|
||||
<div
|
||||
className="level-item is-one-third"
|
||||
>
|
||||
<div
|
||||
title="Total Brokers"
|
||||
>
|
||||
<p
|
||||
className="heading"
|
||||
>
|
||||
Total Brokers
|
||||
</p>
|
||||
<p
|
||||
className="title has-text-centered"
|
||||
>
|
||||
0
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Indicator>
|
||||
<Indicator
|
||||
className="is-one-third"
|
||||
label="Active Controllers"
|
||||
>
|
||||
<div
|
||||
className="level-item is-one-third"
|
||||
>
|
||||
<div
|
||||
title="Active Controllers"
|
||||
>
|
||||
<p
|
||||
className="heading"
|
||||
>
|
||||
Active Controllers
|
||||
</p>
|
||||
<p
|
||||
className="title has-text-centered"
|
||||
>
|
||||
0
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Indicator>
|
||||
<Indicator
|
||||
className="is-one-third"
|
||||
label="Zookeeper Status"
|
||||
>
|
||||
<div
|
||||
className="level-item is-one-third"
|
||||
>
|
||||
<div
|
||||
title="Zookeeper Status"
|
||||
>
|
||||
<p
|
||||
className="heading"
|
||||
>
|
||||
Zookeeper Status
|
||||
</p>
|
||||
<p
|
||||
className="title has-text-centered"
|
||||
>
|
||||
<span
|
||||
className="tag is-danger"
|
||||
>
|
||||
Offline
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Indicator>
|
||||
<Indicator
|
||||
className="is-one-third"
|
||||
label="Version"
|
||||
>
|
||||
<div
|
||||
className="level-item is-one-third"
|
||||
>
|
||||
<div
|
||||
title="Version"
|
||||
>
|
||||
<p
|
||||
className="heading"
|
||||
>
|
||||
Version
|
||||
</p>
|
||||
<p
|
||||
className="title has-text-centered"
|
||||
>
|
||||
1
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Indicator>
|
||||
</div>
|
||||
</div>
|
||||
</MetricsWrapper>
|
||||
<MetricsWrapper
|
||||
title="Partitions"
|
||||
>
|
||||
<div
|
||||
className="box"
|
||||
>
|
||||
<h5
|
||||
className="subtitle is-6"
|
||||
>
|
||||
Partitions
|
||||
</h5>
|
||||
<div
|
||||
className="level"
|
||||
>
|
||||
<Indicator
|
||||
label="Online"
|
||||
>
|
||||
<div
|
||||
className="level-item"
|
||||
>
|
||||
<div
|
||||
title="Online"
|
||||
>
|
||||
<p
|
||||
className="heading"
|
||||
>
|
||||
Online
|
||||
</p>
|
||||
<p
|
||||
className="title has-text-centered"
|
||||
>
|
||||
<span
|
||||
className=""
|
||||
>
|
||||
0
|
||||
</span>
|
||||
<span
|
||||
className="subtitle has-text-weight-light"
|
||||
>
|
||||
|
||||
of
|
||||
0
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Indicator>
|
||||
<Indicator
|
||||
label="URP"
|
||||
title="Under replicated partitions"
|
||||
>
|
||||
<div
|
||||
className="level-item"
|
||||
>
|
||||
<div
|
||||
title="Under replicated partitions"
|
||||
>
|
||||
<p
|
||||
className="heading"
|
||||
>
|
||||
URP
|
||||
</p>
|
||||
<p
|
||||
className="title has-text-centered"
|
||||
>
|
||||
0
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Indicator>
|
||||
<Indicator
|
||||
label="In Sync Replicas"
|
||||
>
|
||||
<div
|
||||
className="level-item"
|
||||
>
|
||||
<div
|
||||
title="In Sync Replicas"
|
||||
>
|
||||
<p
|
||||
className="heading"
|
||||
>
|
||||
In Sync Replicas
|
||||
</p>
|
||||
<p
|
||||
className="title has-text-centered"
|
||||
>
|
||||
0
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Indicator>
|
||||
<Indicator
|
||||
label="Out of Sync Replicas"
|
||||
>
|
||||
<div
|
||||
className="level-item"
|
||||
>
|
||||
<div
|
||||
title="Out of Sync Replicas"
|
||||
>
|
||||
<p
|
||||
className="heading"
|
||||
>
|
||||
Out of Sync Replicas
|
||||
</p>
|
||||
<p
|
||||
className="title has-text-centered"
|
||||
>
|
||||
0
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Indicator>
|
||||
</div>
|
||||
</div>
|
||||
</MetricsWrapper>
|
||||
<MetricsWrapper
|
||||
multiline={true}
|
||||
title="Disk Usage"
|
||||
>
|
||||
<div
|
||||
className="box"
|
||||
>
|
||||
<h5
|
||||
className="subtitle is-6"
|
||||
>
|
||||
Disk Usage
|
||||
</h5>
|
||||
<div
|
||||
className="level level-multiline"
|
||||
/>
|
||||
</div>
|
||||
</MetricsWrapper>
|
||||
</div>
|
||||
</Brokers>
|
||||
</Router>
|
||||
</StaticRouter>
|
||||
`;
|
||||
|
||||
exports[`Brokers Component Brokers matches snapshot 1`] = `
|
||||
<StaticRouter
|
||||
context={Object {}}
|
||||
location={
|
||||
Object {
|
||||
"pathname": "ui/clusters/local/brokers",
|
||||
}
|
||||
}
|
||||
>
|
||||
<Router
|
||||
history={
|
||||
Object {
|
||||
"action": "POP",
|
||||
"block": [Function],
|
||||
"createHref": [Function],
|
||||
"go": [Function],
|
||||
"goBack": [Function],
|
||||
"goForward": [Function],
|
||||
"listen": [Function],
|
||||
"location": Object {
|
||||
"hash": "",
|
||||
"pathname": "ui/clusters/local/brokers",
|
||||
"search": "",
|
||||
},
|
||||
"push": [Function],
|
||||
"replace": [Function],
|
||||
}
|
||||
}
|
||||
staticContext={Object {}}
|
||||
>
|
||||
<Brokers
|
||||
activeControllers={1}
|
||||
brokerCount={1}
|
||||
diskUsage={
|
||||
Array [
|
||||
Object {
|
||||
"brokerId": 1,
|
||||
"segmentCount": 64,
|
||||
"segmentSize": 60718,
|
||||
},
|
||||
]
|
||||
}
|
||||
fetchBrokers={
|
||||
[MockFunction] {
|
||||
"calls": Array [
|
||||
Array [
|
||||
undefined,
|
||||
],
|
||||
],
|
||||
"results": Array [
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": undefined,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
fetchClusterStats={
|
||||
[MockFunction] {
|
||||
"calls": Array [
|
||||
Array [
|
||||
undefined,
|
||||
],
|
||||
],
|
||||
"results": Array [
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": undefined,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
inSyncReplicasCount={64}
|
||||
isFetched={true}
|
||||
offlinePartitionCount={0}
|
||||
onlinePartitionCount={64}
|
||||
outOfSyncReplicasCount={0}
|
||||
underReplicatedPartitionCount={0}
|
||||
version="1"
|
||||
zooKeeperStatus={1}
|
||||
>
|
||||
<div
|
||||
className="section"
|
||||
>
|
||||
<Breadcrumb>
|
||||
<nav
|
||||
aria-label="breadcrumbs"
|
||||
className="breadcrumb"
|
||||
>
|
||||
<ul>
|
||||
<li
|
||||
className="is-active"
|
||||
>
|
||||
<span
|
||||
className=""
|
||||
>
|
||||
Brokers overview
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</Breadcrumb>
|
||||
<MetricsWrapper
|
||||
title="Uptime"
|
||||
>
|
||||
<div
|
||||
className="box"
|
||||
>
|
||||
<h5
|
||||
className="subtitle is-6"
|
||||
>
|
||||
Uptime
|
||||
</h5>
|
||||
<div
|
||||
className="level"
|
||||
>
|
||||
<Indicator
|
||||
className="is-one-third"
|
||||
label="Total Brokers"
|
||||
>
|
||||
<div
|
||||
className="level-item is-one-third"
|
||||
>
|
||||
<div
|
||||
title="Total Brokers"
|
||||
>
|
||||
<p
|
||||
className="heading"
|
||||
>
|
||||
Total Brokers
|
||||
</p>
|
||||
<p
|
||||
className="title has-text-centered"
|
||||
>
|
||||
1
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Indicator>
|
||||
<Indicator
|
||||
className="is-one-third"
|
||||
label="Active Controllers"
|
||||
>
|
||||
<div
|
||||
className="level-item is-one-third"
|
||||
>
|
||||
<div
|
||||
title="Active Controllers"
|
||||
>
|
||||
<p
|
||||
className="heading"
|
||||
>
|
||||
Active Controllers
|
||||
</p>
|
||||
<p
|
||||
className="title has-text-centered"
|
||||
>
|
||||
1
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Indicator>
|
||||
<Indicator
|
||||
className="is-one-third"
|
||||
label="Zookeeper Status"
|
||||
>
|
||||
<div
|
||||
className="level-item is-one-third"
|
||||
>
|
||||
<div
|
||||
title="Zookeeper Status"
|
||||
>
|
||||
<p
|
||||
className="heading"
|
||||
>
|
||||
Zookeeper Status
|
||||
</p>
|
||||
<p
|
||||
className="title has-text-centered"
|
||||
>
|
||||
<span
|
||||
className="tag is-success"
|
||||
>
|
||||
Online
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Indicator>
|
||||
<Indicator
|
||||
className="is-one-third"
|
||||
label="Version"
|
||||
>
|
||||
<div
|
||||
className="level-item is-one-third"
|
||||
>
|
||||
<div
|
||||
title="Version"
|
||||
>
|
||||
<p
|
||||
className="heading"
|
||||
>
|
||||
Version
|
||||
</p>
|
||||
<p
|
||||
className="title has-text-centered"
|
||||
>
|
||||
1
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Indicator>
|
||||
</div>
|
||||
</div>
|
||||
</MetricsWrapper>
|
||||
<MetricsWrapper
|
||||
title="Partitions"
|
||||
>
|
||||
<div
|
||||
className="box"
|
||||
>
|
||||
<h5
|
||||
className="subtitle is-6"
|
||||
>
|
||||
Partitions
|
||||
</h5>
|
||||
<div
|
||||
className="level"
|
||||
>
|
||||
<Indicator
|
||||
label="Online"
|
||||
>
|
||||
<div
|
||||
className="level-item"
|
||||
>
|
||||
<div
|
||||
title="Online"
|
||||
>
|
||||
<p
|
||||
className="heading"
|
||||
>
|
||||
Online
|
||||
</p>
|
||||
<p
|
||||
className="title has-text-centered"
|
||||
>
|
||||
<span
|
||||
className=""
|
||||
>
|
||||
64
|
||||
</span>
|
||||
<span
|
||||
className="subtitle has-text-weight-light"
|
||||
>
|
||||
|
||||
of
|
||||
64
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Indicator>
|
||||
<Indicator
|
||||
label="URP"
|
||||
title="Under replicated partitions"
|
||||
>
|
||||
<div
|
||||
className="level-item"
|
||||
>
|
||||
<div
|
||||
title="Under replicated partitions"
|
||||
>
|
||||
<p
|
||||
className="heading"
|
||||
>
|
||||
URP
|
||||
</p>
|
||||
<p
|
||||
className="title has-text-centered"
|
||||
>
|
||||
0
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Indicator>
|
||||
<Indicator
|
||||
label="In Sync Replicas"
|
||||
>
|
||||
<div
|
||||
className="level-item"
|
||||
>
|
||||
<div
|
||||
title="In Sync Replicas"
|
||||
>
|
||||
<p
|
||||
className="heading"
|
||||
>
|
||||
In Sync Replicas
|
||||
</p>
|
||||
<p
|
||||
className="title has-text-centered"
|
||||
>
|
||||
64
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Indicator>
|
||||
<Indicator
|
||||
label="Out of Sync Replicas"
|
||||
>
|
||||
<div
|
||||
className="level-item"
|
||||
>
|
||||
<div
|
||||
title="Out of Sync Replicas"
|
||||
>
|
||||
<p
|
||||
className="heading"
|
||||
>
|
||||
Out of Sync Replicas
|
||||
</p>
|
||||
<p
|
||||
className="title has-text-centered"
|
||||
>
|
||||
0
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Indicator>
|
||||
</div>
|
||||
</div>
|
||||
</MetricsWrapper>
|
||||
<MetricsWrapper
|
||||
multiline={true}
|
||||
title="Disk Usage"
|
||||
>
|
||||
<div
|
||||
className="box"
|
||||
>
|
||||
<h5
|
||||
className="subtitle is-6"
|
||||
>
|
||||
Disk Usage
|
||||
</h5>
|
||||
<div
|
||||
className="level level-multiline"
|
||||
>
|
||||
<Indicator
|
||||
className="is-one-third"
|
||||
label="Broker"
|
||||
>
|
||||
<div
|
||||
className="level-item is-one-third"
|
||||
>
|
||||
<div
|
||||
title="Broker"
|
||||
>
|
||||
<p
|
||||
className="heading"
|
||||
>
|
||||
Broker
|
||||
</p>
|
||||
<p
|
||||
className="title has-text-centered"
|
||||
>
|
||||
1
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Indicator>
|
||||
<Indicator
|
||||
className="is-one-third"
|
||||
label="Segment Size"
|
||||
title=""
|
||||
>
|
||||
<div
|
||||
className="level-item is-one-third"
|
||||
>
|
||||
<div
|
||||
title="Segment Size"
|
||||
>
|
||||
<p
|
||||
className="heading"
|
||||
>
|
||||
Segment Size
|
||||
</p>
|
||||
<p
|
||||
className="title has-text-centered"
|
||||
>
|
||||
<BytesFormatted
|
||||
value={60718}
|
||||
>
|
||||
<span>
|
||||
59KB
|
||||
</span>
|
||||
</BytesFormatted>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Indicator>
|
||||
<Indicator
|
||||
className="is-one-third"
|
||||
label="Segment count"
|
||||
>
|
||||
<div
|
||||
className="level-item is-one-third"
|
||||
>
|
||||
<div
|
||||
title="Segment count"
|
||||
>
|
||||
<p
|
||||
className="heading"
|
||||
>
|
||||
Segment count
|
||||
</p>
|
||||
<p
|
||||
className="title has-text-centered"
|
||||
>
|
||||
64
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Indicator>
|
||||
</div>
|
||||
</div>
|
||||
</MetricsWrapper>
|
||||
</div>
|
||||
</Brokers>
|
||||
</Router>
|
||||
</StaticRouter>
|
||||
`;
|
|
@ -5,7 +5,7 @@ import { ClusterFeaturesEnum } from 'generated-sources';
|
|||
import {
|
||||
getClustersFeatures,
|
||||
getClustersReadonlyStatus,
|
||||
} from 'redux/reducers/clusters/selectors';
|
||||
} from 'redux/reducers/clusters/clustersSlice';
|
||||
import {
|
||||
clusterBrokersPath,
|
||||
clusterConnectorsPath,
|
||||
|
@ -19,8 +19,8 @@ import Topics from 'components/Topics/Topics';
|
|||
import Schemas from 'components/Schemas/Schemas';
|
||||
import Connect from 'components/Connect/Connect';
|
||||
import ClusterContext from 'components/contexts/ClusterContext';
|
||||
import BrokersContainer from 'components/Brokers/BrokersContainer';
|
||||
import ConsumersGroupsContainer from 'components/ConsumerGroups/ConsumersGroupsContainer';
|
||||
import Brokers from 'components/Brokers/Brokers';
|
||||
import ConsumersGroups from 'components/ConsumerGroups/ConsumerGroups';
|
||||
import KsqlDb from 'components/KsqlDb/KsqlDb';
|
||||
|
||||
const Cluster: React.FC = () => {
|
||||
|
@ -52,14 +52,11 @@ const Cluster: React.FC = () => {
|
|||
return (
|
||||
<ClusterContext.Provider value={contextValue}>
|
||||
<Switch>
|
||||
<Route
|
||||
path={clusterBrokersPath(':clusterName')}
|
||||
component={BrokersContainer}
|
||||
/>
|
||||
<Route path={clusterBrokersPath(':clusterName')} component={Brokers} />
|
||||
<Route path={clusterTopicsPath(':clusterName')} component={Topics} />
|
||||
<Route
|
||||
path={clusterConsumerGroupsPath(':clusterName')}
|
||||
component={ConsumersGroupsContainer}
|
||||
component={ConsumersGroups}
|
||||
/>
|
||||
{hasSchemaRegistryConfigured && (
|
||||
<Route
|
||||
|
|
|
@ -1,85 +1,114 @@
|
|||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { Provider } from 'react-redux';
|
||||
import { Route, StaticRouter } from 'react-router-dom';
|
||||
import { ClusterFeaturesEnum } from 'generated-sources';
|
||||
import { fetchClusterListAction } from 'redux/actions';
|
||||
import configureStore from 'redux/store/configureStore';
|
||||
import { store } from 'redux/store';
|
||||
import { onlineClusterPayload } from 'redux/reducers/clusters/__test__/fixtures';
|
||||
import Cluster from 'components/Cluster/Cluster';
|
||||
import { fetchClusters } from 'redux/reducers/clusters/clustersSlice';
|
||||
import { screen } from '@testing-library/react';
|
||||
import { render } from 'lib/testHelpers';
|
||||
import {
|
||||
clusterBrokersPath,
|
||||
clusterConnectsPath,
|
||||
clusterConsumerGroupsPath,
|
||||
clusterKsqlDbPath,
|
||||
clusterSchemasPath,
|
||||
clusterTopicsPath,
|
||||
} from 'lib/paths';
|
||||
|
||||
const store = configureStore();
|
||||
|
||||
jest.mock('components/Topics/Topics', () => 'mock-Topics');
|
||||
jest.mock('components/Schemas/Schemas', () => 'mock-Schemas');
|
||||
jest.mock('components/Connect/Connect', () => 'mock-Connect');
|
||||
jest.mock('components/Brokers/BrokersContainer', () => 'mock-Brokers');
|
||||
jest.mock(
|
||||
'components/ConsumerGroups/ConsumersGroupsContainer',
|
||||
() => 'mock-ConsumerGroups'
|
||||
);
|
||||
jest.mock('components/Topics/Topics', () => () => <div>Topics</div>);
|
||||
jest.mock('components/Schemas/Schemas', () => () => <div>Schemas</div>);
|
||||
jest.mock('components/Connect/Connect', () => () => <div>Connect</div>);
|
||||
jest.mock('components/Connect/Connect', () => () => <div>Connect</div>);
|
||||
jest.mock('components/Brokers/Brokers', () => () => <div>Brokers</div>);
|
||||
jest.mock('components/ConsumerGroups/ConsumerGroups', () => () => (
|
||||
<div>ConsumerGroups</div>
|
||||
));
|
||||
jest.mock('components/KsqlDb/KsqlDb', () => () => <div>KsqlDb</div>);
|
||||
|
||||
describe('Cluster', () => {
|
||||
const setupComponent = (pathname: string) => (
|
||||
<Provider store={store}>
|
||||
const renderComponent = (pathname: string) =>
|
||||
render(
|
||||
<StaticRouter location={{ pathname }}>
|
||||
<Route path="/ui/clusters/:clusterName">
|
||||
<Cluster />
|
||||
</Route>
|
||||
</StaticRouter>
|
||||
</Provider>
|
||||
);
|
||||
);
|
||||
|
||||
it('renders Brokers', () => {
|
||||
const wrapper = mount(setupComponent('/ui/clusters/secondLocal/brokers'));
|
||||
expect(wrapper.exists('mock-Brokers')).toBeTruthy();
|
||||
renderComponent(clusterBrokersPath('second'));
|
||||
expect(screen.getByText('Brokers')).toBeInTheDocument();
|
||||
});
|
||||
it('renders Topics', () => {
|
||||
const wrapper = mount(setupComponent('/ui/clusters/secondLocal/topics'));
|
||||
expect(wrapper.exists('mock-Topics')).toBeTruthy();
|
||||
renderComponent(clusterTopicsPath('second'));
|
||||
expect(screen.getByText('Topics')).toBeInTheDocument();
|
||||
});
|
||||
it('renders ConsumerGroups', () => {
|
||||
const wrapper = mount(
|
||||
setupComponent('/ui/clusters/secondLocal/consumer-groups')
|
||||
);
|
||||
expect(wrapper.exists('mock-ConsumerGroups')).toBeTruthy();
|
||||
renderComponent(clusterConsumerGroupsPath('second'));
|
||||
expect(screen.getByText('ConsumerGroups')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('configured features', () => {
|
||||
it('does not render Schemas if SCHEMA_REGISTRY is not configured', () => {
|
||||
const wrapper = mount(setupComponent('/ui/clusters/secondLocal/schemas'));
|
||||
expect(wrapper.exists('mock-Schemas')).toBeFalsy();
|
||||
});
|
||||
it('renders Schemas if SCHEMA_REGISTRY is configured', () => {
|
||||
store.dispatch(
|
||||
fetchClusterListAction.success([
|
||||
{
|
||||
...onlineClusterPayload,
|
||||
features: [ClusterFeaturesEnum.SCHEMA_REGISTRY],
|
||||
},
|
||||
])
|
||||
fetchClusters.fulfilled(
|
||||
[
|
||||
{
|
||||
...onlineClusterPayload,
|
||||
features: [],
|
||||
},
|
||||
],
|
||||
'123'
|
||||
)
|
||||
);
|
||||
const wrapper = mount(setupComponent('/ui/clusters/secondLocal/schemas'));
|
||||
expect(wrapper.exists('mock-Schemas')).toBeTruthy();
|
||||
renderComponent(clusterSchemasPath('second'));
|
||||
expect(screen.queryByText('Schemas')).not.toBeInTheDocument();
|
||||
});
|
||||
it('does not render Connect if KAFKA_CONNECT is not configured', () => {
|
||||
const wrapper = mount(
|
||||
setupComponent('/ui/clusters/secondLocal/connectors')
|
||||
);
|
||||
expect(wrapper.exists('mock-Connect')).toBeFalsy();
|
||||
});
|
||||
it('renders Schemas if KAFKA_CONNECT is configured', async () => {
|
||||
it('renders Schemas if SCHEMA_REGISTRY is configured', async () => {
|
||||
store.dispatch(
|
||||
fetchClusterListAction.success([
|
||||
{
|
||||
...onlineClusterPayload,
|
||||
features: [ClusterFeaturesEnum.KAFKA_CONNECT],
|
||||
},
|
||||
])
|
||||
fetchClusters.fulfilled(
|
||||
[
|
||||
{
|
||||
...onlineClusterPayload,
|
||||
features: [ClusterFeaturesEnum.SCHEMA_REGISTRY],
|
||||
},
|
||||
],
|
||||
'123'
|
||||
)
|
||||
);
|
||||
const wrapper = mount(
|
||||
setupComponent('/ui/clusters/secondLocal/connectors')
|
||||
renderComponent(clusterSchemasPath(onlineClusterPayload.name));
|
||||
expect(screen.getByText('Schemas')).toBeInTheDocument();
|
||||
});
|
||||
it('renders Connect if KAFKA_CONNECT is configured', async () => {
|
||||
store.dispatch(
|
||||
fetchClusters.fulfilled(
|
||||
[
|
||||
{
|
||||
...onlineClusterPayload,
|
||||
features: [ClusterFeaturesEnum.KAFKA_CONNECT],
|
||||
},
|
||||
],
|
||||
'requestId'
|
||||
)
|
||||
);
|
||||
expect(wrapper.exists('mock-Connect')).toBeTruthy();
|
||||
renderComponent(clusterConnectsPath(onlineClusterPayload.name));
|
||||
expect(screen.getByText('Connect')).toBeInTheDocument();
|
||||
});
|
||||
it('renders KSQL if KSQL_DB is configured', async () => {
|
||||
store.dispatch(
|
||||
fetchClusters.fulfilled(
|
||||
[
|
||||
{
|
||||
...onlineClusterPayload,
|
||||
features: [ClusterFeaturesEnum.KSQL_DB],
|
||||
},
|
||||
],
|
||||
'requestId'
|
||||
)
|
||||
);
|
||||
renderComponent(clusterKsqlDbPath(onlineClusterPayload.name));
|
||||
expect(screen.getByText('KsqlDb')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,71 +0,0 @@
|
|||
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;
|
|
@ -1,65 +0,0 @@
|
|||
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();
|
||||
});
|
||||
});
|
|
@ -1,109 +0,0 @@
|
|||
// 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>
|
||||
`;
|
|
@ -7,28 +7,13 @@ import {
|
|||
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 = () => (
|
||||
<div className="section">
|
||||
<Switch>
|
||||
<Route
|
||||
path={clusterConnectConnectorPath(
|
||||
':clusterName',
|
||||
':connectName',
|
||||
':connectorName'
|
||||
)}
|
||||
component={Breadcrumbs}
|
||||
/>
|
||||
<Route
|
||||
path={clusterConnectorsPath(':clusterName')}
|
||||
component={Breadcrumbs}
|
||||
/>
|
||||
</Switch>
|
||||
<div>
|
||||
<Switch>
|
||||
<Route
|
||||
exact
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
import cx from 'classnames';
|
||||
import { ConnectorState } from 'generated-sources';
|
||||
import React from 'react';
|
||||
|
||||
export interface StatusTagProps {
|
||||
status: ConnectorState;
|
||||
}
|
||||
|
||||
const ConnectorStatusTag: React.FC<StatusTagProps> = ({ status }) => {
|
||||
const classNames = cx('tag', {
|
||||
'is-success': status === ConnectorState.RUNNING,
|
||||
'is-light': status === ConnectorState.PAUSED,
|
||||
'is-warning': status === ConnectorState.UNASSIGNED,
|
||||
'is-danger':
|
||||
status === ConnectorState.FAILED || status === ConnectorState.TASK_FAILED,
|
||||
});
|
||||
|
||||
return <span className={classNames}>{status}</span>;
|
||||
};
|
||||
|
||||
export default ConnectorStatusTag;
|
|
@ -1,5 +1,5 @@
|
|||
import React from 'react';
|
||||
import { Link, useHistory, useParams } from 'react-router-dom';
|
||||
import { useHistory, useParams } from 'react-router-dom';
|
||||
import { ConnectorState } from 'generated-sources';
|
||||
import { ClusterName, ConnectName, ConnectorName } from 'redux/interfaces';
|
||||
import {
|
||||
|
@ -7,6 +7,8 @@ import {
|
|||
clusterConnectorsPath,
|
||||
} from 'lib/paths';
|
||||
import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
|
||||
import styled from 'styled-components';
|
||||
import { Button } from 'components/common/Button/Button';
|
||||
|
||||
interface RouterParams {
|
||||
clusterName: ClusterName;
|
||||
|
@ -14,6 +16,11 @@ interface RouterParams {
|
|||
connectorName: ConnectorName;
|
||||
}
|
||||
|
||||
const ConnectorActionsWrapperStyled = styled.div`
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
`;
|
||||
|
||||
export interface ActionsProps {
|
||||
deleteConnector(
|
||||
clusterName: ClusterName,
|
||||
|
@ -78,81 +85,79 @@ const Actions: React.FC<ActionsProps> = ({
|
|||
}, [resumeConnector, clusterName, connectName, connectorName]);
|
||||
|
||||
return (
|
||||
<div className="buttons">
|
||||
<ConnectorActionsWrapperStyled>
|
||||
{connectorStatus === ConnectorState.RUNNING && (
|
||||
<button
|
||||
<Button
|
||||
buttonSize="M"
|
||||
buttonType="primary"
|
||||
type="button"
|
||||
className="button"
|
||||
onClick={pauseConnectorHandler}
|
||||
disabled={isConnectorActionRunning}
|
||||
>
|
||||
<span className="icon">
|
||||
<span>
|
||||
<i className="fas fa-pause" />
|
||||
</span>
|
||||
<span>Pause</span>
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{connectorStatus === ConnectorState.PAUSED && (
|
||||
<button
|
||||
<Button
|
||||
buttonSize="M"
|
||||
buttonType="primary"
|
||||
type="button"
|
||||
className="button"
|
||||
onClick={resumeConnectorHandler}
|
||||
disabled={isConnectorActionRunning}
|
||||
>
|
||||
<span className="icon">
|
||||
<span>
|
||||
<i className="fas fa-play" />
|
||||
</span>
|
||||
<span>Resume</span>
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<button
|
||||
<Button
|
||||
buttonSize="M"
|
||||
buttonType="primary"
|
||||
type="button"
|
||||
className="button"
|
||||
onClick={restartConnectorHandler}
|
||||
disabled={isConnectorActionRunning}
|
||||
>
|
||||
<span className="icon">
|
||||
<span>
|
||||
<i className="fas fa-sync-alt" />
|
||||
</span>
|
||||
<span>Restart all tasks</span>
|
||||
</button>
|
||||
</Button>
|
||||
<Button
|
||||
buttonSize="M"
|
||||
buttonType="primary"
|
||||
type="button"
|
||||
isLink
|
||||
disabled={isConnectorActionRunning}
|
||||
to={clusterConnectConnectorEditPath(
|
||||
clusterName,
|
||||
connectName,
|
||||
connectorName
|
||||
)}
|
||||
>
|
||||
<span>
|
||||
<i className="fas fa-pencil-alt" />
|
||||
</span>
|
||||
<span>Edit config</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"
|
||||
<Button
|
||||
buttonSize="M"
|
||||
buttonType="secondary"
|
||||
type="button"
|
||||
onClick={() => setIsDeleteConnectorConfirmationVisible(true)}
|
||||
disabled={isConnectorActionRunning}
|
||||
>
|
||||
<span className="icon">
|
||||
<span>
|
||||
<i className="far fa-trash-alt" />
|
||||
</span>
|
||||
<span>Delete</span>
|
||||
</button>
|
||||
</Button>
|
||||
<ConfirmationModal
|
||||
isOpen={isDeleteConnectorConfirmationVisible}
|
||||
onCancel={() => setIsDeleteConnectorConfirmationVisible(false)}
|
||||
|
@ -161,7 +166,7 @@ const Actions: React.FC<ActionsProps> = ({
|
|||
>
|
||||
Are you sure you want to remove <b>{connectorName}</b> connector?
|
||||
</ConfirmationModal>
|
||||
</div>
|
||||
</ConnectorActionsWrapperStyled>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -10,6 +10,8 @@ import Actions, {
|
|||
} from 'components/Connect/Details/Actions/Actions';
|
||||
import { ConnectorState } from 'generated-sources';
|
||||
import { ConfirmationModalProps } from 'components/common/ConfirmationModal/ConfirmationModal';
|
||||
import { ThemeProvider } from 'styled-components';
|
||||
import theme from 'theme/theme';
|
||||
|
||||
const mockHistoryPush = jest.fn();
|
||||
jest.mock('react-router-dom', () => ({
|
||||
|
@ -38,21 +40,23 @@ describe('Actions', () => {
|
|||
const connectorName = 'my-connector';
|
||||
|
||||
const setupWrapper = (props: Partial<ActionsProps> = {}) => (
|
||||
<TestRouterWrapper
|
||||
pathname={pathname}
|
||||
urlParams={{ clusterName, connectName, connectorName }}
|
||||
>
|
||||
<Actions
|
||||
deleteConnector={jest.fn()}
|
||||
isConnectorDeleting={false}
|
||||
connectorStatus={ConnectorState.RUNNING}
|
||||
restartConnector={jest.fn()}
|
||||
pauseConnector={jest.fn()}
|
||||
resumeConnector={jest.fn()}
|
||||
isConnectorActionRunning={false}
|
||||
{...props}
|
||||
/>
|
||||
</TestRouterWrapper>
|
||||
<ThemeProvider theme={theme}>
|
||||
<TestRouterWrapper
|
||||
pathname={pathname}
|
||||
urlParams={{ clusterName, connectName, connectorName }}
|
||||
>
|
||||
<Actions
|
||||
deleteConnector={jest.fn()}
|
||||
isConnectorDeleting={false}
|
||||
connectorStatus={ConnectorState.RUNNING}
|
||||
restartConnector={jest.fn()}
|
||||
pauseConnector={jest.fn()}
|
||||
resumeConnector={jest.fn()}
|
||||
isConnectorActionRunning={false}
|
||||
{...props}
|
||||
/>
|
||||
</TestRouterWrapper>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
it('matches snapshot', () => {
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -8,6 +8,8 @@ import {
|
|||
} from 'redux/interfaces';
|
||||
import PageLoader from 'components/common/PageLoader/PageLoader';
|
||||
import JSONEditor from 'components/common/JSONEditor/JSONEditor';
|
||||
import styled from 'styled-components';
|
||||
import { Colors } from 'theme/theme';
|
||||
|
||||
interface RouterParams {
|
||||
clusterName: ClusterName;
|
||||
|
@ -26,6 +28,13 @@ export interface ConfigProps {
|
|||
config: ConnectorConfig | null;
|
||||
}
|
||||
|
||||
const ConnectConfigWrapper = styled.div`
|
||||
padding: 16px;
|
||||
margin: 16px;
|
||||
border: 1px solid ${Colors.neutral[10]};
|
||||
border-radius: 8px;
|
||||
`;
|
||||
|
||||
const Config: React.FC<ConfigProps> = ({
|
||||
fetchConfig,
|
||||
isConfigFetching,
|
||||
|
@ -44,13 +53,14 @@ const Config: React.FC<ConfigProps> = ({
|
|||
if (!config) return null;
|
||||
|
||||
return (
|
||||
<JSONEditor
|
||||
readOnly
|
||||
value={JSON.stringify(config, null, '\t')}
|
||||
showGutter={false}
|
||||
highlightActiveLine={false}
|
||||
isFixedHeight
|
||||
/>
|
||||
<ConnectConfigWrapper>
|
||||
<JSONEditor
|
||||
readOnly
|
||||
value={JSON.stringify(config, null, '\t')}
|
||||
highlightActiveLine={false}
|
||||
isFixedHeight
|
||||
/>
|
||||
</ConnectConfigWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -1,18 +1,28 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Config view matches snapshot 1`] = `
|
||||
<mock-JSONEditor
|
||||
highlightActiveLine={false}
|
||||
isFixedHeight={true}
|
||||
readOnly={true}
|
||||
showGutter={false}
|
||||
value="{
|
||||
.c0 {
|
||||
padding: 16px;
|
||||
margin: 16px;
|
||||
border: 1px solid #E3E6E8;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
<div
|
||||
className="c0"
|
||||
>
|
||||
<mock-JSONEditor
|
||||
highlightActiveLine={false}
|
||||
isFixedHeight={true}
|
||||
readOnly={true}
|
||||
value="{
|
||||
\\"connector.class\\": \\"FileStreamSource\\",
|
||||
\\"tasks.max\\": \\"10\\",
|
||||
\\"topic\\": \\"test-topic\\",
|
||||
\\"file\\": \\"/some/file\\"
|
||||
}"
|
||||
/>
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Config view matches snapshot when fetching config 1`] = `<mock-PageLoader />`;
|
||||
|
|
|
@ -8,6 +8,8 @@ import {
|
|||
clusterConnectConnectorTasksPath,
|
||||
} from 'lib/paths';
|
||||
import PageLoader from 'components/common/PageLoader/PageLoader';
|
||||
import Navbar from 'components/common/Navigation/Navbar.styled';
|
||||
import PageHeading from 'components/common/PageHeading/PageHeading';
|
||||
|
||||
import OverviewContainer from './Overview/OverviewContainer';
|
||||
import TasksContainer from './Tasks/TasksContainer';
|
||||
|
@ -61,50 +63,45 @@ const Details: React.FC<DetailsProps> = ({
|
|||
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>
|
||||
<div>
|
||||
<PageHeading text={connectorName}>
|
||||
<ActionsContainer />
|
||||
</PageHeading>
|
||||
<Navbar role="navigation">
|
||||
<NavLink
|
||||
exact
|
||||
to={clusterConnectConnectorPath(
|
||||
clusterName,
|
||||
connectName,
|
||||
connectorName
|
||||
)}
|
||||
activeClassName="is-active"
|
||||
>
|
||||
Overview
|
||||
</NavLink>
|
||||
<NavLink
|
||||
exact
|
||||
to={clusterConnectConnectorTasksPath(
|
||||
clusterName,
|
||||
connectName,
|
||||
connectorName
|
||||
)}
|
||||
activeClassName="is-active"
|
||||
>
|
||||
Tasks
|
||||
</NavLink>
|
||||
<NavLink
|
||||
exact
|
||||
to={clusterConnectConnectorConfigPath(
|
||||
clusterName,
|
||||
connectName,
|
||||
connectorName
|
||||
)}
|
||||
activeClassName="is-active"
|
||||
>
|
||||
Config
|
||||
</NavLink>
|
||||
</Navbar>
|
||||
<Switch>
|
||||
<Route
|
||||
exact
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import React from 'react';
|
||||
import { Connector } from 'generated-sources';
|
||||
import ConnectorStatusTag from 'components/Connect/ConnectorStatusTag';
|
||||
import TagStyled from 'components/common/Tag/Tag.styled';
|
||||
import * as Metrics from 'components/common/Metrics';
|
||||
|
||||
export interface OverviewProps {
|
||||
connector: Connector | null;
|
||||
|
@ -16,42 +17,30 @@ const Overview: React.FC<OverviewProps> = ({
|
|||
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>
|
||||
<ConnectorStatusTag 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>
|
||||
<Metrics.Wrapper>
|
||||
<Metrics.Section>
|
||||
{connector.status?.workerId && (
|
||||
<Metrics.Indicator label="Worker">
|
||||
{connector.status.workerId}
|
||||
</Metrics.Indicator>
|
||||
)}
|
||||
<Metrics.Indicator label="Type">{connector.type}</Metrics.Indicator>
|
||||
{connector.config['connector.class'] && (
|
||||
<Metrics.Indicator label="Class">
|
||||
{connector.config['connector.class']}
|
||||
</Metrics.Indicator>
|
||||
)}
|
||||
<Metrics.Indicator label="State">
|
||||
<TagStyled color="yellow">{connector.status.state}</TagStyled>
|
||||
</Metrics.Indicator>
|
||||
<Metrics.Indicator label="Tasks running">
|
||||
{runningTasksCount}
|
||||
</Metrics.Indicator>
|
||||
<Metrics.Indicator label="Tasks failed" isAlert>
|
||||
{failedTasksCount}
|
||||
</Metrics.Indicator>
|
||||
</Metrics.Section>
|
||||
</Metrics.Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -7,20 +7,22 @@ import Overview, {
|
|||
OverviewProps,
|
||||
} from 'components/Connect/Details/Overview/Overview';
|
||||
import { connector } from 'redux/reducers/connect/__test__/fixtures';
|
||||
|
||||
jest.mock('components/Connect/StatusTag', () => 'mock-StatusTag');
|
||||
import { ThemeProvider } from 'styled-components';
|
||||
import theme from 'theme/theme';
|
||||
|
||||
describe('Overview', () => {
|
||||
containerRendersView(<OverviewContainer />, Overview);
|
||||
|
||||
describe('view', () => {
|
||||
const setupWrapper = (props: Partial<OverviewProps> = {}) => (
|
||||
<Overview
|
||||
connector={connector}
|
||||
runningTasksCount={10}
|
||||
failedTasksCount={2}
|
||||
{...props}
|
||||
/>
|
||||
<ThemeProvider theme={theme}>
|
||||
<Overview
|
||||
connector={connector}
|
||||
runningTasksCount={10}
|
||||
failedTasksCount={2}
|
||||
{...props}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
it('matches snapshot', () => {
|
||||
|
@ -30,7 +32,7 @@ describe('Overview', () => {
|
|||
|
||||
it('is empty when no connector', () => {
|
||||
const wrapper = mount(setupWrapper({ connector: null }));
|
||||
expect(wrapper.html()).toBeNull();
|
||||
expect(wrapper.html()).toEqual('');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,66 +1,224 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Overview view matches snapshot 1`] = `
|
||||
.c5 {
|
||||
border: none;
|
||||
border-radius: 16px;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
background-color: #FFEECC;
|
||||
color: #171A1C;
|
||||
font-size: 12px;
|
||||
display: inline-block;
|
||||
padding-left: 0.75em;
|
||||
padding-right: 0.75em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.c0 {
|
||||
padding: 1.5rem 1rem;
|
||||
background: #F1F2F3;
|
||||
margin-bottom: 0.5rem !important;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
-webkit-flex-wrap: wrap;
|
||||
-ms-flex-wrap: wrap;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.c3 {
|
||||
background-color: #FFFFFF;
|
||||
height: 68px;
|
||||
width: -webkit-fit-content;
|
||||
width: -moz-fit-content;
|
||||
width: fit-content;
|
||||
min-width: 150px;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-flex-direction: column;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
-webkit-box-pack: center;
|
||||
-webkit-justify-content: center;
|
||||
-ms-flex-pack: center;
|
||||
justify-content: center;
|
||||
-webkit-align-items: flex-start;
|
||||
-webkit-box-align: flex-start;
|
||||
-ms-flex-align: flex-start;
|
||||
align-items: flex-start;
|
||||
padding: 12px 16px;
|
||||
box-shadow: 3px 3px 3px rgba(0,0,0,0.08);
|
||||
margin: 0 0 3px 0;
|
||||
-webkit-box-flex: 1;
|
||||
-webkit-flex-grow: 1;
|
||||
-ms-flex-positive: 1;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.c4 {
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
color: #73848C;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.c1 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
-webkit-flex-wrap: wrap;
|
||||
-ms-flex-wrap: wrap;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.c1 > .c2:first-child {
|
||||
border-top-left-radius: 8px;
|
||||
border-bottom-left-radius: 8px;
|
||||
}
|
||||
|
||||
.c1 > .c2:last-child {
|
||||
border-top-right-radius: 8px;
|
||||
border-bottom-right-radius: 8px;
|
||||
}
|
||||
|
||||
@media screen and (max-width:1023px) {
|
||||
.c1 > .c2:first-child,
|
||||
.c1 > .c2:last-child {
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
<div
|
||||
className="tile is-6"
|
||||
className="c0"
|
||||
>
|
||||
<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>
|
||||
<span
|
||||
className="tag is-success"
|
||||
<div>
|
||||
<div
|
||||
className="c1"
|
||||
>
|
||||
<div
|
||||
className="c2 c3"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
className="c4"
|
||||
>
|
||||
RUNNING
|
||||
Worker
|
||||
|
||||
</div>
|
||||
<span>
|
||||
kafka-connect0:8083
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>
|
||||
Tasks Running
|
||||
</th>
|
||||
<td>
|
||||
10
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>
|
||||
Tasks Failed
|
||||
</th>
|
||||
<td>
|
||||
2
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="c2 c3"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
className="c4"
|
||||
>
|
||||
Type
|
||||
|
||||
</div>
|
||||
<span>
|
||||
SOURCE
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="c2 c3"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
className="c4"
|
||||
>
|
||||
Class
|
||||
|
||||
</div>
|
||||
<span>
|
||||
FileStreamSource
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="c2 c3"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
className="c4"
|
||||
>
|
||||
State
|
||||
|
||||
</div>
|
||||
<span>
|
||||
<p
|
||||
className="c5"
|
||||
>
|
||||
RUNNING
|
||||
</p>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="c2 c3"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
className="c4"
|
||||
>
|
||||
Tasks running
|
||||
|
||||
</div>
|
||||
<span>
|
||||
10
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="c2 c3"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
className="c4"
|
||||
>
|
||||
Tasks failed
|
||||
|
||||
<svg
|
||||
fill="none"
|
||||
height="4"
|
||||
viewBox="0 0 4 4"
|
||||
width="4"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<circle
|
||||
cx="2"
|
||||
cy="2"
|
||||
fill="#E61A1A"
|
||||
r="2"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<span>
|
||||
2
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
|
@ -2,7 +2,10 @@ 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';
|
||||
import Dropdown from 'components/common/Dropdown/Dropdown';
|
||||
import DropdownItem from 'components/common/Dropdown/DropdownItem';
|
||||
import VerticalElipsisIcon from 'components/common/Icons/VerticalElipsisIcon';
|
||||
import TagStyled from 'components/common/Tag/Tag.styled';
|
||||
|
||||
interface RouterParams {
|
||||
clusterName: ClusterName;
|
||||
|
@ -22,33 +25,27 @@ export interface ListItemProps {
|
|||
|
||||
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?.id}</td>
|
||||
<td>{task.status?.workerId}</td>
|
||||
<td>
|
||||
<StatusTag status={task.status.state} />
|
||||
<TagStyled color="yellow">{task.status.state}</TagStyled>
|
||||
</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>{task.status.trace || 'null'}</td>
|
||||
<td style={{ width: '5%' }}>
|
||||
<div>
|
||||
<Dropdown label={<VerticalElipsisIcon />} right>
|
||||
<DropdownItem onClick={restartTaskHandler}>
|
||||
<span>Clear Messages</span>
|
||||
</DropdownItem>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
|
|
|
@ -9,8 +9,8 @@ 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');
|
||||
import { ThemeProvider } from 'styled-components';
|
||||
import theme from 'theme/theme';
|
||||
|
||||
describe('ListItem', () => {
|
||||
containerRendersView(
|
||||
|
@ -33,16 +33,18 @@ describe('ListItem', () => {
|
|||
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>
|
||||
<ThemeProvider theme={theme}>
|
||||
<TestRouterWrapper
|
||||
pathname={pathname}
|
||||
urlParams={{ clusterName, connectName, connectorName }}
|
||||
>
|
||||
<table>
|
||||
<tbody>
|
||||
<ListItem task={tasks[0]} restartTask={jest.fn()} {...props} />
|
||||
</tbody>
|
||||
</table>
|
||||
</TestRouterWrapper>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
it('matches snapshot', () => {
|
||||
|
@ -54,7 +56,10 @@ describe('ListItem', () => {
|
|||
const restartTask = jest.fn();
|
||||
const wrapper = mount(setupWrapper({ restartTask }));
|
||||
await act(async () => {
|
||||
wrapper.find('button').simulate('click');
|
||||
wrapper.find('svg').simulate('click');
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper.find('span').simulate('click');
|
||||
});
|
||||
expect(restartTask).toHaveBeenCalledTimes(1);
|
||||
expect(restartTask).toHaveBeenCalledWith(
|
||||
|
|
|
@ -1,38 +1,128 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ListItem view matches snapshot 1`] = `
|
||||
.c2 {
|
||||
background: transparent;
|
||||
border: none;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-align-items: 'center';
|
||||
-webkit-box-align: 'center';
|
||||
-ms-flex-align: 'center';
|
||||
align-items: 'center';
|
||||
-webkit-box-pack: 'center';
|
||||
-webkit-justify-content: 'center';
|
||||
-ms-flex-pack: 'center';
|
||||
justify-content: 'center';
|
||||
}
|
||||
|
||||
.c2:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.c1 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-align-self: center;
|
||||
-ms-flex-item-align: center;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.c0 {
|
||||
border: none;
|
||||
border-radius: 16px;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
background-color: #FFEECC;
|
||||
color: #171A1C;
|
||||
font-size: 12px;
|
||||
display: inline-block;
|
||||
padding-left: 0.75em;
|
||||
padding-right: 0.75em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td
|
||||
className="has-text-overflow-ellipsis"
|
||||
>
|
||||
<td>
|
||||
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"
|
||||
<p
|
||||
className="c0"
|
||||
>
|
||||
<span
|
||||
className="icon"
|
||||
RUNNING
|
||||
</p>
|
||||
</td>
|
||||
<td>
|
||||
null
|
||||
</td>
|
||||
<td
|
||||
style={
|
||||
Object {
|
||||
"width": "5%",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
className="dropdown is-right"
|
||||
>
|
||||
<i
|
||||
className="fas fa-sync-alt"
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
<div
|
||||
className="c1"
|
||||
>
|
||||
<button
|
||||
aria-controls="dropdown-menu"
|
||||
aria-haspopup="true"
|
||||
className="c2"
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
fill="none"
|
||||
height="16"
|
||||
viewBox="0 0 4 16"
|
||||
width="4"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M2 4C3.1 4 4 3.1 4 2C4 0.9 3.1 0 2 0C0.9 0 0 0.9 0 2C0 3.1 0.9 4 2 4ZM2 6C0.9 6 0 6.9 0 8C0 9.1 0.9 10 2 10C3.1 10 4 9.1 4 8C4 6.9 3.1 6 2 6ZM2 12C0.9 12 0 12.9 0 14C0 15.1 0.9 16 2 16C3.1 16 4 15.1 4 14C4 12.9 3.1 12 2 12Z"
|
||||
fill="#73848C"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-menu"
|
||||
id="dropdown-menu"
|
||||
role="menu"
|
||||
>
|
||||
<div
|
||||
className="dropdown-content has-text-left"
|
||||
>
|
||||
<a
|
||||
className="dropdown-item is-link"
|
||||
href="#end"
|
||||
onClick={[Function]}
|
||||
role="menuitem"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Clear Messages
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
|
|
@ -3,6 +3,8 @@ 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 { Table } from 'components/common/table/Table/Table.styled';
|
||||
import TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeaderCell';
|
||||
|
||||
import ListItemContainer from './ListItem/ListItemContainer';
|
||||
|
||||
|
@ -39,16 +41,14 @@ const Tasks: React.FC<TasksProps> = ({
|
|||
}
|
||||
|
||||
return (
|
||||
<table className="table is-fullwidth">
|
||||
<Table isFullwidth>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Worker</th>
|
||||
<th>State</th>
|
||||
<th>Trace</th>
|
||||
<th>
|
||||
<span className="is-pulled-right">Restart</span>
|
||||
</th>
|
||||
<TableHeaderCell title="ID" />
|
||||
<TableHeaderCell title="Worker" />
|
||||
<TableHeaderCell title="State" />
|
||||
<TableHeaderCell title="Trace" />
|
||||
<TableHeaderCell />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
@ -61,7 +61,7 @@ const Tasks: React.FC<TasksProps> = ({
|
|||
<ListItemContainer key={task.status?.id} task={task} />
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</Table>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -6,6 +6,8 @@ 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';
|
||||
import { ThemeProvider } from 'styled-components';
|
||||
import theme from 'theme/theme';
|
||||
|
||||
jest.mock('components/common/PageLoader/PageLoader', () => 'mock-PageLoader');
|
||||
|
||||
|
@ -28,17 +30,19 @@ describe('Tasks', () => {
|
|||
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>
|
||||
<ThemeProvider theme={theme}>
|
||||
<TestRouterWrapper
|
||||
pathname={pathname}
|
||||
urlParams={{ clusterName, connectName, connectorName }}
|
||||
>
|
||||
<Tasks
|
||||
fetchTasks={jest.fn()}
|
||||
areTasksFetching={false}
|
||||
tasks={tasks}
|
||||
{...props}
|
||||
/>
|
||||
</TestRouterWrapper>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
it('matches snapshot', () => {
|
||||
|
|
|
@ -1,30 +1,117 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Tasks view matches snapshot 1`] = `
|
||||
.c0 {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.c0 td {
|
||||
border-top: 1px #f1f2f3 solid;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
padding: 8px 8px 8px 24px;
|
||||
color: #171A1C;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.c0 tbody > tr:hover {
|
||||
background-color: #F1F2F3;
|
||||
}
|
||||
|
||||
.c1 {
|
||||
padding: 4px 0 4px 24px !important;
|
||||
border-bottom-width: 1px !important;
|
||||
vertical-align: middle !important;
|
||||
}
|
||||
|
||||
.c1.is-clickable {
|
||||
cursor: pointer !important;
|
||||
pointer-events: all !important;
|
||||
}
|
||||
|
||||
.c1.has-text-link-dark span {
|
||||
color: #4F4FFF !important;
|
||||
}
|
||||
|
||||
.c1 span {
|
||||
font-family: Inter,sans-serif;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 16px;
|
||||
-webkit-letter-spacing: 0em;
|
||||
-moz-letter-spacing: 0em;
|
||||
-ms-letter-spacing: 0em;
|
||||
letter-spacing: 0em;
|
||||
text-align: left;
|
||||
background: #FFFFFF;
|
||||
color: #73848C;
|
||||
}
|
||||
|
||||
.c1 span.preview {
|
||||
margin-left: 8px;
|
||||
font-size: 14px;
|
||||
color: #4F4FFF;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.c1 span.is-clickable {
|
||||
cursor: pointer !important;
|
||||
pointer-events: all !important;
|
||||
}
|
||||
|
||||
<table
|
||||
className="table is-fullwidth"
|
||||
className="c0"
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
ID
|
||||
</th>
|
||||
<th>
|
||||
Worker
|
||||
</th>
|
||||
<th>
|
||||
State
|
||||
</th>
|
||||
<th>
|
||||
Trace
|
||||
</th>
|
||||
<th>
|
||||
<th
|
||||
className="c1"
|
||||
title="ID"
|
||||
>
|
||||
<span
|
||||
className="is-pulled-right"
|
||||
className="title"
|
||||
>
|
||||
Restart
|
||||
ID
|
||||
</span>
|
||||
</th>
|
||||
<th
|
||||
className="c1"
|
||||
title="Worker"
|
||||
>
|
||||
<span
|
||||
className="title"
|
||||
>
|
||||
Worker
|
||||
</span>
|
||||
</th>
|
||||
<th
|
||||
className="c1"
|
||||
title="State"
|
||||
>
|
||||
<span
|
||||
className="title"
|
||||
>
|
||||
State
|
||||
</span>
|
||||
</th>
|
||||
<th
|
||||
className="c1"
|
||||
title="Trace"
|
||||
>
|
||||
<span
|
||||
className="title"
|
||||
>
|
||||
Trace
|
||||
</span>
|
||||
</th>
|
||||
<th
|
||||
className="c1"
|
||||
>
|
||||
<span
|
||||
className="title"
|
||||
/>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
@ -99,30 +186,117 @@ exports[`Tasks view matches snapshot 1`] = `
|
|||
exports[`Tasks view matches snapshot when fetching tasks 1`] = `<mock-PageLoader />`;
|
||||
|
||||
exports[`Tasks view matches snapshot when no tasks 1`] = `
|
||||
.c0 {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.c0 td {
|
||||
border-top: 1px #f1f2f3 solid;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
padding: 8px 8px 8px 24px;
|
||||
color: #171A1C;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.c0 tbody > tr:hover {
|
||||
background-color: #F1F2F3;
|
||||
}
|
||||
|
||||
.c1 {
|
||||
padding: 4px 0 4px 24px !important;
|
||||
border-bottom-width: 1px !important;
|
||||
vertical-align: middle !important;
|
||||
}
|
||||
|
||||
.c1.is-clickable {
|
||||
cursor: pointer !important;
|
||||
pointer-events: all !important;
|
||||
}
|
||||
|
||||
.c1.has-text-link-dark span {
|
||||
color: #4F4FFF !important;
|
||||
}
|
||||
|
||||
.c1 span {
|
||||
font-family: Inter,sans-serif;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 16px;
|
||||
-webkit-letter-spacing: 0em;
|
||||
-moz-letter-spacing: 0em;
|
||||
-ms-letter-spacing: 0em;
|
||||
letter-spacing: 0em;
|
||||
text-align: left;
|
||||
background: #FFFFFF;
|
||||
color: #73848C;
|
||||
}
|
||||
|
||||
.c1 span.preview {
|
||||
margin-left: 8px;
|
||||
font-size: 14px;
|
||||
color: #4F4FFF;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.c1 span.is-clickable {
|
||||
cursor: pointer !important;
|
||||
pointer-events: all !important;
|
||||
}
|
||||
|
||||
<table
|
||||
className="table is-fullwidth"
|
||||
className="c0"
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
ID
|
||||
</th>
|
||||
<th>
|
||||
Worker
|
||||
</th>
|
||||
<th>
|
||||
State
|
||||
</th>
|
||||
<th>
|
||||
Trace
|
||||
</th>
|
||||
<th>
|
||||
<th
|
||||
className="c1"
|
||||
title="ID"
|
||||
>
|
||||
<span
|
||||
className="is-pulled-right"
|
||||
className="title"
|
||||
>
|
||||
Restart
|
||||
ID
|
||||
</span>
|
||||
</th>
|
||||
<th
|
||||
className="c1"
|
||||
title="Worker"
|
||||
>
|
||||
<span
|
||||
className="title"
|
||||
>
|
||||
Worker
|
||||
</span>
|
||||
</th>
|
||||
<th
|
||||
className="c1"
|
||||
title="State"
|
||||
>
|
||||
<span
|
||||
className="title"
|
||||
>
|
||||
State
|
||||
</span>
|
||||
</th>
|
||||
<th
|
||||
className="c1"
|
||||
title="Trace"
|
||||
>
|
||||
<span
|
||||
className="title"
|
||||
>
|
||||
Trace
|
||||
</span>
|
||||
</th>
|
||||
<th
|
||||
className="c1"
|
||||
>
|
||||
<span
|
||||
className="title"
|
||||
/>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
|
|
@ -6,6 +6,8 @@ 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';
|
||||
import { ThemeProvider } from 'styled-components';
|
||||
import theme from 'theme/theme';
|
||||
|
||||
jest.mock('components/common/PageLoader/PageLoader', () => 'mock-PageLoader');
|
||||
|
||||
|
@ -43,20 +45,22 @@ describe('Details', () => {
|
|||
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>
|
||||
<ThemeProvider theme={theme}>
|
||||
<TestRouterWrapper
|
||||
pathname={pathname}
|
||||
urlParams={{ clusterName, connectName, connectorName }}
|
||||
>
|
||||
<Details
|
||||
fetchConnector={jest.fn()}
|
||||
fetchTasks={jest.fn()}
|
||||
isConnectorFetching={false}
|
||||
areTasksFetching={false}
|
||||
connector={connector}
|
||||
tasks={tasks}
|
||||
{...props}
|
||||
/>
|
||||
</TestRouterWrapper>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
it('matches snapshot', () => {
|
||||
|
|
|
@ -1,47 +1,115 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Details view matches snapshot 1`] = `
|
||||
<div
|
||||
className="box"
|
||||
>
|
||||
<nav
|
||||
className="navbar mb-4"
|
||||
role="navigation"
|
||||
.c1 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
border-bottom: 1px #E3E6E8 solid;
|
||||
}
|
||||
|
||||
.c1 a {
|
||||
height: 40px;
|
||||
width: 96px;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-pack: center;
|
||||
-webkit-justify-content: center;
|
||||
-ms-flex-pack: center;
|
||||
justify-content: center;
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
color: #73848C;
|
||||
border-bottom: 1px transparent solid;
|
||||
}
|
||||
|
||||
.c1 a.is-active {
|
||||
border-bottom: 1px #4F4FFF solid;
|
||||
color: #171A1C;
|
||||
}
|
||||
|
||||
.c1 a:hover:not(.is-active) {
|
||||
border-bottom: 1px transparent solid;
|
||||
color: #171A1C;
|
||||
}
|
||||
|
||||
.c0 {
|
||||
height: 56px;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-pack: justify;
|
||||
-webkit-justify-content: space-between;
|
||||
-ms-flex-pack: justify;
|
||||
justify-content: space-between;
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
padding: 0px 16px;
|
||||
}
|
||||
|
||||
.c0 h1 {
|
||||
font-size: 24px;
|
||||
font-weight: 500;
|
||||
line-height: 32px;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.c0 > div {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
<div>
|
||||
<div
|
||||
className="c0"
|
||||
>
|
||||
<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"
|
||||
>
|
||||
<h1>
|
||||
my-connector
|
||||
</h1>
|
||||
<div>
|
||||
<mock-ActionsContainer />
|
||||
</div>
|
||||
</div>
|
||||
<nav
|
||||
className="c1"
|
||||
role="navigation"
|
||||
>
|
||||
<a
|
||||
aria-current="page"
|
||||
className="is-active"
|
||||
href="/ui/clusters/my-cluster/connects/my-connect/connectors/my-connector"
|
||||
onClick={[Function]}
|
||||
style={Object {}}
|
||||
>
|
||||
Overview
|
||||
</a>
|
||||
<a
|
||||
aria-current={null}
|
||||
href="/ui/clusters/my-cluster/connects/my-connect/connectors/my-connector/tasks"
|
||||
onClick={[Function]}
|
||||
>
|
||||
Tasks
|
||||
</a>
|
||||
<a
|
||||
aria-current={null}
|
||||
href="/ui/clusters/my-cluster/connects/my-connect/connectors/my-connector/config"
|
||||
onClick={[Function]}
|
||||
>
|
||||
Config
|
||||
</a>
|
||||
</nav>
|
||||
<mock-OverviewContainer
|
||||
history={
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
import styled from 'styled-components';
|
||||
import { Colors } from 'theme/theme';
|
||||
|
||||
export const ConnectEditWrapperStyled = styled.div`
|
||||
margin: 16px;
|
||||
|
||||
& form > *:last-child {
|
||||
margin-top: 16px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const ConnectEditWarningMessageStyled = styled.div`
|
||||
height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: ${Colors.yellow[10]};
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
margin-bottom: 16px;
|
||||
`;
|
|
@ -14,6 +14,12 @@ import { clusterConnectConnectorConfigPath } from 'lib/paths';
|
|||
import yup from 'lib/yupExtended';
|
||||
import JSONEditor from 'components/common/JSONEditor/JSONEditor';
|
||||
import PageLoader from 'components/common/PageLoader/PageLoader';
|
||||
import { Button } from 'components/common/Button/Button';
|
||||
|
||||
import {
|
||||
ConnectEditWarningMessageStyled,
|
||||
ConnectEditWrapperStyled,
|
||||
} from './Edit.styled';
|
||||
|
||||
const validationSchema = yup.object().shape({
|
||||
config: yup.string().required().isJsonObject(),
|
||||
|
@ -103,41 +109,36 @@ const Edit: React.FC<EditProps> = ({
|
|||
'"******"'
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<ConnectEditWrapperStyled>
|
||||
{hasCredentials && (
|
||||
<div className="notification is-danger is-light">
|
||||
<ConnectEditWarningMessageStyled>
|
||||
Please replace ****** with the real credential values to avoid
|
||||
accidentally breaking your connector config!
|
||||
</div>
|
||||
</ConnectEditWarningMessageStyled>
|
||||
)}
|
||||
<div className="box">
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="field">
|
||||
<div className="control">
|
||||
<Controller
|
||||
control={control}
|
||||
name="config"
|
||||
render={({ field }) => (
|
||||
<JSONEditor {...field} 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>
|
||||
</>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="config"
|
||||
render={({ field }) => (
|
||||
<JSONEditor {...field} readOnly={isSubmitting} />
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<ErrorMessage errors={errors} name="config" />
|
||||
</div>
|
||||
<Button
|
||||
buttonSize="M"
|
||||
buttonType="primary"
|
||||
type="submit"
|
||||
disabled={!isValid || isSubmitting || !isDirty}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</form>
|
||||
</ConnectEditWrapperStyled>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -10,6 +10,8 @@ import {
|
|||
import EditContainer from 'components/Connect/Edit/EditContainer';
|
||||
import Edit, { EditProps } from 'components/Connect/Edit/Edit';
|
||||
import { connector } from 'redux/reducers/connect/__test__/fixtures';
|
||||
import { ThemeProvider } from 'styled-components';
|
||||
import theme from 'theme/theme';
|
||||
|
||||
jest.mock('components/common/PageLoader/PageLoader', () => 'mock-PageLoader');
|
||||
|
||||
|
@ -37,18 +39,20 @@ describe('Edit', () => {
|
|||
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>
|
||||
<ThemeProvider theme={theme}>
|
||||
<TestRouterWrapper
|
||||
pathname={pathname}
|
||||
urlParams={{ clusterName, connectName, connectorName }}
|
||||
>
|
||||
<Edit
|
||||
fetchConfig={jest.fn()}
|
||||
isConfigFetching={false}
|
||||
config={connector.config}
|
||||
updateConfig={jest.fn()}
|
||||
{...props}
|
||||
/>
|
||||
</TestRouterWrapper>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
it('matches snapshot', () => {
|
||||
|
|
|
@ -1,105 +1,207 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Edit view matches snapshot 1`] = `
|
||||
.c1 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-flex-direction: row;
|
||||
-ms-flex-direction: row;
|
||||
flex-direction: row;
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
-webkit-box-pack: center;
|
||||
-webkit-justify-content: center;
|
||||
-ms-flex-pack: center;
|
||||
justify-content: center;
|
||||
padding: 0px 12px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
white-space: nowrap;
|
||||
background: #4F4FFF;
|
||||
color: #FFFFFF;
|
||||
font-size: 14px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.c1:hover:enabled {
|
||||
background: #1717CF;
|
||||
color: #FFFFFF;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.c1:active:enabled {
|
||||
background: #1414B8;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.c1:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.c1 a {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.c1 i {
|
||||
margin-right: 7px;
|
||||
}
|
||||
|
||||
.c0 {
|
||||
margin: 16px;
|
||||
}
|
||||
|
||||
.c0 form > *:last-child {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
<div
|
||||
className="box"
|
||||
className="c0"
|
||||
>
|
||||
<form
|
||||
onSubmit={[Function]}
|
||||
>
|
||||
<div
|
||||
className="field"
|
||||
>
|
||||
<div
|
||||
className="control"
|
||||
>
|
||||
<mock-JSONEditor
|
||||
name="config"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
readOnly={false}
|
||||
value="{
|
||||
<div>
|
||||
<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 />
|
||||
<button
|
||||
className="c1"
|
||||
disabled={true}
|
||||
type="submit"
|
||||
>
|
||||
<div
|
||||
className="control"
|
||||
>
|
||||
<input
|
||||
className="button is-primary"
|
||||
disabled={true}
|
||||
type="submit"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
Submit
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Edit view matches snapshot when config has credentials 1`] = `
|
||||
Array [
|
||||
.c2 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-flex-direction: row;
|
||||
-ms-flex-direction: row;
|
||||
flex-direction: row;
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
-webkit-box-pack: center;
|
||||
-webkit-justify-content: center;
|
||||
-ms-flex-pack: center;
|
||||
justify-content: center;
|
||||
padding: 0px 12px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
white-space: nowrap;
|
||||
background: #4F4FFF;
|
||||
color: #FFFFFF;
|
||||
font-size: 14px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.c2:hover:enabled {
|
||||
background: #1717CF;
|
||||
color: #FFFFFF;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.c2:active:enabled {
|
||||
background: #1414B8;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.c2:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.c2 a {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.c2 i {
|
||||
margin-right: 7px;
|
||||
}
|
||||
|
||||
.c0 {
|
||||
margin: 16px;
|
||||
}
|
||||
|
||||
.c0 form > *:last-child {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.c1 {
|
||||
height: 48px;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
background-color: #FFEECC;
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
<div
|
||||
className="c0"
|
||||
>
|
||||
<div
|
||||
className="notification is-danger is-light"
|
||||
className="c1"
|
||||
>
|
||||
Please replace ****** with the real credential values to avoid accidentally breaking your connector config!
|
||||
</div>,
|
||||
<div
|
||||
className="box"
|
||||
</div>
|
||||
<form
|
||||
onSubmit={[Function]}
|
||||
>
|
||||
<form
|
||||
onSubmit={[Function]}
|
||||
>
|
||||
<div
|
||||
className="field"
|
||||
>
|
||||
<div
|
||||
className="control"
|
||||
>
|
||||
<mock-JSONEditor
|
||||
name="config"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
readOnly={false}
|
||||
value="{
|
||||
<div>
|
||||
<mock-JSONEditor
|
||||
name="config"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
readOnly={false}
|
||||
value="{
|
||||
\\"connector.class\\": \\"FileStreamSource\\",
|
||||
\\"tasks.max\\": \\"10\\",
|
||||
\\"topic\\": \\"test-topic\\",
|
||||
\\"file\\": \\"/some/file\\",
|
||||
\\"password\\": \\"******\\"
|
||||
}"
|
||||
/>
|
||||
</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>,
|
||||
]
|
||||
/>
|
||||
</div>
|
||||
<div />
|
||||
<button
|
||||
className="c2"
|
||||
disabled={true}
|
||||
type="submit"
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Edit view matches snapshot when fetching config 1`] = `<mock-PageLoader />`;
|
||||
|
|
|
@ -1,13 +1,17 @@
|
|||
import React from 'react';
|
||||
import { Link, useParams } from 'react-router-dom';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { Connect, FullConnectorInfo } from 'generated-sources';
|
||||
import { ClusterName, ConnectorSearch } from 'redux/interfaces';
|
||||
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 Search from 'components/common/Search/Search';
|
||||
import * as Metrics from 'components/common/Metrics';
|
||||
import PageHeading from 'components/common/PageHeading/PageHeading';
|
||||
import { Button } from 'components/common/Button/Button';
|
||||
import { ControlPanelWrapper } from 'components/common/ControlPanel/ControlPanel.styled';
|
||||
import { Table } from 'components/common/table/Table/Table.styled';
|
||||
import TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeaderCell';
|
||||
|
||||
import ListItem from './ListItem';
|
||||
|
||||
|
@ -47,50 +51,51 @@ const List: React.FC<ListProps> = ({
|
|||
|
||||
return (
|
||||
<>
|
||||
<MetricsWrapper>
|
||||
<Indicator
|
||||
className="level-left is-one-third"
|
||||
label="Connects"
|
||||
title="Connects"
|
||||
fetching={areConnectsFetching}
|
||||
>
|
||||
{connectors.length}
|
||||
</Indicator>
|
||||
|
||||
<div className="column">
|
||||
<Search
|
||||
handleSearch={handleSearch}
|
||||
placeholder="Search by Connect Name, Status or Type"
|
||||
value={search}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<PageHeading text="Connectors">
|
||||
{!isReadOnly && (
|
||||
<div className="level-item level-right">
|
||||
<Link
|
||||
className="button is-primary"
|
||||
to={clusterConnectorNewPath(clusterName)}
|
||||
>
|
||||
Create Connector
|
||||
</Link>
|
||||
</div>
|
||||
<Button
|
||||
isLink
|
||||
buttonType="primary"
|
||||
buttonSize="M"
|
||||
to={clusterConnectorNewPath(clusterName)}
|
||||
>
|
||||
Create Connector
|
||||
</Button>
|
||||
)}
|
||||
</MetricsWrapper>
|
||||
</PageHeading>
|
||||
<Metrics.Wrapper>
|
||||
<Metrics.Section>
|
||||
<Metrics.Indicator
|
||||
label="Connects"
|
||||
title="Connects"
|
||||
fetching={areConnectsFetching}
|
||||
>
|
||||
{connectors.length}
|
||||
</Metrics.Indicator>
|
||||
</Metrics.Section>
|
||||
</Metrics.Wrapper>
|
||||
<ControlPanelWrapper hasInput>
|
||||
<Search
|
||||
handleSearch={handleSearch}
|
||||
placeholder="Search by Connect Name, Status or Type"
|
||||
value={search}
|
||||
/>
|
||||
</ControlPanelWrapper>
|
||||
{areConnectorsFetching ? (
|
||||
<PageLoader />
|
||||
) : (
|
||||
<div className="box">
|
||||
<table className="table is-fullwidth">
|
||||
<div>
|
||||
<Table isFullwidth>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Connect</th>
|
||||
<th>Type</th>
|
||||
<th>Plugin</th>
|
||||
<th>Topics</th>
|
||||
<th>Status</th>
|
||||
<th>Running Tasks</th>
|
||||
<th> </th>
|
||||
<TableHeaderCell title="Name" />
|
||||
<TableHeaderCell title="Connect" />
|
||||
<TableHeaderCell title="Type" />
|
||||
<TableHeaderCell title="Plugin" />
|
||||
<TableHeaderCell title="Topics" />
|
||||
<TableHeaderCell title="Status" />
|
||||
<TableHeaderCell title="Running Tasks" />
|
||||
<TableHeaderCell> </TableHeaderCell>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
@ -109,7 +114,7 @@ const List: React.FC<ListProps> = ({
|
|||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import React from 'react';
|
||||
import cx from 'classnames';
|
||||
import { FullConnectorInfo } from 'generated-sources';
|
||||
import { clusterConnectConnectorPath, clusterTopicPath } from 'lib/paths';
|
||||
import { ClusterName } from 'redux/interfaces';
|
||||
|
@ -10,13 +9,22 @@ import Dropdown from 'components/common/Dropdown/Dropdown';
|
|||
import DropdownDivider from 'components/common/Dropdown/DropdownDivider';
|
||||
import DropdownItem from 'components/common/Dropdown/DropdownItem';
|
||||
import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
|
||||
import ConnectorStatusTag from 'components/Connect/ConnectorStatusTag';
|
||||
import TagStyled from 'components/common/Tag/Tag.styled';
|
||||
import { TableKeyLink } from 'components/common/table/Table/TableKeyLink.styled';
|
||||
import VerticalElipsisIcon from 'components/common/Icons/VerticalElipsisIcon';
|
||||
import { Colors } from 'theme/theme';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export interface ListItemProps {
|
||||
clusterName: ClusterName;
|
||||
connector: FullConnectorInfo;
|
||||
}
|
||||
|
||||
const TopicTagsWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
`;
|
||||
|
||||
const ListItem: React.FC<ListItemProps> = ({
|
||||
clusterName,
|
||||
connector: {
|
||||
|
@ -50,55 +58,42 @@ const ListItem: React.FC<ListItemProps> = ({
|
|||
|
||||
return (
|
||||
<tr>
|
||||
<td className="has-text-overflow-ellipsis">
|
||||
<TableKeyLink>
|
||||
<NavLink
|
||||
exact
|
||||
to={clusterConnectConnectorPath(clusterName, connect, name)}
|
||||
activeClassName="is-active"
|
||||
className="title is-6"
|
||||
>
|
||||
{name}
|
||||
</NavLink>
|
||||
</td>
|
||||
</TableKeyLink>
|
||||
<td>{connect}</td>
|
||||
<td>{type}</td>
|
||||
<td>{connectorClass}</td>
|
||||
<td>
|
||||
<div className="is-flex is-flex-wrap-wrap">
|
||||
<TopicTagsWrapper>
|
||||
{topics?.map((t) => (
|
||||
<span key={t} className="tag is-info is-light mr-1 mb-1">
|
||||
<TagStyled key={t} color="gray">
|
||||
<Link to={clusterTopicPath(clusterName, t)}>{t}</Link>
|
||||
</span>
|
||||
</TagStyled>
|
||||
))}
|
||||
</div>
|
||||
</TopicTagsWrapper>
|
||||
</td>
|
||||
<td>{status && <ConnectorStatusTag status={status.state} />}</td>
|
||||
<td>{status && <TagStyled color="yellow">{status.state}</TagStyled>}</td>
|
||||
<td>
|
||||
{runningTasks && (
|
||||
<span
|
||||
className={cx(
|
||||
failedTasksCount ? 'has-text-danger' : 'has-text-success'
|
||||
)}
|
||||
>
|
||||
<span>
|
||||
{runningTasks} of {tasksCount}
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
<div className="has-text-right">
|
||||
<Dropdown
|
||||
label={
|
||||
<span className="icon">
|
||||
<i className="fas fa-cog" />
|
||||
</span>
|
||||
}
|
||||
right
|
||||
>
|
||||
<div>
|
||||
<Dropdown label={<VerticalElipsisIcon />} right>
|
||||
<DropdownDivider />
|
||||
<DropdownItem
|
||||
onClick={() => setDeleteConnectorConfirmationVisible(true)}
|
||||
>
|
||||
<span className="has-text-danger">Remove Connector</span>
|
||||
<span style={{ color: Colors.red[50] }}>Remove Connector</span>
|
||||
</DropdownItem>
|
||||
</Dropdown>
|
||||
</div>
|
||||
|
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
|||
import { mount } from 'enzyme';
|
||||
import { Provider } from 'react-redux';
|
||||
import { StaticRouter } from 'react-router-dom';
|
||||
import configureStore from 'redux/store/configureStore';
|
||||
import { store } from 'redux/store';
|
||||
import { connectors } from 'redux/reducers/connect/__test__/fixtures';
|
||||
import ClusterContext, {
|
||||
ContextProps,
|
||||
|
@ -10,18 +10,20 @@ import ClusterContext, {
|
|||
} from 'components/contexts/ClusterContext';
|
||||
import ListContainer from 'components/Connect/List/ListContainer';
|
||||
import List, { ListProps } from 'components/Connect/List/List';
|
||||
|
||||
const store = configureStore();
|
||||
import { ThemeProvider } from 'styled-components';
|
||||
import theme from 'theme/theme';
|
||||
|
||||
describe('Connectors List', () => {
|
||||
describe('Container', () => {
|
||||
it('renders view with initial state of storage', () => {
|
||||
const wrapper = mount(
|
||||
<Provider store={store}>
|
||||
<StaticRouter>
|
||||
<ListContainer />
|
||||
</StaticRouter>
|
||||
</Provider>
|
||||
<ThemeProvider theme={theme}>
|
||||
<Provider store={store}>
|
||||
<StaticRouter>
|
||||
<ListContainer />
|
||||
</StaticRouter>
|
||||
</Provider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
expect(wrapper.exists(List)).toBeTruthy();
|
||||
|
@ -36,21 +38,25 @@ describe('Connectors List', () => {
|
|||
props: Partial<ListProps> = {},
|
||||
contextValue: ContextProps = initialValue
|
||||
) => (
|
||||
<StaticRouter>
|
||||
<ClusterContext.Provider value={contextValue}>
|
||||
<List
|
||||
areConnectorsFetching
|
||||
areConnectsFetching
|
||||
connectors={[]}
|
||||
connects={[]}
|
||||
fetchConnects={fetchConnects}
|
||||
fetchConnectors={fetchConnectors}
|
||||
search=""
|
||||
setConnectorSearch={setConnectorSearch}
|
||||
{...props}
|
||||
/>
|
||||
</ClusterContext.Provider>
|
||||
</StaticRouter>
|
||||
<ThemeProvider theme={theme}>
|
||||
<Provider store={store}>
|
||||
<StaticRouter>
|
||||
<ClusterContext.Provider value={contextValue}>
|
||||
<List
|
||||
areConnectorsFetching
|
||||
areConnectsFetching
|
||||
connectors={[]}
|
||||
connects={[]}
|
||||
fetchConnects={fetchConnects}
|
||||
fetchConnectors={fetchConnectors}
|
||||
search=""
|
||||
setConnectorSearch={setConnectorSearch}
|
||||
{...props}
|
||||
/>
|
||||
</ClusterContext.Provider>
|
||||
</StaticRouter>
|
||||
</Provider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
it('renders PageLoader', () => {
|
||||
|
@ -87,9 +93,7 @@ describe('Connectors List', () => {
|
|||
const wrapper = mount(
|
||||
setupComponent({}, { ...initialValue, isReadOnly: false })
|
||||
);
|
||||
expect(
|
||||
wrapper.exists('.level-item.level-right > .button.is-primary')
|
||||
).toBeTruthy();
|
||||
expect(wrapper.exists('button')).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('readonly cluster', () => {
|
||||
|
|
|
@ -3,16 +3,17 @@ import { mount } from 'enzyme';
|
|||
import { Provider } from 'react-redux';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { connectors } from 'redux/reducers/connect/__test__/fixtures';
|
||||
import configureStore from 'redux/store/configureStore';
|
||||
import { store } from 'redux/store';
|
||||
import ListItem, { ListItemProps } from 'components/Connect/List/ListItem';
|
||||
import { ConfirmationModalProps } from 'components/common/ConfirmationModal/ConfirmationModal';
|
||||
import { ThemeProvider } from 'styled-components';
|
||||
import theme from 'theme/theme';
|
||||
|
||||
const store = configureStore();
|
||||
const mockDeleteConnector = jest.fn(() => ({ type: 'test' }));
|
||||
|
||||
const mockDeleteConnector = jest.fn();
|
||||
jest.mock('redux/actions', () => ({
|
||||
...jest.requireActual('redux/actions'),
|
||||
deleteConnector: () => mockDeleteConnector(),
|
||||
deleteConnector: () => mockDeleteConnector,
|
||||
}));
|
||||
|
||||
jest.mock(
|
||||
|
@ -23,22 +24,22 @@ jest.mock(
|
|||
describe('Connectors ListItem', () => {
|
||||
const connector = connectors[0];
|
||||
const setupWrapper = (props: Partial<ListItemProps> = {}) => (
|
||||
<Provider store={store}>
|
||||
<BrowserRouter>
|
||||
<table>
|
||||
<tbody>
|
||||
<ListItem clusterName="local" connector={connector} {...props} />
|
||||
</tbody>
|
||||
</table>
|
||||
</BrowserRouter>
|
||||
</Provider>
|
||||
<ThemeProvider theme={theme}>
|
||||
<Provider store={store}>
|
||||
<BrowserRouter>
|
||||
<table>
|
||||
<tbody>
|
||||
<ListItem clusterName="local" connector={connector} {...props} />
|
||||
</tbody>
|
||||
</table>
|
||||
</BrowserRouter>
|
||||
</Provider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
it('renders item', () => {
|
||||
const wrapper = mount(setupWrapper());
|
||||
expect(wrapper.find('td').at(6).find('.has-text-success').text()).toEqual(
|
||||
'2 of 2'
|
||||
);
|
||||
expect(wrapper.find('td').at(6).text()).toEqual('2 of 2');
|
||||
});
|
||||
|
||||
it('renders item with failed tasks', () => {
|
||||
|
@ -50,9 +51,7 @@ describe('Connectors ListItem', () => {
|
|||
},
|
||||
})
|
||||
);
|
||||
expect(wrapper.find('td').at(6).find('.has-text-danger').text()).toEqual(
|
||||
'1 of 2'
|
||||
);
|
||||
expect(wrapper.find('td').at(6).text()).toEqual('1 of 2');
|
||||
});
|
||||
|
||||
it('does not render info about tasks if taksCount is undefined', () => {
|
||||
|
|
|
@ -1,246 +1,507 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Connectors ListItem matches snapshot 1`] = `
|
||||
<Provider
|
||||
store={
|
||||
.c5 {
|
||||
background: transparent;
|
||||
border: none;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-align-items: 'center';
|
||||
-webkit-box-align: 'center';
|
||||
-ms-flex-align: 'center';
|
||||
align-items: 'center';
|
||||
-webkit-box-pack: 'center';
|
||||
-webkit-justify-content: 'center';
|
||||
-ms-flex-pack: 'center';
|
||||
justify-content: 'center';
|
||||
}
|
||||
|
||||
.c5:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.c4 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-align-self: center;
|
||||
-ms-flex-item-align: center;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.c2 {
|
||||
border: none;
|
||||
border-radius: 16px;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
background-color: #E3E6E8;
|
||||
color: #171A1C;
|
||||
font-size: 12px;
|
||||
display: inline-block;
|
||||
padding-left: 0.75em;
|
||||
padding-right: 0.75em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.c3 {
|
||||
border: none;
|
||||
border-radius: 16px;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
background-color: #FFEECC;
|
||||
color: #171A1C;
|
||||
font-size: 12px;
|
||||
display: inline-block;
|
||||
padding-left: 0.75em;
|
||||
padding-right: 0.75em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.c0 > a {
|
||||
color: #171A1C;
|
||||
font-weight: 500;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.c1 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-flex-wrap: wrap;
|
||||
-ms-flex-wrap: wrap;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
<Component
|
||||
theme={
|
||||
Object {
|
||||
"@@observable": [Function],
|
||||
"dispatch": [Function],
|
||||
"getState": [Function],
|
||||
"replaceReducer": [Function],
|
||||
"subscribe": [Function],
|
||||
"alert": Object {
|
||||
"color": Object {
|
||||
"error": "#FAD1D1",
|
||||
"info": "#E3E6E8",
|
||||
"success": "#D6F5E0",
|
||||
"warning": "#FFEECC",
|
||||
},
|
||||
},
|
||||
"buttonStyles": Object {
|
||||
"fontSize": Object {
|
||||
"L": "16px",
|
||||
"M": "14px",
|
||||
"S": "14px",
|
||||
},
|
||||
"height": Object {
|
||||
"L": "40px",
|
||||
"M": "32px",
|
||||
"S": "24px",
|
||||
},
|
||||
"primary": Object {
|
||||
"backgroundColor": Object {
|
||||
"active": "#1414B8",
|
||||
"hover": "#1717CF",
|
||||
"normal": "#4F4FFF",
|
||||
},
|
||||
"color": "#FFFFFF",
|
||||
"invertedColors": Object {
|
||||
"active": "#1414B8",
|
||||
"hover": "#1717CF",
|
||||
"normal": "#4F4FFF",
|
||||
},
|
||||
},
|
||||
"secondary": Object {
|
||||
"backgroundColor": Object {
|
||||
"active": "#D5DADD",
|
||||
"hover": "#E3E6E8",
|
||||
"normal": "#F1F2F3",
|
||||
},
|
||||
"color": "#171A1C",
|
||||
"invertedColors": Object {
|
||||
"active": "#171A1C",
|
||||
"hover": "#454F54",
|
||||
"normal": "#73848C",
|
||||
},
|
||||
},
|
||||
},
|
||||
"layout": Object {
|
||||
"minWidth": "1200px",
|
||||
"navBarHeight": "3.25rem",
|
||||
"navBarWidth": "201px",
|
||||
},
|
||||
"menuStyles": Object {
|
||||
"backgroundColor": Object {
|
||||
"active": "#E3E6E8",
|
||||
"hover": "#F1F2F3",
|
||||
"normal": "#FFFFFF",
|
||||
},
|
||||
"chevronIconColor": "#73848C",
|
||||
"color": Object {
|
||||
"active": "#171A1C",
|
||||
"hover": "#73848C",
|
||||
"normal": "#73848C",
|
||||
},
|
||||
"statusIconColor": Object {
|
||||
"offline": "#E51A1A",
|
||||
"online": "#5CD685",
|
||||
},
|
||||
},
|
||||
"metrics": Object {
|
||||
"backgroundColor": "#F1F2F3",
|
||||
"indicator": Object {
|
||||
"backgroundColor": "#FFFFFF",
|
||||
"lightTextColor": "#ABB5BA",
|
||||
"titleColor": "#73848C",
|
||||
"warningTextColor": "#E51A1A",
|
||||
},
|
||||
},
|
||||
"pageLoader": Object {
|
||||
"borderBottomColor": "#FFFFFF",
|
||||
"borderColor": "#4F4FFF",
|
||||
},
|
||||
"paginationStyles": Object {
|
||||
"borderColor": Object {
|
||||
"active": "#454F54",
|
||||
"disabled": "#C7CED1",
|
||||
"hover": "#73848C",
|
||||
"normal": "#ABB5BA",
|
||||
},
|
||||
"color": Object {
|
||||
"active": "#171A1C",
|
||||
"disabled": "#C7CED1",
|
||||
"hover": "#171A1C",
|
||||
"normal": "#171A1C",
|
||||
},
|
||||
},
|
||||
"primaryTabStyles": Object {
|
||||
"borderColor": Object {
|
||||
"active": "#4F4FFF",
|
||||
"hover": "transparent",
|
||||
"normal": "transparent",
|
||||
},
|
||||
"color": Object {
|
||||
"active": "#171A1C",
|
||||
"hover": "#171A1C",
|
||||
"normal": "#73848C",
|
||||
},
|
||||
},
|
||||
"secondaryTabStyles": Object {
|
||||
"backgroundColor": Object {
|
||||
"active": "#E3E6E8",
|
||||
"hover": "#F1F2F3",
|
||||
"normal": "#FFFFFF",
|
||||
},
|
||||
"color": Object {
|
||||
"active": "#171A1C",
|
||||
"hover": "#171A1C",
|
||||
"normal": "#73848C",
|
||||
},
|
||||
},
|
||||
"selectStyles": Object {
|
||||
"borderColor": Object {
|
||||
"active": "#454F54",
|
||||
"disabled": "#E3E6E8",
|
||||
"hover": "#73848C",
|
||||
"normal": "#ABB5BA",
|
||||
},
|
||||
"color": Object {
|
||||
"active": "#171A1C",
|
||||
"disabled": "#ABB5BA",
|
||||
"hover": "#171A1C",
|
||||
"normal": "#171A1C",
|
||||
},
|
||||
},
|
||||
"switch": Object {
|
||||
"checked": "#29A352",
|
||||
"unchecked": "#ABB5BA",
|
||||
},
|
||||
"tagStyles": Object {
|
||||
"backgroundColor": Object {
|
||||
"gray": "#E3E6E8",
|
||||
"green": "#D6F5E0",
|
||||
"yellow": "#FFEECC",
|
||||
},
|
||||
"color": "#171A1C",
|
||||
},
|
||||
"thStyles": Object {
|
||||
"backgroundColor": Object {
|
||||
"normal": "#FFFFFF",
|
||||
},
|
||||
"color": Object {
|
||||
"normal": "#73848C",
|
||||
},
|
||||
"previewColor": Object {
|
||||
"normal": "#4F4FFF",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
<BrowserRouter>
|
||||
<Router
|
||||
history={
|
||||
Object {
|
||||
"action": "POP",
|
||||
"block": [Function],
|
||||
"createHref": [Function],
|
||||
"go": [Function],
|
||||
"goBack": [Function],
|
||||
"goForward": [Function],
|
||||
"length": 1,
|
||||
"listen": [Function],
|
||||
"location": Object {
|
||||
"hash": "",
|
||||
"pathname": "/",
|
||||
"search": "",
|
||||
"state": undefined,
|
||||
},
|
||||
"push": [Function],
|
||||
"replace": [Function],
|
||||
}
|
||||
<Provider
|
||||
store={
|
||||
Object {
|
||||
"@@observable": [Function],
|
||||
"dispatch": [Function],
|
||||
"getState": [Function],
|
||||
"replaceReducer": [Function],
|
||||
"subscribe": [Function],
|
||||
}
|
||||
>
|
||||
<table>
|
||||
<tbody>
|
||||
<ListItem
|
||||
clusterName="local"
|
||||
connector={
|
||||
Object {
|
||||
"connect": "first",
|
||||
"connectorClass": "FileStreamSource",
|
||||
"failedTasksCount": 0,
|
||||
"name": "hdfs-source-connector",
|
||||
"status": Object {
|
||||
"state": "RUNNING",
|
||||
},
|
||||
"tasksCount": 2,
|
||||
"topics": Array [
|
||||
"test-topic",
|
||||
],
|
||||
"type": "SOURCE",
|
||||
}
|
||||
>
|
||||
<BrowserRouter>
|
||||
<Router
|
||||
history={
|
||||
Object {
|
||||
"action": "POP",
|
||||
"block": [Function],
|
||||
"createHref": [Function],
|
||||
"go": [Function],
|
||||
"goBack": [Function],
|
||||
"goForward": [Function],
|
||||
"length": 1,
|
||||
"listen": [Function],
|
||||
"location": Object {
|
||||
"hash": "",
|
||||
"pathname": "/",
|
||||
"search": "",
|
||||
"state": undefined,
|
||||
},
|
||||
"push": [Function],
|
||||
"replace": [Function],
|
||||
}
|
||||
}
|
||||
>
|
||||
<table>
|
||||
<tbody>
|
||||
<ListItem
|
||||
clusterName="local"
|
||||
connector={
|
||||
Object {
|
||||
"connect": "first",
|
||||
"connectorClass": "FileStreamSource",
|
||||
"failedTasksCount": 0,
|
||||
"name": "hdfs-source-connector",
|
||||
"status": Object {
|
||||
"state": "RUNNING",
|
||||
},
|
||||
"tasksCount": 2,
|
||||
"topics": Array [
|
||||
"test-topic",
|
||||
],
|
||||
"type": "SOURCE",
|
||||
}
|
||||
}
|
||||
}
|
||||
>
|
||||
<tr>
|
||||
<td
|
||||
className="has-text-overflow-ellipsis"
|
||||
>
|
||||
<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,
|
||||
}
|
||||
}
|
||||
>
|
||||
<tr>
|
||||
<styled.td>
|
||||
<td
|
||||
className="c0"
|
||||
>
|
||||
<LinkAnchor
|
||||
aria-current={null}
|
||||
className="title is-6"
|
||||
href="/ui/clusters/local/connects/first/connectors/hdfs-source-connector"
|
||||
navigate={[Function]}
|
||||
<NavLink
|
||||
exact={true}
|
||||
to="/ui/clusters/local/connects/first/connectors/hdfs-source-connector"
|
||||
>
|
||||
<a
|
||||
<Link
|
||||
aria-current={null}
|
||||
className="title is-6"
|
||||
href="/ui/clusters/local/connects/first/connectors/hdfs-source-connector"
|
||||
onClick={[Function]}
|
||||
to={
|
||||
Object {
|
||||
"hash": "",
|
||||
"pathname": "/ui/clusters/local/connects/first/connectors/hdfs-source-connector",
|
||||
"search": "",
|
||||
"state": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
hdfs-source-connector
|
||||
</a>
|
||||
</LinkAnchor>
|
||||
</Link>
|
||||
</NavLink>
|
||||
</td>
|
||||
<td>
|
||||
first
|
||||
</td>
|
||||
<td>
|
||||
SOURCE
|
||||
</td>
|
||||
<td>
|
||||
FileStreamSource
|
||||
</td>
|
||||
<td>
|
||||
<div
|
||||
className="is-flex is-flex-wrap-wrap"
|
||||
>
|
||||
<span
|
||||
className="tag is-info is-light mr-1 mb-1"
|
||||
key="test-topic"
|
||||
>
|
||||
<Link
|
||||
to="/ui/clusters/local/topics/test-topic"
|
||||
>
|
||||
<LinkAnchor
|
||||
href="/ui/clusters/local/topics/test-topic"
|
||||
navigate={[Function]}
|
||||
>
|
||||
<a
|
||||
href="/ui/clusters/local/topics/test-topic"
|
||||
onClick={[Function]}
|
||||
<LinkAnchor
|
||||
aria-current={null}
|
||||
href="/ui/clusters/local/connects/first/connectors/hdfs-source-connector"
|
||||
navigate={[Function]}
|
||||
>
|
||||
test-topic
|
||||
</a>
|
||||
</LinkAnchor>
|
||||
</Link>
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<ConnectorStatusTag
|
||||
status="RUNNING"
|
||||
>
|
||||
<span
|
||||
className="tag is-success"
|
||||
>
|
||||
RUNNING
|
||||
</span>
|
||||
</ConnectorStatusTag>
|
||||
</td>
|
||||
<td>
|
||||
<span
|
||||
className="has-text-success"
|
||||
>
|
||||
2
|
||||
of
|
||||
2
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<div
|
||||
className="has-text-right"
|
||||
>
|
||||
<Dropdown
|
||||
label={
|
||||
<span
|
||||
className="icon"
|
||||
>
|
||||
<i
|
||||
className="fas fa-cog"
|
||||
/>
|
||||
</span>
|
||||
}
|
||||
right={true}
|
||||
>
|
||||
<div
|
||||
className="dropdown is-right"
|
||||
>
|
||||
<div
|
||||
className="dropdown-trigger"
|
||||
>
|
||||
<button
|
||||
aria-controls="dropdown-menu"
|
||||
aria-haspopup="true"
|
||||
className="button is-small is-link"
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
className="icon"
|
||||
>
|
||||
<i
|
||||
className="fas fa-cog"
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-menu"
|
||||
id="dropdown-menu"
|
||||
role="menu"
|
||||
>
|
||||
<div
|
||||
className="dropdown-content has-text-left"
|
||||
>
|
||||
<DropdownDivider>
|
||||
<hr
|
||||
className="dropdown-divider"
|
||||
/>
|
||||
</DropdownDivider>
|
||||
<DropdownItem
|
||||
<a
|
||||
aria-current={null}
|
||||
href="/ui/clusters/local/connects/first/connectors/hdfs-source-connector"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<a
|
||||
className="dropdown-item is-link"
|
||||
href="#end"
|
||||
onClick={[Function]}
|
||||
role="menuitem"
|
||||
type="button"
|
||||
hdfs-source-connector
|
||||
</a>
|
||||
</LinkAnchor>
|
||||
</Link>
|
||||
</NavLink>
|
||||
</td>
|
||||
</styled.td>
|
||||
<td>
|
||||
first
|
||||
</td>
|
||||
<td>
|
||||
SOURCE
|
||||
</td>
|
||||
<td>
|
||||
FileStreamSource
|
||||
</td>
|
||||
<td>
|
||||
<styled.div>
|
||||
<div
|
||||
className="c1"
|
||||
>
|
||||
<Styled(Tag)
|
||||
color="gray"
|
||||
key="test-topic"
|
||||
>
|
||||
<Tag
|
||||
className="c2"
|
||||
color="gray"
|
||||
>
|
||||
<p
|
||||
className="c2"
|
||||
>
|
||||
<Link
|
||||
to="/ui/clusters/local/topics/test-topic"
|
||||
>
|
||||
<span
|
||||
className="has-text-danger"
|
||||
<LinkAnchor
|
||||
href="/ui/clusters/local/topics/test-topic"
|
||||
navigate={[Function]}
|
||||
>
|
||||
Remove Connector
|
||||
</span>
|
||||
</a>
|
||||
</DropdownItem>
|
||||
<a
|
||||
href="/ui/clusters/local/topics/test-topic"
|
||||
onClick={[Function]}
|
||||
>
|
||||
test-topic
|
||||
</a>
|
||||
</LinkAnchor>
|
||||
</Link>
|
||||
</p>
|
||||
</Tag>
|
||||
</Styled(Tag)>
|
||||
</div>
|
||||
</styled.div>
|
||||
</td>
|
||||
<td>
|
||||
<Styled(Tag)
|
||||
color="yellow"
|
||||
>
|
||||
<Tag
|
||||
className="c3"
|
||||
color="yellow"
|
||||
>
|
||||
<p
|
||||
className="c3"
|
||||
>
|
||||
RUNNING
|
||||
</p>
|
||||
</Tag>
|
||||
</Styled(Tag)>
|
||||
</td>
|
||||
<td>
|
||||
<span>
|
||||
2
|
||||
of
|
||||
2
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<div>
|
||||
<Dropdown
|
||||
label={<VerticalElipsisIcon />}
|
||||
right={true}
|
||||
>
|
||||
<div
|
||||
className="dropdown is-right"
|
||||
>
|
||||
<styled.div>
|
||||
<div
|
||||
className="c4"
|
||||
>
|
||||
<Styled(DropdownTrigger)
|
||||
onClick={[Function]}
|
||||
>
|
||||
<DropdownTrigger
|
||||
className="c5"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<button
|
||||
aria-controls="dropdown-menu"
|
||||
aria-haspopup="true"
|
||||
className="c5"
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<VerticalElipsisIcon>
|
||||
<svg
|
||||
fill="none"
|
||||
height="16"
|
||||
viewBox="0 0 4 16"
|
||||
width="4"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M2 4C3.1 4 4 3.1 4 2C4 0.9 3.1 0 2 0C0.9 0 0 0.9 0 2C0 3.1 0.9 4 2 4ZM2 6C0.9 6 0 6.9 0 8C0 9.1 0.9 10 2 10C3.1 10 4 9.1 4 8C4 6.9 3.1 6 2 6ZM2 12C0.9 12 0 12.9 0 14C0 15.1 0.9 16 2 16C3.1 16 4 15.1 4 14C4 12.9 3.1 12 2 12Z"
|
||||
fill="#73848C"
|
||||
/>
|
||||
</svg>
|
||||
</VerticalElipsisIcon>
|
||||
</button>
|
||||
</DropdownTrigger>
|
||||
</Styled(DropdownTrigger)>
|
||||
</div>
|
||||
</styled.div>
|
||||
<div
|
||||
className="dropdown-menu"
|
||||
id="dropdown-menu"
|
||||
role="menu"
|
||||
>
|
||||
<div
|
||||
className="dropdown-content has-text-left"
|
||||
>
|
||||
<DropdownDivider>
|
||||
<hr
|
||||
className="dropdown-divider"
|
||||
/>
|
||||
</DropdownDivider>
|
||||
<DropdownItem
|
||||
onClick={[Function]}
|
||||
>
|
||||
<a
|
||||
className="dropdown-item is-link"
|
||||
href="#end"
|
||||
onClick={[Function]}
|
||||
role="menuitem"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
style={
|
||||
Object {
|
||||
"color": "#E51A1A",
|
||||
}
|
||||
}
|
||||
>
|
||||
Remove Connector
|
||||
</span>
|
||||
</a>
|
||||
</DropdownItem>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
<mock-ConfirmationModal
|
||||
isOpen={false}
|
||||
onCancel={[Function]}
|
||||
onConfirm={[Function]}
|
||||
>
|
||||
Are you sure want to remove
|
||||
<b>
|
||||
hdfs-source-connector
|
||||
</b>
|
||||
connector?
|
||||
</mock-ConfirmationModal>
|
||||
</td>
|
||||
</tr>
|
||||
</ListItem>
|
||||
</tbody>
|
||||
</table>
|
||||
</Router>
|
||||
</BrowserRouter>
|
||||
</Provider>
|
||||
</Dropdown>
|
||||
</div>
|
||||
<mock-ConfirmationModal
|
||||
isOpen={false}
|
||||
onCancel={[Function]}
|
||||
onConfirm={[Function]}
|
||||
>
|
||||
Are you sure want to remove
|
||||
<b>
|
||||
hdfs-source-connector
|
||||
</b>
|
||||
connector?
|
||||
</mock-ConfirmationModal>
|
||||
</td>
|
||||
</tr>
|
||||
</ListItem>
|
||||
</tbody>
|
||||
</table>
|
||||
</Router>
|
||||
</BrowserRouter>
|
||||
</Provider>
|
||||
</Component>
|
||||
`;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
import { useHistory, useParams } from 'react-router-dom';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { Controller, FormProvider, useForm } from 'react-hook-form';
|
||||
import { ErrorMessage } from '@hookform/error-message';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import { Connect, Connector, NewConnector } from 'generated-sources';
|
||||
|
@ -9,6 +9,13 @@ import { clusterConnectConnectorPath } from 'lib/paths';
|
|||
import yup from 'lib/yupExtended';
|
||||
import JSONEditor from 'components/common/JSONEditor/JSONEditor';
|
||||
import PageLoader from 'components/common/PageLoader/PageLoader';
|
||||
import { InputLabel } from 'components/common/Input/InputLabel.styled';
|
||||
import Select from 'components/common/Select/Select';
|
||||
import { FormError } from 'components/common/Input/Input.styled';
|
||||
import Input from 'components/common/Input/Input';
|
||||
import { Button } from 'components/common/Button/Button';
|
||||
import styled from 'styled-components';
|
||||
import PageHeading from 'components/common/PageHeading/PageHeading';
|
||||
|
||||
const validationSchema = yup.object().shape({
|
||||
name: yup.string().required(),
|
||||
|
@ -19,6 +26,17 @@ interface RouterParams {
|
|||
clusterName: ClusterName;
|
||||
}
|
||||
|
||||
const NewConnectFormStyled = styled.form`
|
||||
padding: 16px;
|
||||
padding-top: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
& > button:last-child {
|
||||
align-self: flex-start;
|
||||
}
|
||||
`;
|
||||
|
||||
export interface NewProps {
|
||||
fetchConnects(clusterName: ClusterName): void;
|
||||
areConnectsFetching: boolean;
|
||||
|
@ -45,14 +63,7 @@ const New: React.FC<NewProps> = ({
|
|||
const { clusterName } = useParams<RouterParams>();
|
||||
const history = useHistory();
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
control,
|
||||
formState: { isDirty, isSubmitting, isValid, errors },
|
||||
getValues,
|
||||
setValue,
|
||||
} = useForm<FormValues>({
|
||||
const methods = useForm<FormValues>({
|
||||
mode: 'onTouched',
|
||||
resolver: yupResolver(validationSchema),
|
||||
defaultValues: {
|
||||
|
@ -61,6 +72,13 @@ const New: React.FC<NewProps> = ({
|
|||
config: '',
|
||||
},
|
||||
});
|
||||
const {
|
||||
handleSubmit,
|
||||
control,
|
||||
formState: { isDirty, isSubmitting, isValid, errors },
|
||||
getValues,
|
||||
setValue,
|
||||
} = methods;
|
||||
|
||||
React.useEffect(() => {
|
||||
fetchConnects(clusterName);
|
||||
|
@ -105,66 +123,60 @@ const New: React.FC<NewProps> = ({
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="box">
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<FormProvider {...methods}>
|
||||
<PageHeading text="Create new connector" />
|
||||
<NewConnectFormStyled onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className={['field', connectNameFieldClassName].join(' ')}>
|
||||
<label className="label">Connect *</label>
|
||||
<div className="control select">
|
||||
<select {...register('connectName')} disabled={isSubmitting}>
|
||||
{connects.map(({ name }) => (
|
||||
<option key={name} value={name}>
|
||||
{name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<p className="help is-danger">
|
||||
<InputLabel>Connect *</InputLabel>
|
||||
<Select selectSize="M" name="connectName" disabled={isSubmitting}>
|
||||
{connects.map(({ name }) => (
|
||||
<option key={name} value={name}>
|
||||
{name}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
<FormError>
|
||||
<ErrorMessage errors={errors} name="connectName" />
|
||||
</p>
|
||||
</FormError>
|
||||
</div>
|
||||
|
||||
<div className="field">
|
||||
<label className="label">Name *</label>
|
||||
<div className="control">
|
||||
<input
|
||||
className="input"
|
||||
placeholder="Connector Name"
|
||||
{...register('name')}
|
||||
autoComplete="off"
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
<p className="help is-danger">
|
||||
<div>
|
||||
<InputLabel>Name *</InputLabel>
|
||||
<Input
|
||||
inputSize="M"
|
||||
placeholder="Connector Name"
|
||||
name="name"
|
||||
autoComplete="off"
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
<FormError>
|
||||
<ErrorMessage errors={errors} name="name" />
|
||||
</p>
|
||||
</FormError>
|
||||
</div>
|
||||
|
||||
<div className="field">
|
||||
<label className="label">Config *</label>
|
||||
<div className="control">
|
||||
<Controller
|
||||
control={control}
|
||||
name="config"
|
||||
render={({ field }) => (
|
||||
<JSONEditor {...field} readOnly={isSubmitting} />
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<p className="help is-danger">
|
||||
<div>
|
||||
<InputLabel>Config *</InputLabel>
|
||||
<Controller
|
||||
control={control}
|
||||
name="config"
|
||||
render={({ field }) => (
|
||||
<JSONEditor {...field} readOnly={isSubmitting} />
|
||||
)}
|
||||
/>
|
||||
<FormError>
|
||||
<ErrorMessage errors={errors} name="config" />
|
||||
</p>
|
||||
</FormError>
|
||||
</div>
|
||||
<div className="field">
|
||||
<div className="control">
|
||||
<input
|
||||
type="submit"
|
||||
className="button is-primary"
|
||||
disabled={!isValid || isSubmitting || !isDirty}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<Button
|
||||
buttonSize="M"
|
||||
buttonType="primary"
|
||||
type="submit"
|
||||
disabled={!isValid || isSubmitting || !isDirty}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</NewConnectFormStyled>
|
||||
</FormProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -10,6 +10,8 @@ import {
|
|||
import NewContainer from 'components/Connect/New/NewContainer';
|
||||
import New, { NewProps } from 'components/Connect/New/New';
|
||||
import { connects, connector } from 'redux/reducers/connect/__test__/fixtures';
|
||||
import { ThemeProvider } from 'styled-components';
|
||||
import theme from 'theme/theme';
|
||||
|
||||
jest.mock('components/common/PageLoader/PageLoader', () => 'mock-PageLoader');
|
||||
|
||||
|
@ -37,19 +39,21 @@ describe('New', () => {
|
|||
wrapper
|
||||
.find('mock-JSONEditor')
|
||||
.simulate('change', { target: { value: '{"class":"MyClass"}' } });
|
||||
wrapper.find('input[type="submit"]').simulate('submit');
|
||||
wrapper.find('button[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>
|
||||
<ThemeProvider theme={theme}>
|
||||
<TestRouterWrapper pathname={pathname} urlParams={{ clusterName }}>
|
||||
<New
|
||||
fetchConnects={jest.fn()}
|
||||
areConnectsFetching={false}
|
||||
connects={connects}
|
||||
createConnector={jest.fn()}
|
||||
{...props}
|
||||
/>
|
||||
</TestRouterWrapper>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
it('matches snapshot', async () => {
|
||||
|
|
|
@ -1,24 +1,263 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`New view matches snapshot 1`] = `
|
||||
Array [
|
||||
.c0 {
|
||||
height: 56px;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-pack: justify;
|
||||
-webkit-justify-content: space-between;
|
||||
-ms-flex-pack: justify;
|
||||
justify-content: space-between;
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
padding: 0px 16px;
|
||||
}
|
||||
|
||||
.c0 h1 {
|
||||
font-size: 24px;
|
||||
font-weight: 500;
|
||||
line-height: 32px;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.c0 > div {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
<div
|
||||
className="box"
|
||||
>
|
||||
<form
|
||||
className="c0"
|
||||
>
|
||||
<h1>
|
||||
Create new connector
|
||||
</h1>
|
||||
<div />
|
||||
</div>,
|
||||
.c1 {
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
color: #454F54;
|
||||
}
|
||||
|
||||
.c3 {
|
||||
height: 32px;
|
||||
border: 1px #ABB5BA solid;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
width: 100%;
|
||||
padding-left: 12px;
|
||||
padding-right: 16px;
|
||||
color: #171A1C;
|
||||
background-image: url('data:image/svg+xml,%3Csvg width="10" height="6" viewBox="0 0 10 6" fill="none" xmlns="http://www.w3.org/2000/svg"%3E%3Cpath d="M1 1L5 5L9 1" stroke="%23454F54"/%3E%3C/svg%3E%0A') !important;
|
||||
background-repeat: no-repeat !important;
|
||||
background-position-x: calc(100% - 8px) !important;
|
||||
background-position-y: 55% !important;
|
||||
-webkit-appearance: none !important;
|
||||
-moz-appearance: none !important;
|
||||
appearance: none !important;
|
||||
}
|
||||
|
||||
.c3:hover {
|
||||
color: #171A1C;
|
||||
border-color: #73848C;
|
||||
}
|
||||
|
||||
.c3:focus {
|
||||
outline: none;
|
||||
color: #171A1C;
|
||||
border-color: #454F54;
|
||||
}
|
||||
|
||||
.c3:disabled {
|
||||
color: #ABB5BA;
|
||||
border-color: #E3E6E8;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.c2 {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.c6 {
|
||||
border: 1px #ABB5BA solid;
|
||||
border-radius: 4px;
|
||||
height: 32px;
|
||||
width: 100%;
|
||||
padding-left: 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.c6::-webkit-input-placeholder {
|
||||
color: #ABB5BA;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.c6::-moz-placeholder {
|
||||
color: #ABB5BA;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.c6:-ms-input-placeholder {
|
||||
color: #ABB5BA;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.c6::placeholder {
|
||||
color: #ABB5BA;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.c6:hover {
|
||||
border-color: #73848C;
|
||||
}
|
||||
|
||||
.c6:focus {
|
||||
outline: none;
|
||||
border-color: #454F54;
|
||||
}
|
||||
|
||||
.c6:focus::-webkit-input-placeholder {
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.c6:focus::-moz-placeholder {
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.c6:focus:-ms-input-placeholder {
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.c6:focus::placeholder {
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.c6:disabled {
|
||||
color: #ABB5BA;
|
||||
border-color: #E3E6E8;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.c6:read-only {
|
||||
color: #171A1C;
|
||||
border: none;
|
||||
background-color: #F1F2F3;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.c6:-moz-read-only:focus::placeholder {
|
||||
color: #ABB5BA;
|
||||
}
|
||||
|
||||
.c6:read-only:focus::placeholder {
|
||||
color: #ABB5BA;
|
||||
}
|
||||
|
||||
.c4 {
|
||||
color: #E51A1A;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.c5 {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.c7 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-flex-direction: row;
|
||||
-ms-flex-direction: row;
|
||||
flex-direction: row;
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
-webkit-box-pack: center;
|
||||
-webkit-justify-content: center;
|
||||
-ms-flex-pack: center;
|
||||
justify-content: center;
|
||||
padding: 0px 12px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
white-space: nowrap;
|
||||
background: #4F4FFF;
|
||||
color: #FFFFFF;
|
||||
font-size: 14px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.c7:hover:enabled {
|
||||
background: #1717CF;
|
||||
color: #FFFFFF;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.c7:active:enabled {
|
||||
background: #1414B8;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.c7:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.c7 a {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.c7 i {
|
||||
margin-right: 7px;
|
||||
}
|
||||
|
||||
.c0 {
|
||||
padding: 16px;
|
||||
padding-top: 0;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-flex-direction: column;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.c0 > button:last-child {
|
||||
-webkit-align-self: flex-start;
|
||||
-ms-flex-item-align: start;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
<form
|
||||
className="c0"
|
||||
onSubmit={[Function]}
|
||||
>
|
||||
<div
|
||||
className="field "
|
||||
>
|
||||
<label
|
||||
className="label"
|
||||
className="c1"
|
||||
>
|
||||
Connect *
|
||||
</label>
|
||||
<div
|
||||
className="control select"
|
||||
className="select-wrapper c2"
|
||||
>
|
||||
<select
|
||||
className="c3"
|
||||
disabled={false}
|
||||
name="connectName"
|
||||
onBlur={[Function]}
|
||||
|
@ -37,23 +276,21 @@ exports[`New view matches snapshot 1`] = `
|
|||
</select>
|
||||
</div>
|
||||
<p
|
||||
className="help is-danger"
|
||||
className="c4"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="field"
|
||||
>
|
||||
<div>
|
||||
<label
|
||||
className="label"
|
||||
className="c1"
|
||||
>
|
||||
Name *
|
||||
</label>
|
||||
<div
|
||||
className="control"
|
||||
className="c5"
|
||||
>
|
||||
<input
|
||||
autoComplete="off"
|
||||
className="input"
|
||||
className="c6 c5"
|
||||
disabled={false}
|
||||
name="name"
|
||||
onBlur={[Function]}
|
||||
|
@ -62,47 +299,35 @@ exports[`New view matches snapshot 1`] = `
|
|||
/>
|
||||
</div>
|
||||
<p
|
||||
className="help is-danger"
|
||||
className="c4"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="field"
|
||||
>
|
||||
<div>
|
||||
<label
|
||||
className="label"
|
||||
className="c1"
|
||||
>
|
||||
Config *
|
||||
</label>
|
||||
<div
|
||||
className="control"
|
||||
>
|
||||
<mock-JSONEditor
|
||||
name="config"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
readOnly={false}
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
<mock-JSONEditor
|
||||
name="config"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
readOnly={false}
|
||||
value=""
|
||||
/>
|
||||
<p
|
||||
className="help is-danger"
|
||||
className="c4"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="field"
|
||||
<button
|
||||
className="c7"
|
||||
disabled={true}
|
||||
type="submit"
|
||||
>
|
||||
<div
|
||||
className="control"
|
||||
>
|
||||
<input
|
||||
className="button is-primary"
|
||||
disabled={true}
|
||||
type="submit"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
Submit
|
||||
</button>
|
||||
</form>,
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`New view matches snapshot when fetching connects 1`] = `<mock-PageLoader />`;
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
import cx from 'classnames';
|
||||
import { ConnectorTaskStatus } from 'generated-sources';
|
||||
import React from 'react';
|
||||
|
||||
export interface StatusTagProps {
|
||||
status: ConnectorTaskStatus;
|
||||
}
|
||||
|
||||
const StatusTag: React.FC<StatusTagProps> = ({ status }) => {
|
||||
const classNames = cx('tag', {
|
||||
'is-success': status === ConnectorTaskStatus.RUNNING,
|
||||
'is-light': status === ConnectorTaskStatus.PAUSED,
|
||||
'is-warning': status === ConnectorTaskStatus.UNASSIGNED,
|
||||
'is-danger': status === ConnectorTaskStatus.FAILED,
|
||||
});
|
||||
|
||||
return <span className={classNames}>{status}</span>;
|
||||
};
|
||||
|
||||
export default StatusTag;
|
|
@ -1,38 +0,0 @@
|
|||
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();
|
||||
});
|
||||
});
|
|
@ -1,19 +1,7 @@
|
|||
// 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>
|
||||
<div>
|
||||
<Switch>
|
||||
<Route
|
||||
component={
|
||||
|
@ -30,7 +18,7 @@ exports[`Connect matches snapshot 1`] = `
|
|||
<Route
|
||||
component={[Function]}
|
||||
exact={true}
|
||||
path="/ui/clusters/:clusterName/connectors/create_new"
|
||||
path="/ui/clusters/:clusterName/connectors/create-new"
|
||||
/>
|
||||
<Route
|
||||
component={[Function]}
|
||||
|
|
|
@ -1,33 +0,0 @@
|
|||
// 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>
|
||||
`;
|
|
@ -1,26 +1,23 @@
|
|||
import React from 'react';
|
||||
import { ClusterName } from 'redux/interfaces';
|
||||
import { Switch, Route } from 'react-router-dom';
|
||||
import { Switch, Route, useParams } from 'react-router-dom';
|
||||
import PageLoader from 'components/common/PageLoader/PageLoader';
|
||||
import DetailsContainer from 'components/ConsumerGroups/Details/DetailsContainer';
|
||||
import ListContainer from 'components/ConsumerGroups/List/ListContainer';
|
||||
import Details from 'components/ConsumerGroups/Details/Details';
|
||||
import List from 'components/ConsumerGroups/List/List';
|
||||
import ResetOffsets from 'components/ConsumerGroups/Details/ResetOffsets/ResetOffsets';
|
||||
import { useAppDispatch, useAppSelector } from 'lib/hooks/redux';
|
||||
import {
|
||||
fetchConsumerGroups,
|
||||
getAreConsumerGroupsFulfilled,
|
||||
} from 'redux/reducers/consumerGroups/consumerGroupsSlice';
|
||||
|
||||
import ResetOffsetsContainer from './Details/ResetOffsets/ResetOffsetsContainer';
|
||||
|
||||
interface Props {
|
||||
clusterName: ClusterName;
|
||||
isFetched: boolean;
|
||||
fetchConsumerGroupsList: (clusterName: ClusterName) => void;
|
||||
}
|
||||
|
||||
const ConsumerGroups: React.FC<Props> = ({
|
||||
clusterName,
|
||||
isFetched,
|
||||
fetchConsumerGroupsList,
|
||||
}) => {
|
||||
const ConsumerGroups: React.FC = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { clusterName } = useParams<{ clusterName: ClusterName }>();
|
||||
const isFetched = useAppSelector(getAreConsumerGroupsFulfilled);
|
||||
React.useEffect(() => {
|
||||
fetchConsumerGroupsList(clusterName);
|
||||
}, [fetchConsumerGroupsList, clusterName]);
|
||||
dispatch(fetchConsumerGroups(clusterName));
|
||||
}, [fetchConsumerGroups, clusterName]);
|
||||
|
||||
if (isFetched) {
|
||||
return (
|
||||
|
@ -28,16 +25,16 @@ const ConsumerGroups: React.FC<Props> = ({
|
|||
<Route
|
||||
exact
|
||||
path="/ui/clusters/:clusterName/consumer-groups"
|
||||
component={ListContainer}
|
||||
component={List}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/ui/clusters/:clusterName/consumer-groups/:consumerGroupID"
|
||||
component={DetailsContainer}
|
||||
component={Details}
|
||||
/>
|
||||
<Route
|
||||
path="/ui/clusters/:clusterName/consumer-groups/:consumerGroupID/reset-offsets"
|
||||
component={ResetOffsetsContainer}
|
||||
component={ResetOffsets}
|
||||
/>
|
||||
</Switch>
|
||||
);
|
||||
|
|
|
@ -1,32 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { fetchConsumerGroupsList } from 'redux/actions';
|
||||
import { RootState, ClusterName } from 'redux/interfaces';
|
||||
import { RouteComponentProps } from 'react-router-dom';
|
||||
import { getIsConsumerGroupsListFetched } from 'redux/reducers/consumerGroups/selectors';
|
||||
|
||||
import ConsumerGroups from './ConsumerGroups';
|
||||
|
||||
interface RouteProps {
|
||||
clusterName: ClusterName;
|
||||
}
|
||||
|
||||
type OwnProps = RouteComponentProps<RouteProps>;
|
||||
|
||||
const mapStateToProps = (
|
||||
state: RootState,
|
||||
{
|
||||
match: {
|
||||
params: { clusterName },
|
||||
},
|
||||
}: OwnProps
|
||||
) => ({
|
||||
isFetched: getIsConsumerGroupsListFetched(state),
|
||||
clusterName,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchConsumerGroupsList: (clusterName: ClusterName) =>
|
||||
fetchConsumerGroupsList(clusterName),
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ConsumerGroups);
|
|
@ -1,56 +1,57 @@
|
|||
import React from 'react';
|
||||
import { ClusterName } from 'redux/interfaces';
|
||||
import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb';
|
||||
import {
|
||||
clusterConsumerGroupResetOffsetsPath,
|
||||
clusterConsumerGroupsPath,
|
||||
} from 'lib/paths';
|
||||
import { ConsumerGroupID } from 'redux/interfaces/consumerGroup';
|
||||
import {
|
||||
ConsumerGroup,
|
||||
ConsumerGroupDetails,
|
||||
ConsumerGroupTopicPartition,
|
||||
} from 'generated-sources';
|
||||
import PageLoader from 'components/common/PageLoader/PageLoader';
|
||||
import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
|
||||
import { useHistory } from 'react-router';
|
||||
import { useHistory, useParams } from 'react-router';
|
||||
import ClusterContext from 'components/contexts/ClusterContext';
|
||||
import PageHeading from 'components/common/PageHeading/PageHeading';
|
||||
import VerticalElipsisIcon from 'components/common/Icons/VerticalElipsisIcon';
|
||||
import * as Metrics from 'components/common/Metrics';
|
||||
import TagStyled from 'components/common/Tag/Tag.styled';
|
||||
import Dropdown from 'components/common/Dropdown/Dropdown';
|
||||
import DropdownItem from 'components/common/Dropdown/DropdownItem';
|
||||
import { Colors } from 'theme/theme';
|
||||
import { groupBy } from 'lodash';
|
||||
import { Table } from 'components/common/table/Table/Table.styled';
|
||||
import TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeaderCell';
|
||||
import { useAppDispatch, useAppSelector } from 'lib/hooks/redux';
|
||||
import {
|
||||
fetchConsumerGroupDetails,
|
||||
deleteConsumerGroup,
|
||||
selectById,
|
||||
getIsConsumerGroupDeleted,
|
||||
getAreConsumerGroupDetailsFulfilled,
|
||||
} from 'redux/reducers/consumerGroups/consumerGroupsSlice';
|
||||
|
||||
import ListItem from './ListItem';
|
||||
|
||||
export interface Props extends ConsumerGroup, ConsumerGroupDetails {
|
||||
clusterName: ClusterName;
|
||||
partitions?: ConsumerGroupTopicPartition[];
|
||||
isFetched: boolean;
|
||||
isDeleted: boolean;
|
||||
fetchConsumerGroupDetails: (
|
||||
clusterName: ClusterName,
|
||||
consumerGroupID: ConsumerGroupID
|
||||
) => void;
|
||||
deleteConsumerGroup: (clusterName: string, id: ConsumerGroupID) => void;
|
||||
}
|
||||
|
||||
const Details: React.FC<Props> = ({
|
||||
clusterName,
|
||||
groupId,
|
||||
partitions,
|
||||
isFetched,
|
||||
isDeleted,
|
||||
fetchConsumerGroupDetails,
|
||||
deleteConsumerGroup,
|
||||
}) => {
|
||||
React.useEffect(() => {
|
||||
fetchConsumerGroupDetails(clusterName, groupId);
|
||||
}, [fetchConsumerGroupDetails, clusterName, groupId]);
|
||||
const items = partitions || [];
|
||||
const [isConfirmationModelVisible, setIsConfirmationModelVisible] =
|
||||
React.useState<boolean>(false);
|
||||
const Details: React.FC = () => {
|
||||
const history = useHistory();
|
||||
const { isReadOnly } = React.useContext(ClusterContext);
|
||||
const { consumerGroupID, clusterName } =
|
||||
useParams<{ consumerGroupID: ConsumerGroupID; clusterName: ClusterName }>();
|
||||
const dispatch = useAppDispatch();
|
||||
const consumerGroup = useAppSelector((state) =>
|
||||
selectById(state, consumerGroupID)
|
||||
);
|
||||
const isDeleted = useAppSelector(getIsConsumerGroupDeleted);
|
||||
const isFetched = useAppSelector(getAreConsumerGroupDetailsFulfilled);
|
||||
|
||||
const [isConfirmationModalVisible, setIsConfirmationModalVisible] =
|
||||
React.useState<boolean>(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
dispatch(fetchConsumerGroupDetails({ clusterName, consumerGroupID }));
|
||||
}, [fetchConsumerGroupDetails, clusterName, consumerGroupID]);
|
||||
|
||||
const onDelete = () => {
|
||||
setIsConfirmationModelVisible(false);
|
||||
deleteConsumerGroup(clusterName, groupId);
|
||||
setIsConfirmationModalVisible(false);
|
||||
dispatch(deleteConsumerGroup({ clusterName, consumerGroupID }));
|
||||
};
|
||||
React.useEffect(() => {
|
||||
if (isDeleted) {
|
||||
|
@ -59,83 +60,76 @@ const Details: React.FC<Props> = ({
|
|||
}, [isDeleted]);
|
||||
|
||||
const onResetOffsets = () => {
|
||||
history.push(clusterConsumerGroupResetOffsetsPath(clusterName, groupId));
|
||||
history.push(
|
||||
clusterConsumerGroupResetOffsetsPath(clusterName, consumerGroupID)
|
||||
);
|
||||
};
|
||||
|
||||
if (!isFetched || !consumerGroup) {
|
||||
return <PageLoader />;
|
||||
}
|
||||
|
||||
const partitionsByTopic = groupBy(consumerGroup.partitions, 'topic');
|
||||
|
||||
return (
|
||||
<div className="section">
|
||||
<div className="level">
|
||||
<div className="level-item level-left">
|
||||
<Breadcrumb
|
||||
links={[
|
||||
{
|
||||
href: clusterConsumerGroupsPath(clusterName),
|
||||
label: 'All Consumer Groups',
|
||||
},
|
||||
]}
|
||||
>
|
||||
{groupId}
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isFetched ? (
|
||||
<div className="box">
|
||||
<div>
|
||||
<div>
|
||||
<PageHeading text={consumerGroupID}>
|
||||
{!isReadOnly && (
|
||||
<div className="level">
|
||||
<div className="level-item level-right buttons">
|
||||
<button
|
||||
type="button"
|
||||
className="button"
|
||||
onClick={onResetOffsets}
|
||||
>
|
||||
Reset offsets
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="button is-danger"
|
||||
onClick={() => setIsConfirmationModelVisible(true)}
|
||||
>
|
||||
Delete consumer group
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<Dropdown label={<VerticalElipsisIcon />} right>
|
||||
<DropdownItem onClick={onResetOffsets}>
|
||||
Reset offsest
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
style={{ color: Colors.red[50] }}
|
||||
onClick={() => setIsConfirmationModalVisible(true)}
|
||||
>
|
||||
Delete consumer group
|
||||
</DropdownItem>
|
||||
</Dropdown>
|
||||
)}
|
||||
|
||||
<table className="table is-striped is-fullwidth">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Consumer ID</th>
|
||||
<th>Host</th>
|
||||
<th>Topic</th>
|
||||
<th>Partition</th>
|
||||
<th>Messages behind</th>
|
||||
<th>Current offset</th>
|
||||
<th>End offset</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{items.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={10}>No active consumer groups</td>
|
||||
</tr>
|
||||
)}
|
||||
{items.map((consumer) => (
|
||||
<ListItem
|
||||
key={consumer.consumerId}
|
||||
clusterName={clusterName}
|
||||
consumer={consumer}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
) : (
|
||||
<PageLoader />
|
||||
)}
|
||||
</PageHeading>
|
||||
</div>
|
||||
<Metrics.Wrapper>
|
||||
<Metrics.Section>
|
||||
<Metrics.Indicator label="State">
|
||||
<TagStyled color="yellow">{consumerGroup.state}</TagStyled>
|
||||
</Metrics.Indicator>
|
||||
<Metrics.Indicator label="Members">
|
||||
{consumerGroup.members}
|
||||
</Metrics.Indicator>
|
||||
<Metrics.Indicator label="Assigned topics">
|
||||
{consumerGroup.topics}
|
||||
</Metrics.Indicator>
|
||||
<Metrics.Indicator label="Assigned partitions">
|
||||
{consumerGroup.partitions?.length}
|
||||
</Metrics.Indicator>
|
||||
<Metrics.Indicator label="Coordinator ID">
|
||||
{consumerGroup.coordinator?.id}
|
||||
</Metrics.Indicator>
|
||||
</Metrics.Section>
|
||||
</Metrics.Wrapper>
|
||||
<Table isFullwidth>
|
||||
<thead>
|
||||
<tr>
|
||||
<TableHeaderCell> </TableHeaderCell>
|
||||
<TableHeaderCell title="Topic" />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{Object.keys(partitionsByTopic).map((key) => (
|
||||
<ListItem
|
||||
clusterName={clusterName}
|
||||
consumers={partitionsByTopic[key]}
|
||||
name={key}
|
||||
key={key}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
<ConfirmationModal
|
||||
isOpen={isConfirmationModelVisible}
|
||||
onCancel={() => setIsConfirmationModelVisible(false)}
|
||||
isOpen={isConfirmationModalVisible}
|
||||
onCancel={() => setIsConfirmationModalVisible(false)}
|
||||
onConfirm={onDelete}
|
||||
>
|
||||
Are you sure you want to delete this consumer group?
|
||||
|
|
|
@ -1,49 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { ClusterName, RootState } from 'redux/interfaces';
|
||||
import { withRouter, RouteComponentProps } from 'react-router-dom';
|
||||
import {
|
||||
getIsConsumerGroupDetailsFetched,
|
||||
getIsConsumerGroupsDeleted,
|
||||
getConsumerGroupByID,
|
||||
} from 'redux/reducers/consumerGroups/selectors';
|
||||
import { ConsumerGroupID } from 'redux/interfaces/consumerGroup';
|
||||
import {
|
||||
deleteConsumerGroup,
|
||||
fetchConsumerGroupDetails,
|
||||
} from 'redux/actions/thunks';
|
||||
|
||||
import Details from './Details';
|
||||
|
||||
interface RouteProps {
|
||||
clusterName: ClusterName;
|
||||
consumerGroupID: ConsumerGroupID;
|
||||
}
|
||||
|
||||
type OwnProps = RouteComponentProps<RouteProps>;
|
||||
|
||||
const mapStateToProps = (
|
||||
state: RootState,
|
||||
{
|
||||
match: {
|
||||
params: { consumerGroupID, clusterName },
|
||||
},
|
||||
}: OwnProps
|
||||
) => ({
|
||||
clusterName,
|
||||
isFetched: getIsConsumerGroupDetailsFetched(state),
|
||||
isDeleted: getIsConsumerGroupsDeleted(state),
|
||||
...getConsumerGroupByID(state, consumerGroupID),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchConsumerGroupDetails: (
|
||||
clusterName: ClusterName,
|
||||
consumerGroupID: ConsumerGroupID
|
||||
) => fetchConsumerGroupDetails(clusterName, consumerGroupID),
|
||||
deleteConsumerGroup: (clusterName: string, id: ConsumerGroupID) =>
|
||||
deleteConsumerGroup(clusterName, id),
|
||||
};
|
||||
|
||||
export default withRouter(
|
||||
connect(mapStateToProps, mapDispatchToProps)(Details)
|
||||
);
|
|
@ -0,0 +1,6 @@
|
|||
import styled from 'styled-components';
|
||||
|
||||
export const ToggleButton = styled.td`
|
||||
padding: 8px 8px 8px 16px !important;
|
||||
width: 30px;
|
||||
`;
|
|
@ -1,34 +1,37 @@
|
|||
import React from 'react';
|
||||
import { ConsumerGroupTopicPartition } from 'generated-sources';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { ClusterName } from 'redux/interfaces/cluster';
|
||||
import { clusterTopicPath } from 'lib/paths';
|
||||
import MessageToggleIcon from 'components/common/Icons/MessageToggleIcon';
|
||||
import IconButtonWrapper from 'components/common/Icons/IconButtonWrapper';
|
||||
import { TableKeyLink } from 'components/common/table/Table/TableKeyLink.styled';
|
||||
|
||||
import TopicContents from './TopicContents/TopicContents';
|
||||
import { ToggleButton } from './ListItem.styled';
|
||||
|
||||
interface Props {
|
||||
clusterName: ClusterName;
|
||||
consumer: ConsumerGroupTopicPartition;
|
||||
name: string;
|
||||
consumers: ConsumerGroupTopicPartition[];
|
||||
}
|
||||
|
||||
const ListItem: React.FC<Props> = ({ clusterName, consumer }) => {
|
||||
const ListItem: React.FC<Props> = ({ clusterName, name, consumers }) => {
|
||||
const [isOpen, setIsOpen] = React.useState(false);
|
||||
return (
|
||||
<tr>
|
||||
<td>{consumer.consumerId}</td>
|
||||
<td>{consumer.host}</td>
|
||||
<td>
|
||||
<NavLink
|
||||
exact
|
||||
to={clusterTopicPath(clusterName, consumer.topic)}
|
||||
activeClassName="is-active"
|
||||
className="title is-6"
|
||||
>
|
||||
{consumer.topic}
|
||||
</NavLink>
|
||||
</td>
|
||||
<td>{consumer.partition}</td>
|
||||
<td>{consumer.messagesBehind}</td>
|
||||
<td>{consumer.currentOffset}</td>
|
||||
<td>{consumer.endOffset}</td>
|
||||
</tr>
|
||||
<>
|
||||
<tr>
|
||||
<ToggleButton>
|
||||
<IconButtonWrapper onClick={() => setIsOpen(!isOpen)} aria-hidden>
|
||||
<MessageToggleIcon isOpen={isOpen} />
|
||||
</IconButtonWrapper>
|
||||
</ToggleButton>
|
||||
<TableKeyLink>
|
||||
<Link to={clusterTopicPath(clusterName, name)}>{name}</Link>
|
||||
</TableKeyLink>
|
||||
</tr>
|
||||
{isOpen && <TopicContents consumers={consumers} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
import styled from 'styled-components';
|
||||
|
||||
export const ResetOffsetsStyledWrapper = styled.div`
|
||||
padding: 16px;
|
||||
padding-top: 0;
|
||||
|
||||
& > form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
& > button:last-child {
|
||||
align-self: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
& .multi-select {
|
||||
height: 32px;
|
||||
& > .dropdown-container {
|
||||
height: 32px;
|
||||
& > .dropdown-heading {
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& .date-picker {
|
||||
height: 32px;
|
||||
border: 1px ${(props) => props.theme.selectStyles.borderColor.normal} solid;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
width: 50%;
|
||||
padding-left: 12px;
|
||||
color: ${(props) => props.theme.selectStyles.color.normal};
|
||||
|
||||
background-image: url('data:image/svg+xml,%3Csvg width="10" height="6" viewBox="0 0 10 6" fill="none" xmlns="http://www.w3.org/2000/svg"%3E%3Cpath d="M1 1L5 5L9 1" stroke="%23454F54"/%3E%3C/svg%3E%0A') !important;
|
||||
background-repeat: no-repeat !important;
|
||||
background-position-x: 96% !important;
|
||||
background-position-y: 55% !important;
|
||||
appearance: none !important;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const MainSelectorsWrapperStyled = styled.div`
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
& > * {
|
||||
flex-grow: 1;
|
||||
}
|
||||
`;
|
||||
|
||||
export const OffsetsWrapperStyled = styled.div`
|
||||
display: flex;
|
||||
width: 100%;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
`;
|
||||
|
||||
export const OffsetsTitleStyled = styled.h1`
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
`;
|
|
@ -1,14 +1,12 @@
|
|||
import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb';
|
||||
import {
|
||||
ConsumerGroupDetails,
|
||||
ConsumerGroupOffsetsResetType,
|
||||
} from 'generated-sources';
|
||||
import {
|
||||
clusterConsumerGroupsPath,
|
||||
clusterConsumerGroupDetailsPath,
|
||||
} from 'lib/paths';
|
||||
import { ConsumerGroupOffsetsResetType } from 'generated-sources';
|
||||
import { clusterConsumerGroupDetailsPath } from 'lib/paths';
|
||||
import React from 'react';
|
||||
import { Controller, useFieldArray, useForm } from 'react-hook-form';
|
||||
import {
|
||||
Controller,
|
||||
FormProvider,
|
||||
useFieldArray,
|
||||
useForm,
|
||||
} from 'react-hook-form';
|
||||
import { ClusterName, ConsumerGroupID } from 'redux/interfaces';
|
||||
import MultiSelect from 'react-multi-select-component';
|
||||
import { Option } from 'react-multi-select-component/dist/lib/interfaces';
|
||||
|
@ -17,31 +15,29 @@ import 'react-datepicker/dist/react-datepicker.css';
|
|||
import { groupBy } from 'lodash';
|
||||
import PageLoader from 'components/common/PageLoader/PageLoader';
|
||||
import { ErrorMessage } from '@hookform/error-message';
|
||||
import { useHistory } from 'react-router';
|
||||
import { useHistory, useParams } from 'react-router';
|
||||
import Select from 'components/common/Select/Select';
|
||||
import { InputLabel } from 'components/common/Input/InputLabel.styled';
|
||||
import { Button } from 'components/common/Button/Button';
|
||||
import Input from 'components/common/Input/Input';
|
||||
import { FormError } from 'components/common/Input/Input.styled';
|
||||
import PageHeading from 'components/common/PageHeading/PageHeading';
|
||||
import {
|
||||
fetchConsumerGroupDetails,
|
||||
selectById,
|
||||
getAreConsumerGroupDetailsFulfilled,
|
||||
getIsOffsetReseted,
|
||||
resetConsumerGroupOffsets,
|
||||
} from 'redux/reducers/consumerGroups/consumerGroupsSlice';
|
||||
import { useAppDispatch, useAppSelector } from 'lib/hooks/redux';
|
||||
import { resetLoaderById } from 'redux/reducers/loader/loaderSlice';
|
||||
|
||||
export interface Props {
|
||||
clusterName: ClusterName;
|
||||
consumerGroupID: ConsumerGroupID;
|
||||
consumerGroup: ConsumerGroupDetails;
|
||||
detailsAreFetched: boolean;
|
||||
IsOffsetReset: boolean;
|
||||
fetchConsumerGroupDetails(
|
||||
clusterName: ClusterName,
|
||||
consumerGroupID: ConsumerGroupID
|
||||
): void;
|
||||
resetConsumerGroupOffsets(
|
||||
clusterName: ClusterName,
|
||||
consumerGroupID: ConsumerGroupID,
|
||||
requestBody: {
|
||||
topic: string;
|
||||
resetType: ConsumerGroupOffsetsResetType;
|
||||
partitionsOffsets?: { offset: string; partition: number }[];
|
||||
resetToTimestamp?: Date;
|
||||
partitions: number[];
|
||||
}
|
||||
): void;
|
||||
resetResettingStatus: () => void;
|
||||
}
|
||||
import {
|
||||
MainSelectorsWrapperStyled,
|
||||
OffsetsWrapperStyled,
|
||||
ResetOffsetsStyledWrapper,
|
||||
OffsetsTitleStyled,
|
||||
} from './ResetOffsets.styled';
|
||||
|
||||
interface FormType {
|
||||
topic: string;
|
||||
|
@ -50,18 +46,19 @@ interface FormType {
|
|||
resetToTimestamp: Date;
|
||||
}
|
||||
|
||||
const ResetOffsets: React.FC<Props> = ({
|
||||
clusterName,
|
||||
consumerGroupID,
|
||||
consumerGroup,
|
||||
detailsAreFetched,
|
||||
IsOffsetReset,
|
||||
fetchConsumerGroupDetails,
|
||||
resetConsumerGroupOffsets,
|
||||
resetResettingStatus,
|
||||
}) => {
|
||||
const ResetOffsets: React.FC = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { consumerGroupID, clusterName } =
|
||||
useParams<{ consumerGroupID: ConsumerGroupID; clusterName: ClusterName }>();
|
||||
const consumerGroup = useAppSelector((state) =>
|
||||
selectById(state, consumerGroupID)
|
||||
);
|
||||
|
||||
const isFetched = useAppSelector(getAreConsumerGroupDetailsFulfilled);
|
||||
const isOffsetReseted = useAppSelector(getIsOffsetReseted);
|
||||
|
||||
React.useEffect(() => {
|
||||
fetchConsumerGroupDetails(clusterName, consumerGroupID);
|
||||
dispatch(fetchConsumerGroupDetails({ clusterName, consumerGroupID }));
|
||||
}, [clusterName, consumerGroupID]);
|
||||
|
||||
const [uniqueTopics, setUniqueTopics] = React.useState<string[]>([]);
|
||||
|
@ -69,8 +66,14 @@ const ResetOffsets: React.FC<Props> = ({
|
|||
[]
|
||||
);
|
||||
|
||||
const methods = useForm<FormType>({
|
||||
defaultValues: {
|
||||
resetType: ConsumerGroupOffsetsResetType.EARLIEST,
|
||||
topic: '',
|
||||
partitionsOffsets: [],
|
||||
},
|
||||
});
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setValue,
|
||||
watch,
|
||||
|
@ -78,13 +81,7 @@ const ResetOffsets: React.FC<Props> = ({
|
|||
setError,
|
||||
clearErrors,
|
||||
formState: { errors },
|
||||
} = useForm<FormType>({
|
||||
defaultValues: {
|
||||
resetType: ConsumerGroupOffsetsResetType.EARLIEST,
|
||||
topic: '',
|
||||
partitionsOffsets: [],
|
||||
},
|
||||
});
|
||||
} = methods;
|
||||
const { fields } = useFieldArray({
|
||||
control,
|
||||
name: 'partitionsOffsets',
|
||||
|
@ -94,11 +91,11 @@ const ResetOffsets: React.FC<Props> = ({
|
|||
const offsetsValue = watch('partitionsOffsets');
|
||||
|
||||
React.useEffect(() => {
|
||||
if (detailsAreFetched && consumerGroup.partitions) {
|
||||
if (isFetched && consumerGroup?.partitions) {
|
||||
setValue('topic', consumerGroup.partitions[0].topic);
|
||||
setUniqueTopics(Object.keys(groupBy(consumerGroup.partitions, 'topic')));
|
||||
}
|
||||
}, [detailsAreFetched]);
|
||||
}, [isFetched]);
|
||||
|
||||
const onSelectedPartitionsChange = (value: Option[]) => {
|
||||
clearErrors();
|
||||
|
@ -153,174 +150,140 @@ const ResetOffsets: React.FC<Props> = ({
|
|||
}
|
||||
}
|
||||
if (isValid) {
|
||||
resetConsumerGroupOffsets(clusterName, consumerGroupID, augmentedData);
|
||||
dispatch(
|
||||
resetConsumerGroupOffsets({
|
||||
clusterName,
|
||||
consumerGroupID,
|
||||
requestBody: augmentedData,
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const history = useHistory();
|
||||
React.useEffect(() => {
|
||||
if (IsOffsetReset) {
|
||||
resetResettingStatus();
|
||||
if (isOffsetReseted) {
|
||||
dispatch(resetLoaderById('consumerGroups/resetConsumerGroupOffsets'));
|
||||
history.push(
|
||||
clusterConsumerGroupDetailsPath(clusterName, consumerGroupID)
|
||||
);
|
||||
}
|
||||
}, [IsOffsetReset]);
|
||||
}, [isOffsetReseted]);
|
||||
|
||||
if (!detailsAreFetched) {
|
||||
if (!isFetched || !consumerGroup) {
|
||||
return <PageLoader />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="section">
|
||||
<div className="level">
|
||||
<div className="level-item level-left">
|
||||
<Breadcrumb
|
||||
links={[
|
||||
{
|
||||
href: clusterConsumerGroupsPath(clusterName),
|
||||
label: 'All Consumer Groups',
|
||||
},
|
||||
{
|
||||
href: clusterConsumerGroupDetailsPath(
|
||||
clusterName,
|
||||
consumerGroupID
|
||||
),
|
||||
label: consumerGroupID,
|
||||
},
|
||||
]}
|
||||
>
|
||||
Reset Offsets
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="box">
|
||||
<FormProvider {...methods}>
|
||||
<PageHeading text="Reset offsets" />
|
||||
<ResetOffsetsStyledWrapper>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="columns">
|
||||
<div className="column is-one-third">
|
||||
<label className="label" htmlFor="topic">
|
||||
Topic
|
||||
</label>
|
||||
<div className="select">
|
||||
<select {...register('topic')} id="topic">
|
||||
{uniqueTopics.map((topic) => (
|
||||
<option key={topic} value={topic}>
|
||||
{topic}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<MainSelectorsWrapperStyled>
|
||||
<div>
|
||||
<InputLabel htmlFor="topic">Topic</InputLabel>
|
||||
<Select name="topic" id="topic" selectSize="M">
|
||||
{uniqueTopics.map((topic) => (
|
||||
<option key={topic} value={topic}>
|
||||
{topic}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
<div className="column is-one-third">
|
||||
<label className="label" htmlFor="resetType">
|
||||
Reset Type
|
||||
</label>
|
||||
<div className="select">
|
||||
<select {...register('resetType')} id="resetType">
|
||||
{Object.values(ConsumerGroupOffsetsResetType).map((type) => (
|
||||
<option key={type} value={type}>
|
||||
{type}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<InputLabel htmlFor="resetType">Reset Type</InputLabel>
|
||||
<Select name="resetType" id="resetType" selectSize="M">
|
||||
{Object.values(ConsumerGroupOffsetsResetType).map((type) => (
|
||||
<option key={type} value={type}>
|
||||
{type}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
<div className="column is-one-third">
|
||||
<label className="label">Partitions</label>
|
||||
<div className="select">
|
||||
<MultiSelect
|
||||
options={
|
||||
consumerGroup.partitions
|
||||
?.filter((p) => p.topic === topicValue)
|
||||
.map((p) => ({
|
||||
label: `Partition #${p.partition.toString()}`,
|
||||
value: p.partition,
|
||||
})) || []
|
||||
}
|
||||
value={selectedPartitions}
|
||||
onChange={onSelectedPartitionsChange}
|
||||
labelledBy="Select partitions"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<InputLabel>Partitions</InputLabel>
|
||||
<MultiSelect
|
||||
options={
|
||||
consumerGroup.partitions
|
||||
?.filter((p) => p.topic === topicValue)
|
||||
.map((p) => ({
|
||||
label: `Partition #${p.partition.toString()}`,
|
||||
value: p.partition,
|
||||
})) || []
|
||||
}
|
||||
value={selectedPartitions}
|
||||
onChange={onSelectedPartitionsChange}
|
||||
labelledBy="Select partitions"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</MainSelectorsWrapperStyled>
|
||||
{resetTypeValue === ConsumerGroupOffsetsResetType.TIMESTAMP &&
|
||||
selectedPartitions.length > 0 && (
|
||||
<div className="columns">
|
||||
<div className="column is-half">
|
||||
<label className="label">Timestamp</label>
|
||||
<Controller
|
||||
control={control}
|
||||
name="resetToTimestamp"
|
||||
render={({ field: { onChange, onBlur, value, ref } }) => (
|
||||
<DatePicker
|
||||
ref={ref}
|
||||
selected={value}
|
||||
onChange={onChange}
|
||||
onBlur={onBlur}
|
||||
showTimeInput
|
||||
timeInputLabel="Time:"
|
||||
dateFormat="MMMM d, yyyy h:mm aa"
|
||||
className="input"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<ErrorMessage
|
||||
errors={errors}
|
||||
name="resetToTimestamp"
|
||||
render={({ message }) => (
|
||||
<p className="help is-danger">{message}</p>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<InputLabel>Timestamp</InputLabel>
|
||||
<Controller
|
||||
control={control}
|
||||
name="resetToTimestamp"
|
||||
render={({ field: { onChange, onBlur, value, ref } }) => (
|
||||
<DatePicker
|
||||
ref={ref}
|
||||
selected={value}
|
||||
onChange={onChange}
|
||||
onBlur={onBlur}
|
||||
showTimeInput
|
||||
timeInputLabel="Time:"
|
||||
dateFormat="MMMM d, yyyy h:mm aa"
|
||||
className="date-picker"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<ErrorMessage
|
||||
errors={errors}
|
||||
name="resetToTimestamp"
|
||||
render={({ message }) => <FormError>{message}</FormError>}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{resetTypeValue === ConsumerGroupOffsetsResetType.OFFSET &&
|
||||
selectedPartitions.length > 0 && (
|
||||
<div className="columns">
|
||||
<div className="column is-one-third">
|
||||
<label className="label">Offsets</label>
|
||||
<div>
|
||||
<OffsetsTitleStyled>Offsets</OffsetsTitleStyled>
|
||||
<OffsetsWrapperStyled>
|
||||
{fields.map((field, index) => (
|
||||
<div key={field.id} className="mb-2">
|
||||
<label
|
||||
className="subtitle is-6"
|
||||
htmlFor={`partitionsOffsets.${index}.offset`}
|
||||
>
|
||||
<div key={field.id}>
|
||||
<InputLabel htmlFor={`partitionsOffsets.${index}.offset`}>
|
||||
Partition #{field.partition}
|
||||
</label>
|
||||
<input
|
||||
</InputLabel>
|
||||
<Input
|
||||
id={`partitionsOffsets.${index}.offset`}
|
||||
type="number"
|
||||
className="input"
|
||||
{...register(
|
||||
`partitionsOffsets.${index}.offset` as const,
|
||||
{ shouldUnregister: true }
|
||||
)}
|
||||
name={`partitionsOffsets.${index}.offset` as const}
|
||||
hookFormOptions={{ shouldUnregister: true }}
|
||||
defaultValue={field.offset}
|
||||
/>
|
||||
<ErrorMessage
|
||||
errors={errors}
|
||||
name={`partitionsOffsets.${index}.offset`}
|
||||
render={({ message }) => (
|
||||
<p className="help is-danger">{message}</p>
|
||||
<FormError>{message}</FormError>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</OffsetsWrapperStyled>
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
className="button is-primary"
|
||||
<Button
|
||||
buttonSize="M"
|
||||
buttonType="primary"
|
||||
type="submit"
|
||||
disabled={selectedPartitions.length === 0}
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</ResetOffsetsStyledWrapper>
|
||||
</FormProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -1,47 +0,0 @@
|
|||
import { RouteComponentProps, withRouter } from 'react-router-dom';
|
||||
import { connect } from 'react-redux';
|
||||
import { ClusterName, ConsumerGroupID, RootState } from 'redux/interfaces';
|
||||
import {
|
||||
getConsumerGroupByID,
|
||||
getIsConsumerGroupDetailsFetched,
|
||||
getOffsetReset,
|
||||
} from 'redux/reducers/consumerGroups/selectors';
|
||||
import {
|
||||
fetchConsumerGroupDetails,
|
||||
resetConsumerGroupOffsets,
|
||||
resetConsumerGroupOffsetsAction,
|
||||
} from 'redux/actions';
|
||||
|
||||
import ResetOffsets from './ResetOffsets';
|
||||
|
||||
interface RouteProps {
|
||||
clusterName: ClusterName;
|
||||
consumerGroupID: ConsumerGroupID;
|
||||
}
|
||||
|
||||
type OwnProps = RouteComponentProps<RouteProps>;
|
||||
|
||||
const mapStateToProps = (
|
||||
state: RootState,
|
||||
{
|
||||
match: {
|
||||
params: { consumerGroupID, clusterName },
|
||||
},
|
||||
}: OwnProps
|
||||
) => ({
|
||||
clusterName,
|
||||
consumerGroupID,
|
||||
consumerGroup: getConsumerGroupByID(state, consumerGroupID),
|
||||
detailsAreFetched: getIsConsumerGroupDetailsFetched(state),
|
||||
IsOffsetReset: getOffsetReset(state),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchConsumerGroupDetails,
|
||||
resetConsumerGroupOffsets,
|
||||
resetResettingStatus: resetConsumerGroupOffsetsAction.cancel,
|
||||
};
|
||||
|
||||
export default withRouter(
|
||||
connect(mapStateToProps, mapDispatchToProps)(ResetOffsets)
|
||||
);
|
|
@ -1,178 +1,162 @@
|
|||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import ResetOffsets, {
|
||||
Props,
|
||||
} from 'components/ConsumerGroups/Details/ResetOffsets/ResetOffsets';
|
||||
import { ConsumerGroupState } from 'generated-sources';
|
||||
import React from 'react';
|
||||
import { StaticRouter } from 'react-router';
|
||||
import fetchMock from 'fetch-mock';
|
||||
import { Route, StaticRouter } from 'react-router';
|
||||
import { screen, waitFor, fireEvent } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { render } from 'lib/testHelpers';
|
||||
import { clusterConsumerGroupResetOffsetsPath } from 'lib/paths';
|
||||
import { consumerGroupPayload } from 'redux/reducers/consumerGroups/__test__/fixtures';
|
||||
import ResetOffsets from 'components/ConsumerGroups/Details/ResetOffsets/ResetOffsets';
|
||||
|
||||
import { expectedOutputs } from './fixtures';
|
||||
const clusterName = 'cluster1';
|
||||
const { groupId } = consumerGroupPayload;
|
||||
|
||||
const setupWrapper = (props?: Partial<Props>) => (
|
||||
<StaticRouter>
|
||||
<ResetOffsets
|
||||
clusterName="testCluster"
|
||||
consumerGroupID="testGroup"
|
||||
consumerGroup={{
|
||||
groupId: 'amazon.msk.canary.group.broker-1',
|
||||
members: 0,
|
||||
topics: 2,
|
||||
simple: false,
|
||||
partitionAssignor: '',
|
||||
state: ConsumerGroupState.EMPTY,
|
||||
coordinator: {
|
||||
id: 2,
|
||||
host: 'b-2.kad-msk.st2jzq.c6.kafka.eu-west-1.amazonaws.com',
|
||||
},
|
||||
messagesBehind: 0,
|
||||
partitions: [
|
||||
{
|
||||
topic: '__amazon_msk_canary',
|
||||
partition: 1,
|
||||
currentOffset: 0,
|
||||
endOffset: 0,
|
||||
messagesBehind: 0,
|
||||
consumerId: undefined,
|
||||
host: undefined,
|
||||
},
|
||||
{
|
||||
topic: '__amazon_msk_canary',
|
||||
partition: 0,
|
||||
currentOffset: 56932,
|
||||
endOffset: 56932,
|
||||
messagesBehind: 0,
|
||||
consumerId: undefined,
|
||||
host: undefined,
|
||||
},
|
||||
{
|
||||
topic: 'other_topic',
|
||||
partition: 3,
|
||||
currentOffset: 56932,
|
||||
endOffset: 56932,
|
||||
messagesBehind: 0,
|
||||
consumerId: undefined,
|
||||
host: undefined,
|
||||
},
|
||||
{
|
||||
topic: 'other_topic',
|
||||
partition: 4,
|
||||
currentOffset: 56932,
|
||||
endOffset: 56932,
|
||||
messagesBehind: 0,
|
||||
consumerId: undefined,
|
||||
host: undefined,
|
||||
},
|
||||
],
|
||||
const renderComponent = () =>
|
||||
render(
|
||||
<StaticRouter
|
||||
location={{
|
||||
pathname: clusterConsumerGroupResetOffsetsPath(
|
||||
clusterName,
|
||||
consumerGroupPayload.groupId
|
||||
),
|
||||
}}
|
||||
detailsAreFetched
|
||||
IsOffsetReset={false}
|
||||
fetchConsumerGroupDetails={jest.fn()}
|
||||
resetConsumerGroupOffsets={jest.fn()}
|
||||
resetResettingStatus={jest.fn()}
|
||||
{...props}
|
||||
/>
|
||||
</StaticRouter>
|
||||
);
|
||||
>
|
||||
<Route
|
||||
path={clusterConsumerGroupResetOffsetsPath(
|
||||
':clusterName',
|
||||
':consumerGroupID'
|
||||
)}
|
||||
>
|
||||
<ResetOffsets />
|
||||
</Route>
|
||||
</StaticRouter>
|
||||
);
|
||||
|
||||
const resetConsumerGroupOffsetsMockCalled = () =>
|
||||
expect(
|
||||
fetchMock.called(
|
||||
`/api/clusters/${clusterName}/consumer-groups/${groupId}/offsets`
|
||||
)
|
||||
).toBeTruthy();
|
||||
|
||||
const selectresetTypeAndPartitions = async (resetType: string) => {
|
||||
fireEvent.change(screen.getByLabelText('Reset Type'), {
|
||||
target: { value: resetType },
|
||||
});
|
||||
userEvent.selectOptions(screen.getByLabelText('Reset Type'), resetType);
|
||||
userEvent.click(screen.getByText('Select...'));
|
||||
await waitFor(() => {
|
||||
fireEvent.click(screen.getByText('Select...'));
|
||||
});
|
||||
await waitFor(() => {
|
||||
fireEvent.click(screen.getByText('Partition #0'));
|
||||
userEvent.click(screen.getByText('Partition #0'));
|
||||
});
|
||||
};
|
||||
|
||||
const resetConsumerGroupOffsetsWith = async (
|
||||
resetType: string,
|
||||
offset: null | number = null
|
||||
) => {
|
||||
userEvent.selectOptions(screen.getByLabelText('Reset Type'), resetType);
|
||||
userEvent.click(screen.getByText('Select...'));
|
||||
await waitFor(() => {
|
||||
userEvent.click(screen.getByText('Partition #0'));
|
||||
});
|
||||
fetchMock.postOnce(
|
||||
`/api/clusters/${clusterName}/consumer-groups/${groupId}/offsets`,
|
||||
200,
|
||||
{
|
||||
body: {
|
||||
topic: '__amazon_msk_canary',
|
||||
resetType,
|
||||
partitions: [0],
|
||||
partitionsOffsets: [{ partition: 0, offset }],
|
||||
},
|
||||
}
|
||||
);
|
||||
userEvent.click(screen.getByText('Submit'));
|
||||
await waitFor(() => resetConsumerGroupOffsetsMockCalled());
|
||||
};
|
||||
|
||||
describe('ResetOffsets', () => {
|
||||
describe('on initial render', () => {
|
||||
const component = render(setupWrapper());
|
||||
it('matches the snapshot', () => {
|
||||
expect(component.baseElement).toMatchSnapshot();
|
||||
});
|
||||
afterEach(() => {
|
||||
fetchMock.reset();
|
||||
});
|
||||
|
||||
describe('on submit', () => {
|
||||
describe('with the default ResetType', () => {
|
||||
it('calls resetConsumerGroupOffsets', async () => {
|
||||
const mockResetConsumerGroupOffsets = jest.fn();
|
||||
render(
|
||||
setupWrapper({
|
||||
resetConsumerGroupOffsets: mockResetConsumerGroupOffsets,
|
||||
})
|
||||
);
|
||||
await selectresetTypeAndPartitions('EARLIEST');
|
||||
await waitFor(() => {
|
||||
fireEvent.click(screen.getByText('Submit'));
|
||||
});
|
||||
expect(mockResetConsumerGroupOffsets).toHaveBeenCalledTimes(1);
|
||||
expect(mockResetConsumerGroupOffsets).toHaveBeenCalledWith(
|
||||
'testCluster',
|
||||
'testGroup',
|
||||
expectedOutputs.EARLIEST
|
||||
);
|
||||
});
|
||||
});
|
||||
it('renders progress bar for initial state', () => {
|
||||
fetchMock.getOnce(
|
||||
`/api/clusters/${clusterName}/consumer-groups/${groupId}`,
|
||||
404
|
||||
);
|
||||
renderComponent();
|
||||
expect(screen.getByRole('progressbar')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('with the ResetType set to LATEST', () => {
|
||||
it('calls resetConsumerGroupOffsets', async () => {
|
||||
const mockResetConsumerGroupOffsets = jest.fn();
|
||||
render(
|
||||
setupWrapper({
|
||||
resetConsumerGroupOffsets: mockResetConsumerGroupOffsets,
|
||||
})
|
||||
describe('with consumer group', () => {
|
||||
describe('submit handles resetConsumerGroupOffsets', () => {
|
||||
beforeEach(async () => {
|
||||
const fetchConsumerGroupMock = fetchMock.getOnce(
|
||||
`/api/clusters/${clusterName}/consumer-groups/${groupId}`,
|
||||
consumerGroupPayload
|
||||
);
|
||||
await selectresetTypeAndPartitions('LATEST');
|
||||
await waitFor(() => {
|
||||
fireEvent.click(screen.getByText('Submit'));
|
||||
});
|
||||
expect(mockResetConsumerGroupOffsets).toHaveBeenCalledTimes(1);
|
||||
expect(mockResetConsumerGroupOffsets).toHaveBeenCalledWith(
|
||||
'testCluster',
|
||||
'testGroup',
|
||||
expectedOutputs.LATEST
|
||||
renderComponent();
|
||||
await waitFor(() =>
|
||||
expect(fetchConsumerGroupMock.called()).toBeTruthy()
|
||||
);
|
||||
await waitFor(() => screen.queryByRole('form'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('with the ResetType set to OFFSET', () => {
|
||||
it('calls resetConsumerGroupOffsets', async () => {
|
||||
const mockResetConsumerGroupOffsets = jest.fn();
|
||||
render(
|
||||
setupWrapper({
|
||||
resetConsumerGroupOffsets: mockResetConsumerGroupOffsets,
|
||||
})
|
||||
);
|
||||
it('calls resetConsumerGroupOffsets with EARLIEST', async () => {
|
||||
await resetConsumerGroupOffsetsWith('EARLIEST');
|
||||
});
|
||||
|
||||
it('calls resetConsumerGroupOffsets with LATEST', async () => {
|
||||
await resetConsumerGroupOffsetsWith('LATEST');
|
||||
});
|
||||
it('calls resetConsumerGroupOffsets with OFFSET', async () => {
|
||||
await selectresetTypeAndPartitions('OFFSET');
|
||||
fetchMock.postOnce(
|
||||
`/api/clusters/${clusterName}/consumer-groups/${groupId}/offsets`,
|
||||
200,
|
||||
{
|
||||
body: {
|
||||
topic: '__amazon_msk_canary',
|
||||
resetType: 'OFFSET',
|
||||
partitions: [0],
|
||||
partitionsOffsets: [{ partition: 0, offset: 10 }],
|
||||
},
|
||||
}
|
||||
);
|
||||
await waitFor(() => {
|
||||
fireEvent.change(screen.getAllByLabelText('Partition #0')[1], {
|
||||
target: { value: '10' },
|
||||
});
|
||||
});
|
||||
await waitFor(() => {
|
||||
fireEvent.click(screen.getByText('Submit'));
|
||||
});
|
||||
expect(mockResetConsumerGroupOffsets).toHaveBeenCalledTimes(1);
|
||||
expect(mockResetConsumerGroupOffsets).toHaveBeenCalledWith(
|
||||
'testCluster',
|
||||
'testGroup',
|
||||
expectedOutputs.OFFSET
|
||||
);
|
||||
userEvent.click(screen.getByText('Submit'));
|
||||
await waitFor(() => resetConsumerGroupOffsetsMockCalled());
|
||||
});
|
||||
});
|
||||
|
||||
describe('with the ResetType set to TIMESTAMP', () => {
|
||||
it('adds error to the page when the input is left empty', async () => {
|
||||
const mockResetConsumerGroupOffsets = jest.fn();
|
||||
render(setupWrapper());
|
||||
it('calls resetConsumerGroupOffsets with TIMESTAMP', async () => {
|
||||
await selectresetTypeAndPartitions('TIMESTAMP');
|
||||
await waitFor(() => {
|
||||
fireEvent.click(screen.getByText('Submit'));
|
||||
});
|
||||
expect(mockResetConsumerGroupOffsets).toHaveBeenCalledTimes(0);
|
||||
expect(screen.getByText("This field shouldn't be empty!")).toBeTruthy();
|
||||
const resetConsumerGroupOffsetsMock = fetchMock.postOnce(
|
||||
`/api/clusters/${clusterName}/consumer-groups/${groupId}/offsets`,
|
||||
200,
|
||||
{
|
||||
body: {
|
||||
topic: '__amazon_msk_canary',
|
||||
resetType: 'OFFSET',
|
||||
partitions: [0],
|
||||
partitionsOffsets: [{ partition: 0, offset: 10 }],
|
||||
},
|
||||
}
|
||||
);
|
||||
userEvent.click(screen.getByText('Submit'));
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
screen.getByText("This field shouldn't be empty!")
|
||||
).toBeInTheDocument()
|
||||
);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
resetConsumerGroupOffsetsMock.called(
|
||||
`/api/clusters/${clusterName}/consumer-groups/${groupId}/offsets`
|
||||
)
|
||||
).toBeFalsy()
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,184 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ResetOffsets on initial render matches the snapshot 1`] = `
|
||||
<body>
|
||||
<div>
|
||||
<div
|
||||
class="section"
|
||||
>
|
||||
<div
|
||||
class="level"
|
||||
>
|
||||
<div
|
||||
class="level-item level-left"
|
||||
>
|
||||
<nav
|
||||
aria-label="breadcrumbs"
|
||||
class="breadcrumb"
|
||||
>
|
||||
<ul>
|
||||
<li>
|
||||
<a
|
||||
href="/ui/clusters/testCluster/consumer-groups"
|
||||
>
|
||||
All Consumer Groups
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="/ui/clusters/testCluster/consumer-groups/testGroup"
|
||||
>
|
||||
testGroup
|
||||
</a>
|
||||
</li>
|
||||
<li
|
||||
class="is-active"
|
||||
>
|
||||
<span
|
||||
class=""
|
||||
>
|
||||
Reset Offsets
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="box"
|
||||
>
|
||||
<form>
|
||||
<div
|
||||
class="columns"
|
||||
>
|
||||
<div
|
||||
class="column is-one-third"
|
||||
>
|
||||
<label
|
||||
class="label"
|
||||
for="topic"
|
||||
>
|
||||
Topic
|
||||
</label>
|
||||
<div
|
||||
class="select"
|
||||
>
|
||||
<select
|
||||
id="topic"
|
||||
name="topic"
|
||||
>
|
||||
<option
|
||||
value="__amazon_msk_canary"
|
||||
>
|
||||
__amazon_msk_canary
|
||||
</option>
|
||||
<option
|
||||
value="other_topic"
|
||||
>
|
||||
other_topic
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="column is-one-third"
|
||||
>
|
||||
<label
|
||||
class="label"
|
||||
for="resetType"
|
||||
>
|
||||
Reset Type
|
||||
</label>
|
||||
<div
|
||||
class="select"
|
||||
>
|
||||
<select
|
||||
id="resetType"
|
||||
name="resetType"
|
||||
>
|
||||
<option
|
||||
value="EARLIEST"
|
||||
>
|
||||
EARLIEST
|
||||
</option>
|
||||
<option
|
||||
value="LATEST"
|
||||
>
|
||||
LATEST
|
||||
</option>
|
||||
<option
|
||||
value="TIMESTAMP"
|
||||
>
|
||||
TIMESTAMP
|
||||
</option>
|
||||
<option
|
||||
value="OFFSET"
|
||||
>
|
||||
OFFSET
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="column is-one-third"
|
||||
>
|
||||
<label
|
||||
class="label"
|
||||
>
|
||||
Partitions
|
||||
</label>
|
||||
<div
|
||||
class="select"
|
||||
>
|
||||
<div
|
||||
class="rmsc multi-select"
|
||||
>
|
||||
<div
|
||||
aria-labelledby="Select partitions"
|
||||
aria-readonly="true"
|
||||
class="dropdown-container"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="dropdown-heading"
|
||||
>
|
||||
<div
|
||||
class="dropdown-heading-value"
|
||||
>
|
||||
<span
|
||||
class="gray"
|
||||
>
|
||||
Select...
|
||||
</span>
|
||||
</div>
|
||||
<svg
|
||||
class="dropdown-heading-dropdown-arrow gray"
|
||||
fill="none"
|
||||
height="24"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
width="24"
|
||||
>
|
||||
<path
|
||||
d="M6 9L12 15 18 9"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="button is-primary"
|
||||
disabled=""
|
||||
type="submit"
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
`;
|
|
@ -0,0 +1,15 @@
|
|||
import styled from 'styled-components';
|
||||
import { Colors } from 'theme/theme';
|
||||
|
||||
export const TopicContentWrapper = styled.tr`
|
||||
background-color: ${Colors.neutral[5]};
|
||||
& > td {
|
||||
padding: 16px !important;
|
||||
}
|
||||
`;
|
||||
|
||||
export const ContentBox = styled.div`
|
||||
background-color: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
`;
|
|
@ -0,0 +1,47 @@
|
|||
import { Table } from 'components/common/table/Table/Table.styled';
|
||||
import TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeaderCell';
|
||||
import { ConsumerGroupTopicPartition } from 'generated-sources';
|
||||
import React from 'react';
|
||||
|
||||
import { ContentBox, TopicContentWrapper } from './TopicContent.styled';
|
||||
|
||||
interface Props {
|
||||
consumers: ConsumerGroupTopicPartition[];
|
||||
}
|
||||
|
||||
const TopicContents: React.FC<Props> = ({ consumers }) => {
|
||||
return (
|
||||
<TopicContentWrapper>
|
||||
<td colSpan={3}>
|
||||
<ContentBox>
|
||||
<Table isFullwidth>
|
||||
<thead>
|
||||
<tr>
|
||||
<TableHeaderCell title="Partition" />
|
||||
<TableHeaderCell title="Consumer ID" />
|
||||
<TableHeaderCell title="Host" />
|
||||
<TableHeaderCell title="Messages behind" />
|
||||
<TableHeaderCell title="Current offset" />
|
||||
<TableHeaderCell title="End offset" />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{consumers.map((consumer) => (
|
||||
<tr key={consumer.partition}>
|
||||
<td>{consumer.partition}</td>
|
||||
<td>{consumer.consumerId}</td>
|
||||
<td>{consumer.host}</td>
|
||||
<td>{consumer.messagesBehind}</td>
|
||||
<td>{consumer.currentOffset}</td>
|
||||
<td>{consumer.endOffset}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
</ContentBox>
|
||||
</td>
|
||||
</TopicContentWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default TopicContents;
|
|
@ -1,102 +1,115 @@
|
|||
import Details, { Props } from 'components/ConsumerGroups/Details/Details';
|
||||
import { mount, shallow } from 'enzyme';
|
||||
import Details from 'components/ConsumerGroups/Details/Details';
|
||||
import React from 'react';
|
||||
import { StaticRouter } from 'react-router';
|
||||
import fetchMock from 'fetch-mock';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import { render } from 'lib/testHelpers';
|
||||
import { Route, Router } from 'react-router';
|
||||
import {
|
||||
clusterConsumerGroupDetailsPath,
|
||||
clusterConsumerGroupResetOffsetsPath,
|
||||
clusterConsumerGroupsPath,
|
||||
} from 'lib/paths';
|
||||
import { consumerGroupPayload } from 'redux/reducers/consumerGroups/__test__/fixtures';
|
||||
import {
|
||||
screen,
|
||||
waitFor,
|
||||
waitForElementToBeRemoved,
|
||||
} from '@testing-library/dom';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
const mockHistory = {
|
||||
push: jest.fn(),
|
||||
};
|
||||
jest.mock('react-router', () => ({
|
||||
...jest.requireActual('react-router'),
|
||||
useHistory: () => mockHistory,
|
||||
}));
|
||||
const clusterName = 'cluster1';
|
||||
const { groupId } = consumerGroupPayload;
|
||||
const history = createMemoryHistory();
|
||||
|
||||
describe('Details component', () => {
|
||||
const setupWrapper = (props?: Partial<Props>) => (
|
||||
<Details
|
||||
clusterName="local"
|
||||
groupId="test"
|
||||
isFetched
|
||||
isDeleted={false}
|
||||
fetchConsumerGroupDetails={jest.fn()}
|
||||
deleteConsumerGroup={jest.fn()}
|
||||
partitions={[
|
||||
{
|
||||
consumerId:
|
||||
'consumer-messages-consumer-1-122fbf98-643b-491d-8aec-c0641d2513d0',
|
||||
topic: 'messages',
|
||||
host: '/172.31.9.153',
|
||||
partition: 6,
|
||||
currentOffset: 394,
|
||||
endOffset: 394,
|
||||
messagesBehind: 0,
|
||||
},
|
||||
{
|
||||
consumerId:
|
||||
'consumer-messages-consumer-1-122fbf98-643b-491d-8aec-c0641d2513d1',
|
||||
topic: 'messages',
|
||||
host: '/172.31.9.153',
|
||||
partition: 7,
|
||||
currentOffset: 384,
|
||||
endOffset: 384,
|
||||
messagesBehind: 0,
|
||||
},
|
||||
]}
|
||||
{...props}
|
||||
/>
|
||||
const renderComponent = () => {
|
||||
history.push(clusterConsumerGroupDetailsPath(clusterName, groupId));
|
||||
render(
|
||||
<Router history={history}>
|
||||
<Route
|
||||
path={clusterConsumerGroupDetailsPath(
|
||||
':clusterName',
|
||||
':consumerGroupID'
|
||||
)}
|
||||
>
|
||||
<Details />
|
||||
</Route>
|
||||
</Router>
|
||||
);
|
||||
};
|
||||
describe('Details component', () => {
|
||||
afterEach(() => {
|
||||
fetchMock.reset();
|
||||
});
|
||||
|
||||
describe('when consumer gruops are NOT fetched', () => {
|
||||
it('Matches the snapshot', () => {
|
||||
expect(shallow(setupWrapper({ isFetched: false }))).toMatchSnapshot();
|
||||
it('renders progress bar for initial state', () => {
|
||||
fetchMock.getOnce(
|
||||
`/api/clusters/${clusterName}/consumer-groups/${groupId}`,
|
||||
404
|
||||
);
|
||||
renderComponent();
|
||||
expect(screen.getByRole('progressbar')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when consumer gruops are fetched', () => {
|
||||
it('Matches the snapshot', () => {
|
||||
expect(shallow(setupWrapper())).toMatchSnapshot();
|
||||
beforeEach(async () => {
|
||||
const fetchConsumerGroupMock = fetchMock.getOnce(
|
||||
`/api/clusters/${clusterName}/consumer-groups/${groupId}`,
|
||||
consumerGroupPayload
|
||||
);
|
||||
renderComponent();
|
||||
await waitForElementToBeRemoved(() => screen.getByRole('progressbar'));
|
||||
await waitFor(() => expect(fetchConsumerGroupMock.called()).toBeTruthy());
|
||||
});
|
||||
|
||||
describe('onDelete', () => {
|
||||
it('calls deleteConsumerGroup', () => {
|
||||
const deleteConsumerGroup = jest.fn();
|
||||
const component = mount(
|
||||
<StaticRouter>{setupWrapper({ deleteConsumerGroup })}</StaticRouter>
|
||||
);
|
||||
component.find('button').at(1).simulate('click');
|
||||
component.update();
|
||||
component
|
||||
.find('ConfirmationModal')
|
||||
.find('button')
|
||||
.at(1)
|
||||
.simulate('click');
|
||||
expect(deleteConsumerGroup).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
it('renders component', () => {
|
||||
expect(screen.getByRole('heading')).toBeInTheDocument();
|
||||
expect(screen.getByText(groupId)).toBeInTheDocument();
|
||||
|
||||
describe('on ConfirmationModal cancel', () => {
|
||||
it('does not call deleteConsumerGroup', () => {
|
||||
const deleteConsumerGroup = jest.fn();
|
||||
const component = mount(
|
||||
<StaticRouter>{setupWrapper({ deleteConsumerGroup })}</StaticRouter>
|
||||
);
|
||||
component.find('button').at(1).simulate('click');
|
||||
component.update();
|
||||
component
|
||||
.find('ConfirmationModal')
|
||||
.find('button')
|
||||
.at(0)
|
||||
.simulate('click');
|
||||
expect(deleteConsumerGroup).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
expect(screen.getByRole('table')).toBeInTheDocument();
|
||||
expect(screen.getAllByRole('columnheader').length).toEqual(2);
|
||||
|
||||
describe('after deletion', () => {
|
||||
it('calls history.push', () => {
|
||||
mount(
|
||||
<StaticRouter>{setupWrapper({ isDeleted: true })}</StaticRouter>
|
||||
);
|
||||
expect(mockHistory.push).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('hanles [Reset offsest] click', async () => {
|
||||
userEvent.click(screen.getByText('Reset offsest'));
|
||||
expect(history.location.pathname).toEqual(
|
||||
clusterConsumerGroupResetOffsetsPath(clusterName, groupId)
|
||||
);
|
||||
});
|
||||
|
||||
it('shows confirmation modal on consumer group delete', async () => {
|
||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
||||
userEvent.click(screen.getByText('Delete consumer group'));
|
||||
await waitFor(() =>
|
||||
expect(screen.queryByRole('dialog')).toBeInTheDocument()
|
||||
);
|
||||
userEvent.click(screen.getByText('Cancel'));
|
||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('hanles [Delete consumer group] click', async () => {
|
||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
||||
userEvent.click(screen.getByText('Delete consumer group'));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.queryByRole('dialog')).toBeInTheDocument()
|
||||
);
|
||||
|
||||
const deleteConsumerGroupMock = fetchMock.deleteOnce(
|
||||
`/api/clusters/${clusterName}/consumer-groups/${groupId}`,
|
||||
200
|
||||
);
|
||||
userEvent.click(screen.getByText('Submit'));
|
||||
await waitFor(() =>
|
||||
expect(deleteConsumerGroupMock.called()).toBeTruthy()
|
||||
);
|
||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
||||
expect(history.location.pathname).toEqual(
|
||||
clusterConsumerGroupsPath(clusterName)
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
import React from 'react';
|
||||
import { containerRendersView } from 'lib/testHelpers';
|
||||
import Details from 'components/ConsumerGroups/Details/Details';
|
||||
import DetailsContainer from 'components/ConsumerGroups/Details/DetailsContainer';
|
||||
|
||||
describe('DetailsContainer', () => {
|
||||
containerRendersView(<DetailsContainer />, Details);
|
||||
});
|
|
@ -1,157 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Details component when consumer gruops are NOT fetched Matches the snapshot 1`] = `
|
||||
<div
|
||||
className="section"
|
||||
>
|
||||
<div
|
||||
className="level"
|
||||
>
|
||||
<div
|
||||
className="level-item level-left"
|
||||
>
|
||||
<Breadcrumb
|
||||
links={
|
||||
Array [
|
||||
Object {
|
||||
"href": "/ui/clusters/local/consumer-groups",
|
||||
"label": "All Consumer Groups",
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
test
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
</div>
|
||||
<PageLoader />
|
||||
<ConfirmationModal
|
||||
isOpen={false}
|
||||
onCancel={[Function]}
|
||||
onConfirm={[Function]}
|
||||
>
|
||||
Are you sure you want to delete this consumer group?
|
||||
</ConfirmationModal>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Details component when consumer gruops are fetched Matches the snapshot 1`] = `
|
||||
<div
|
||||
className="section"
|
||||
>
|
||||
<div
|
||||
className="level"
|
||||
>
|
||||
<div
|
||||
className="level-item level-left"
|
||||
>
|
||||
<Breadcrumb
|
||||
links={
|
||||
Array [
|
||||
Object {
|
||||
"href": "/ui/clusters/local/consumer-groups",
|
||||
"label": "All Consumer Groups",
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
test
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="box"
|
||||
>
|
||||
<div
|
||||
className="level"
|
||||
>
|
||||
<div
|
||||
className="level-item level-right buttons"
|
||||
>
|
||||
<button
|
||||
className="button"
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Reset offsets
|
||||
</button>
|
||||
<button
|
||||
className="button is-danger"
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Delete consumer group
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<table
|
||||
className="table is-striped is-fullwidth"
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
Consumer ID
|
||||
</th>
|
||||
<th>
|
||||
Host
|
||||
</th>
|
||||
<th>
|
||||
Topic
|
||||
</th>
|
||||
<th>
|
||||
Partition
|
||||
</th>
|
||||
<th>
|
||||
Messages behind
|
||||
</th>
|
||||
<th>
|
||||
Current offset
|
||||
</th>
|
||||
<th>
|
||||
End offset
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<ListItem
|
||||
clusterName="local"
|
||||
consumer={
|
||||
Object {
|
||||
"consumerId": "consumer-messages-consumer-1-122fbf98-643b-491d-8aec-c0641d2513d0",
|
||||
"currentOffset": 394,
|
||||
"endOffset": 394,
|
||||
"host": "/172.31.9.153",
|
||||
"messagesBehind": 0,
|
||||
"partition": 6,
|
||||
"topic": "messages",
|
||||
}
|
||||
}
|
||||
key="consumer-messages-consumer-1-122fbf98-643b-491d-8aec-c0641d2513d0"
|
||||
/>
|
||||
<ListItem
|
||||
clusterName="local"
|
||||
consumer={
|
||||
Object {
|
||||
"consumerId": "consumer-messages-consumer-1-122fbf98-643b-491d-8aec-c0641d2513d1",
|
||||
"currentOffset": 384,
|
||||
"endOffset": 384,
|
||||
"host": "/172.31.9.153",
|
||||
"messagesBehind": 0,
|
||||
"partition": 7,
|
||||
"topic": "messages",
|
||||
}
|
||||
}
|
||||
key="consumer-messages-consumer-1-122fbf98-643b-491d-8aec-c0641d2513d1"
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<ConfirmationModal
|
||||
isOpen={false}
|
||||
onCancel={[Function]}
|
||||
onConfirm={[Function]}
|
||||
>
|
||||
Are you sure you want to delete this consumer group?
|
||||
</ConfirmationModal>
|
||||
</div>
|
||||
`;
|
|
@ -1,74 +1,62 @@
|
|||
import React from 'react';
|
||||
import { ClusterName } from 'redux/interfaces';
|
||||
import { ConsumerGroup } from 'generated-sources';
|
||||
import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb';
|
||||
import { Table } from 'components/common/table/Table/Table.styled';
|
||||
import TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeaderCell';
|
||||
import PageHeading from 'components/common/PageHeading/PageHeading';
|
||||
import Search from 'components/common/Search/Search';
|
||||
import { ControlPanelWrapper } from 'components/common/ControlPanel/ControlPanel.styled';
|
||||
import { useAppSelector } from 'lib/hooks/redux';
|
||||
import { selectAll } from 'redux/reducers/consumerGroups/consumerGroupsSlice';
|
||||
|
||||
import ListItem from './ListItem';
|
||||
|
||||
interface Props {
|
||||
clusterName: ClusterName;
|
||||
consumerGroups: ConsumerGroup[];
|
||||
}
|
||||
|
||||
const List: React.FC<Props> = ({ consumerGroups }) => {
|
||||
const List: React.FC = () => {
|
||||
const consumerGroups = useAppSelector(selectAll);
|
||||
const [searchText, setSearchText] = React.useState<string>('');
|
||||
|
||||
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setSearchText(event.target.value);
|
||||
const handleInputChange = (search: string) => {
|
||||
setSearchText(search);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="section">
|
||||
<Breadcrumb>All Consumer Groups</Breadcrumb>
|
||||
|
||||
<div className="box">
|
||||
<div>
|
||||
<div className="columns">
|
||||
<div className="column is-half is-offset-half">
|
||||
<input
|
||||
id="searchText"
|
||||
type="text"
|
||||
name="searchText"
|
||||
className="input"
|
||||
placeholder="Search"
|
||||
value={searchText}
|
||||
onChange={handleInputChange}
|
||||
<div>
|
||||
<PageHeading text="Consumers" />
|
||||
<ControlPanelWrapper hasInput>
|
||||
<Search
|
||||
placeholder="Search"
|
||||
value={searchText}
|
||||
handleSearch={handleInputChange}
|
||||
/>
|
||||
</ControlPanelWrapper>
|
||||
<Table isFullwidth>
|
||||
<thead>
|
||||
<tr>
|
||||
<TableHeaderCell title="Consumer group ID" />
|
||||
<TableHeaderCell title="Num of members" />
|
||||
<TableHeaderCell title="Num of topics" />
|
||||
<TableHeaderCell title="Messages behind" />
|
||||
<TableHeaderCell title="Coordinator" />
|
||||
<TableHeaderCell title="State" />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{consumerGroups
|
||||
.filter(
|
||||
(consumerGroup) =>
|
||||
!searchText || consumerGroup?.groupId?.indexOf(searchText) >= 0
|
||||
)
|
||||
.map((consumerGroup) => (
|
||||
<ListItem
|
||||
key={consumerGroup.groupId}
|
||||
consumerGroup={consumerGroup}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<table className="table is-striped is-fullwidth is-hoverable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Consumer group ID</th>
|
||||
<th>Num of members</th>
|
||||
<th>Num of topics</th>
|
||||
<th>Messages behind</th>
|
||||
<th>Coordinator</th>
|
||||
<th>State</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{consumerGroups
|
||||
.filter(
|
||||
(consumerGroup) =>
|
||||
!searchText ||
|
||||
consumerGroup?.groupId?.indexOf(searchText) >= 0
|
||||
)
|
||||
.map((consumerGroup) => (
|
||||
<ListItem
|
||||
key={consumerGroup.groupId}
|
||||
consumerGroup={consumerGroup}
|
||||
/>
|
||||
))}
|
||||
{consumerGroups.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={10}>No active consumer groups</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{consumerGroups.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={10}>No active consumer groups</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,26 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { ClusterName, RootState } from 'redux/interfaces';
|
||||
import { getConsumerGroupsList } from 'redux/reducers/consumerGroups/selectors';
|
||||
import { withRouter, RouteComponentProps } from 'react-router-dom';
|
||||
|
||||
import List from './List';
|
||||
|
||||
interface RouteProps {
|
||||
clusterName: ClusterName;
|
||||
}
|
||||
|
||||
type OwnProps = RouteComponentProps<RouteProps>;
|
||||
|
||||
const mapStateToProps = (
|
||||
state: RootState,
|
||||
{
|
||||
match: {
|
||||
params: { clusterName },
|
||||
},
|
||||
}: OwnProps
|
||||
) => ({
|
||||
clusterName,
|
||||
consumerGroups: getConsumerGroupsList(state),
|
||||
});
|
||||
|
||||
export default withRouter(connect(mapStateToProps)(List));
|
|
@ -1,26 +1,25 @@
|
|||
import React from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { ConsumerGroup } from 'generated-sources';
|
||||
import ConsumerGroupStateTag from 'components/common/ConsumerGroupState/ConsumerGroupStateTag';
|
||||
import TagStyled from 'components/common/Tag/Tag.styled';
|
||||
import { TableKeyLink } from 'components/common/table/Table/TableKeyLink.styled';
|
||||
|
||||
const ListItem: React.FC<{ consumerGroup: ConsumerGroup }> = ({
|
||||
consumerGroup,
|
||||
}) => {
|
||||
const history = useHistory();
|
||||
|
||||
function goToConsumerGroupDetails() {
|
||||
history.push(`consumer-groups/${consumerGroup.groupId}`);
|
||||
}
|
||||
|
||||
return (
|
||||
<tr className="is-clickable" onClick={goToConsumerGroupDetails}>
|
||||
<td>{consumerGroup.groupId}</td>
|
||||
<tr>
|
||||
<TableKeyLink>
|
||||
<Link to={`consumer-groups/${consumerGroup.groupId}`}>
|
||||
{consumerGroup.groupId}
|
||||
</Link>
|
||||
</TableKeyLink>
|
||||
<td>{consumerGroup.members}</td>
|
||||
<td>{consumerGroup.topics}</td>
|
||||
<td>{consumerGroup.messagesBehind}</td>
|
||||
<td>{consumerGroup.coordinator?.id}</td>
|
||||
<td>
|
||||
<ConsumerGroupStateTag state={consumerGroup.state} />
|
||||
<TagStyled color="yellow">{consumerGroup.state}</TagStyled>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
|
|
|
@ -1,46 +1,46 @@
|
|||
import React from 'react';
|
||||
import { shallow, mount } from 'enzyme';
|
||||
import List from 'components/ConsumerGroups/List/List';
|
||||
import { screen } from '@testing-library/react';
|
||||
import { StaticRouter } from 'react-router';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { render } from 'lib/testHelpers';
|
||||
import { store } from 'redux/store';
|
||||
import { fetchConsumerGroups } from 'redux/reducers/consumerGroups/consumerGroupsSlice';
|
||||
import { consumerGroups } from 'redux/reducers/consumerGroups/__test__/fixtures';
|
||||
|
||||
describe('List', () => {
|
||||
const mockConsumerGroups = [
|
||||
{
|
||||
groupId: 'groupId',
|
||||
members: 0,
|
||||
topics: 1,
|
||||
simple: false,
|
||||
partitionAssignor: '',
|
||||
coordinator: {
|
||||
id: 1,
|
||||
host: 'host',
|
||||
},
|
||||
partitions: [
|
||||
{
|
||||
consumerId: null,
|
||||
currentOffset: 0,
|
||||
endOffset: 0,
|
||||
host: null,
|
||||
messagesBehind: 0,
|
||||
partition: 1,
|
||||
topic: 'topic',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
const component = shallow(
|
||||
<List consumerGroups={mockConsumerGroups} clusterName="cluster" />
|
||||
);
|
||||
const componentEmpty = mount(
|
||||
<List consumerGroups={[]} clusterName="cluster" />
|
||||
beforeEach(() =>
|
||||
render(
|
||||
<StaticRouter>
|
||||
<List />
|
||||
</StaticRouter>
|
||||
)
|
||||
);
|
||||
|
||||
it('render empty List consumer Groups', () => {
|
||||
expect(componentEmpty.find('td').text()).toEqual(
|
||||
'No active consumer groups'
|
||||
);
|
||||
it('renders empty table', () => {
|
||||
expect(screen.getByRole('table')).toBeInTheDocument();
|
||||
expect(screen.getByText('No active consumer groups')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('render List consumer Groups', () => {
|
||||
expect(component.exists('.section')).toBeTruthy();
|
||||
describe('consumerGroups are fecthed', () => {
|
||||
beforeEach(() => {
|
||||
store.dispatch({
|
||||
type: fetchConsumerGroups.fulfilled.type,
|
||||
payload: consumerGroups,
|
||||
});
|
||||
});
|
||||
|
||||
it('renders all rows with consumers', () => {
|
||||
expect(screen.getByText('groupId1')).toBeInTheDocument();
|
||||
expect(screen.getByText('groupId2')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('when searched', () => {
|
||||
it('renders only searched consumers', () => {
|
||||
userEvent.type(screen.getByPlaceholderText('Search'), 'groupId1');
|
||||
expect(screen.getByText('groupId1')).toBeInTheDocument();
|
||||
expect(screen.getByText('groupId2')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
import React from 'react';
|
||||
import { containerRendersView } from 'lib/testHelpers';
|
||||
import ListContainer from 'components/ConsumerGroups/List/ListContainer';
|
||||
import List from 'components/ConsumerGroups/List/List';
|
||||
|
||||
describe('ListContainer', () => {
|
||||
containerRendersView(<ListContainer />, List);
|
||||
});
|
|
@ -1,6 +1,9 @@
|
|||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { mount } from 'enzyme';
|
||||
import ListItem from 'components/ConsumerGroups/List/ListItem';
|
||||
import { ThemeProvider } from 'styled-components';
|
||||
import theme from 'theme/theme';
|
||||
import { StaticRouter } from 'react-router';
|
||||
|
||||
describe('List', () => {
|
||||
const mockConsumerGroup = {
|
||||
|
@ -25,9 +28,19 @@ describe('List', () => {
|
|||
},
|
||||
],
|
||||
};
|
||||
const component = shallow(<ListItem consumerGroup={mockConsumerGroup} />);
|
||||
const component = mount(
|
||||
<StaticRouter>
|
||||
<ThemeProvider theme={theme}>
|
||||
<table>
|
||||
<tbody>
|
||||
<ListItem consumerGroup={mockConsumerGroup} />
|
||||
</tbody>
|
||||
</table>
|
||||
</ThemeProvider>
|
||||
</StaticRouter>
|
||||
);
|
||||
|
||||
it('render empty ListItem', () => {
|
||||
expect(component.exists('.is-clickable')).toBeTruthy();
|
||||
expect(component.exists('tr')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,78 +0,0 @@
|
|||
import React from 'react';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import { clusterBrokersPath, clusterTopicsPath } from 'lib/paths';
|
||||
import { Cluster, ServerStatus } from 'generated-sources';
|
||||
import BytesFormatted from 'components/common/BytesFormatted/BytesFormatted';
|
||||
|
||||
interface ClusterWidgetProps {
|
||||
cluster: Cluster;
|
||||
}
|
||||
|
||||
const ClusterWidget: React.FC<ClusterWidgetProps> = ({
|
||||
cluster: {
|
||||
name,
|
||||
status,
|
||||
topicCount,
|
||||
brokerCount,
|
||||
bytesInPerSec,
|
||||
bytesOutPerSec,
|
||||
onlinePartitionCount,
|
||||
readOnly,
|
||||
version,
|
||||
},
|
||||
}) => (
|
||||
<div className="column is-full-modile is-6">
|
||||
<div className="box">
|
||||
<div className="title is-6 has-text-overflow-ellipsis">
|
||||
<div
|
||||
className={`tag mr-2 ${
|
||||
status === ServerStatus.ONLINE ? 'is-success' : 'is-danger'
|
||||
}`}
|
||||
>
|
||||
{status}
|
||||
</div>
|
||||
{readOnly && <div className="tag mr-2 is-info is-light">readonly</div>}
|
||||
{name}
|
||||
</div>
|
||||
|
||||
<table className="table is-fullwidth">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Version</th>
|
||||
<td>{version}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Brokers</th>
|
||||
<td>
|
||||
<NavLink to={clusterBrokersPath(name)}>{brokerCount}</NavLink>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Partitions</th>
|
||||
<td>{onlinePartitionCount}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Topics</th>
|
||||
<td>
|
||||
<NavLink to={clusterTopicsPath(name)}>{topicCount}</NavLink>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Production</th>
|
||||
<td>
|
||||
<BytesFormatted value={bytesInPerSec} />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Consumption</th>
|
||||
<td>
|
||||
<BytesFormatted value={bytesOutPerSec} />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default ClusterWidget;
|
|
@ -1,11 +1,15 @@
|
|||
import React from 'react';
|
||||
import { chunk } from 'lodash';
|
||||
import { v4 } from 'uuid';
|
||||
import MetricsWrapper from 'components/common/Dashboard/MetricsWrapper';
|
||||
import Indicator from 'components/common/Dashboard/Indicator';
|
||||
import * as Metrics from 'components/common/Metrics';
|
||||
import { Cluster } from 'generated-sources';
|
||||
|
||||
import ClusterWidget from './ClusterWidget';
|
||||
import TagStyled from 'components/common/Tag/Tag.styled';
|
||||
import { Table } from 'components/common/table/Table/Table.styled';
|
||||
import TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeaderCell';
|
||||
import BytesFormatted from 'components/common/BytesFormatted/BytesFormatted';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import { clusterTopicsPath } from 'lib/paths';
|
||||
import Switch from 'components/common/Switch/Switch';
|
||||
|
||||
interface Props {
|
||||
clusters: Cluster[];
|
||||
|
@ -41,37 +45,68 @@ const ClustersWidget: React.FC<Props> = ({
|
|||
const handleSwitch = () => setShowOfflineOnly(!showOfflineOnly);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h5 className="title is-5">Clusters</h5>
|
||||
|
||||
<MetricsWrapper>
|
||||
<Indicator label="Online Clusters">
|
||||
<span className="tag is-success">{onlineClusters.length}</span>
|
||||
</Indicator>
|
||||
<Indicator label="Offline Clusters">
|
||||
<span className="tag is-danger">{offlineClusters.length}</span>
|
||||
</Indicator>
|
||||
<Indicator label="Hide online clusters">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="switch is-rounded"
|
||||
name="switchRoundedDefault"
|
||||
id="switchRoundedDefault"
|
||||
checked={showOfflineOnly}
|
||||
onChange={handleSwitch}
|
||||
/>
|
||||
<label htmlFor="switchRoundedDefault" />
|
||||
</Indicator>
|
||||
</MetricsWrapper>
|
||||
|
||||
<>
|
||||
<Metrics.Wrapper>
|
||||
<Metrics.Section>
|
||||
<Metrics.Indicator
|
||||
label={<TagStyled color="green">Online</TagStyled>}
|
||||
>
|
||||
<span data-testid="onlineCount">{onlineClusters.length}</span>{' '}
|
||||
<Metrics.LightText>clusters</Metrics.LightText>
|
||||
</Metrics.Indicator>
|
||||
<Metrics.Indicator
|
||||
label={<TagStyled color="gray">Offline</TagStyled>}
|
||||
>
|
||||
<span data-testid="offlineCount">{offlineClusters.length}</span>{' '}
|
||||
<Metrics.LightText>clusters</Metrics.LightText>
|
||||
</Metrics.Indicator>
|
||||
</Metrics.Section>
|
||||
</Metrics.Wrapper>
|
||||
<div className="p-4">
|
||||
<Switch
|
||||
name="switchRoundedDefault"
|
||||
checked={showOfflineOnly}
|
||||
onChange={handleSwitch}
|
||||
/>
|
||||
<span>Only offline clusters</span>
|
||||
</div>
|
||||
{clusterList.map((chunkItem) => (
|
||||
<div className="columns" key={chunkItem.id}>
|
||||
{chunkItem.data.map((cluster) => (
|
||||
<ClusterWidget cluster={cluster} key={cluster.name} />
|
||||
))}
|
||||
</div>
|
||||
<Table key={chunkItem.id} isFullwidth>
|
||||
<thead>
|
||||
<tr>
|
||||
<TableHeaderCell title="Cluster name" />
|
||||
<TableHeaderCell title="Version" />
|
||||
<TableHeaderCell title="Brokers count" />
|
||||
<TableHeaderCell title="Partitions" />
|
||||
<TableHeaderCell title="Topics" />
|
||||
<TableHeaderCell title="Production" />
|
||||
<TableHeaderCell title="Consumption" />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{chunkItem.data.map((cluster) => (
|
||||
<tr key={cluster.name}>
|
||||
<td>{cluster.name}</td>
|
||||
<td>{cluster.version}</td>
|
||||
<td>{cluster.brokerCount}</td>
|
||||
<td>{cluster.onlinePartitionCount}</td>
|
||||
<td>
|
||||
<NavLink to={clusterTopicsPath(cluster.name)}>
|
||||
{cluster.topicCount}
|
||||
</NavLink>
|
||||
</td>
|
||||
<td>
|
||||
<BytesFormatted value={cluster.bytesInPerSec} />
|
||||
</td>
|
||||
<td>
|
||||
<BytesFormatted value={cluster.bytesOutPerSec} />
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ import {
|
|||
getClusterList,
|
||||
getOnlineClusters,
|
||||
getOfflineClusters,
|
||||
} from 'redux/reducers/clusters/selectors';
|
||||
} from 'redux/reducers/clusters/clustersSlice';
|
||||
import { RootState } from 'redux/interfaces';
|
||||
|
||||
import ClustersWidget from './ClustersWidget';
|
||||
|
|
|
@ -1,87 +0,0 @@
|
|||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { ServerStatus } from 'generated-sources';
|
||||
import { clusterBrokersPath, clusterTopicsPath } from 'lib/paths';
|
||||
import ClusterWidget from 'components/Dashboard/ClustersWidget/ClusterWidget';
|
||||
|
||||
import { offlineCluster, onlineCluster } from './fixtures';
|
||||
|
||||
describe('ClusterWidget', () => {
|
||||
describe('when cluster is online', () => {
|
||||
it('renders with correct tag', () => {
|
||||
const tag = shallow(<ClusterWidget cluster={onlineCluster} />).find(
|
||||
'.tag'
|
||||
);
|
||||
expect(tag.hasClass('is-success')).toBeTruthy();
|
||||
expect(tag.text()).toEqual(ServerStatus.ONLINE);
|
||||
});
|
||||
|
||||
it('renders table', () => {
|
||||
const table = shallow(<ClusterWidget cluster={onlineCluster} />).find(
|
||||
'table'
|
||||
);
|
||||
expect(table.hasClass('is-fullwidth')).toBeTruthy();
|
||||
|
||||
expect(
|
||||
table.find(`NavLink[to="${clusterBrokersPath(onlineCluster.name)}"]`)
|
||||
.exists
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
table.find(`NavLink[to="${clusterTopicsPath(onlineCluster.name)}"]`)
|
||||
.exists
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('matches snapshot', () => {
|
||||
expect(
|
||||
shallow(<ClusterWidget cluster={onlineCluster} />)
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when cluster is offline', () => {
|
||||
it('renders with correct tag', () => {
|
||||
const tag = shallow(<ClusterWidget cluster={offlineCluster} />).find(
|
||||
'.tag'
|
||||
);
|
||||
|
||||
expect(tag.hasClass('is-danger')).toBeTruthy();
|
||||
expect(tag.text()).toEqual(ServerStatus.OFFLINE);
|
||||
});
|
||||
|
||||
it('renders table', () => {
|
||||
const table = shallow(<ClusterWidget cluster={offlineCluster} />).find(
|
||||
'table'
|
||||
);
|
||||
expect(table.hasClass('is-fullwidth')).toBeTruthy();
|
||||
|
||||
expect(
|
||||
table.find(`NavLink[to="${clusterBrokersPath(onlineCluster.name)}"]`)
|
||||
.exists
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
table.find(`NavLink[to="${clusterTopicsPath(onlineCluster.name)}"]`)
|
||||
.exists
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('matches snapshot', () => {
|
||||
expect(
|
||||
shallow(<ClusterWidget cluster={offlineCluster} />)
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when cluster is read-only', () => {
|
||||
it('renders the tag', () => {
|
||||
expect(
|
||||
shallow(
|
||||
<ClusterWidget cluster={{ ...onlineCluster, readOnly: true }} />
|
||||
)
|
||||
.find('.title')
|
||||
.childAt(1)
|
||||
.text()
|
||||
).toEqual('readonly');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,37 +1,33 @@
|
|||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { StaticRouter } from 'react-router';
|
||||
import { screen } from '@testing-library/react';
|
||||
import ClustersWidget from 'components/Dashboard/ClustersWidget/ClustersWidget';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { render } from 'lib/testHelpers';
|
||||
|
||||
import { offlineCluster, onlineCluster, clusters } from './fixtures';
|
||||
|
||||
const component = () =>
|
||||
shallow(
|
||||
<ClustersWidget
|
||||
clusters={clusters}
|
||||
onlineClusters={[onlineCluster]}
|
||||
offlineClusters={[offlineCluster]}
|
||||
/>
|
||||
const setupComponent = () =>
|
||||
render(
|
||||
<StaticRouter>
|
||||
<ClustersWidget
|
||||
clusters={clusters}
|
||||
onlineClusters={[onlineCluster]}
|
||||
offlineClusters={[offlineCluster]}
|
||||
/>
|
||||
</StaticRouter>
|
||||
);
|
||||
|
||||
describe('ClustersWidget', () => {
|
||||
beforeEach(() => setupComponent());
|
||||
|
||||
it('renders clusterWidget list', () => {
|
||||
const clusterWidget = component().find('ClusterWidget');
|
||||
expect(clusterWidget.length).toBe(2);
|
||||
});
|
||||
|
||||
it('renders ClusterWidget', () => {
|
||||
expect(component().exists('ClusterWidget')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('renders columns', () => {
|
||||
expect(component().exists('.columns')).toBeTruthy();
|
||||
expect(screen.getAllByRole('row').length).toBe(3);
|
||||
});
|
||||
|
||||
it('hides online cluster widgets', () => {
|
||||
const value = component();
|
||||
const input = value.find('input');
|
||||
expect(value.find('ClusterWidget').length).toBe(2);
|
||||
input.simulate('change', { target: { checked: true } });
|
||||
expect(value.find('ClusterWidget').length).toBe(1);
|
||||
expect(screen.getAllByRole('row').length).toBe(3);
|
||||
userEvent.click(screen.getByRole('checkbox'));
|
||||
expect(screen.getAllByRole('row').length).toBe(2);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -3,16 +3,24 @@ import { mount } from 'enzyme';
|
|||
import { containerRendersView } from 'lib/testHelpers';
|
||||
import ClustersWidget from 'components/Dashboard/ClustersWidget/ClustersWidget';
|
||||
import ClustersWidgetContainer from 'components/Dashboard/ClustersWidget/ClustersWidgetContainer';
|
||||
import theme from 'theme/theme';
|
||||
import { ThemeProvider } from 'styled-components';
|
||||
|
||||
describe('ClustersWidgetContainer', () => {
|
||||
containerRendersView(<ClustersWidgetContainer />, ClustersWidget);
|
||||
describe('view empty ClusterWidget', () => {
|
||||
const setupEmptyWrapper = () => (
|
||||
<ClustersWidget clusters={[]} onlineClusters={[]} offlineClusters={[]} />
|
||||
<ThemeProvider theme={theme}>
|
||||
<ClustersWidget
|
||||
clusters={[]}
|
||||
onlineClusters={[]}
|
||||
offlineClusters={[]}
|
||||
/>
|
||||
</ThemeProvider>
|
||||
);
|
||||
it(' is empty when no online clusters', () => {
|
||||
const wrapper = mount(setupEmptyWrapper());
|
||||
expect(wrapper.find('.is-success').text()).toBe('0');
|
||||
expect(wrapper.find('[data-testid="onlineCount"]').text()).toBe('0');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,171 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ClusterWidget when cluster is offline matches snapshot 1`] = `
|
||||
<div
|
||||
className="column is-full-modile is-6"
|
||||
>
|
||||
<div
|
||||
className="box"
|
||||
>
|
||||
<div
|
||||
className="title is-6 has-text-overflow-ellipsis"
|
||||
>
|
||||
<div
|
||||
className="tag mr-2 is-danger"
|
||||
>
|
||||
offline
|
||||
</div>
|
||||
local
|
||||
</div>
|
||||
<table
|
||||
className="table is-fullwidth"
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>
|
||||
Version
|
||||
</th>
|
||||
<td />
|
||||
</tr>
|
||||
<tr>
|
||||
<th>
|
||||
Brokers
|
||||
</th>
|
||||
<td>
|
||||
<NavLink
|
||||
to="/ui/clusters/local/brokers"
|
||||
>
|
||||
1
|
||||
</NavLink>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>
|
||||
Partitions
|
||||
</th>
|
||||
<td>
|
||||
2
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>
|
||||
Topics
|
||||
</th>
|
||||
<td>
|
||||
<NavLink
|
||||
to="/ui/clusters/local/topics"
|
||||
>
|
||||
2
|
||||
</NavLink>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>
|
||||
Production
|
||||
</th>
|
||||
<td>
|
||||
<BytesFormatted
|
||||
value={8000.00000673768}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>
|
||||
Consumption
|
||||
</th>
|
||||
<td>
|
||||
<BytesFormatted
|
||||
value={0.815306356729712}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`ClusterWidget when cluster is online matches snapshot 1`] = `
|
||||
<div
|
||||
className="column is-full-modile is-6"
|
||||
>
|
||||
<div
|
||||
className="box"
|
||||
>
|
||||
<div
|
||||
className="title is-6 has-text-overflow-ellipsis"
|
||||
>
|
||||
<div
|
||||
className="tag mr-2 is-success"
|
||||
>
|
||||
online
|
||||
</div>
|
||||
secondLocal
|
||||
</div>
|
||||
<table
|
||||
className="table is-fullwidth"
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>
|
||||
Version
|
||||
</th>
|
||||
<td />
|
||||
</tr>
|
||||
<tr>
|
||||
<th>
|
||||
Brokers
|
||||
</th>
|
||||
<td>
|
||||
<NavLink
|
||||
to="/ui/clusters/secondLocal/brokers"
|
||||
>
|
||||
1
|
||||
</NavLink>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>
|
||||
Partitions
|
||||
</th>
|
||||
<td>
|
||||
6
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>
|
||||
Topics
|
||||
</th>
|
||||
<td>
|
||||
<NavLink
|
||||
to="/ui/clusters/secondLocal/topics"
|
||||
>
|
||||
3
|
||||
</NavLink>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>
|
||||
Production
|
||||
</th>
|
||||
<td>
|
||||
<BytesFormatted
|
||||
value={0.00003061819685376472}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>
|
||||
Consumption
|
||||
</th>
|
||||
<td>
|
||||
<BytesFormatted
|
||||
value={5.737800890036267}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
|
@ -1,16 +1,9 @@
|
|||
import React from 'react';
|
||||
import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb';
|
||||
|
||||
import ClustersWidgetContainer from './ClustersWidget/ClustersWidgetContainer';
|
||||
|
||||
const Dashboard: React.FC = () => (
|
||||
<div className="section">
|
||||
<div className="level">
|
||||
<div className="level-item level-left">
|
||||
<Breadcrumb>Dashboard</Breadcrumb>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<ClustersWidgetContainer />
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -5,10 +5,6 @@ import Dashboard from 'components/Dashboard/Dashboard';
|
|||
const component = shallow(<Dashboard />);
|
||||
|
||||
describe('Dashboard', () => {
|
||||
it('renders section', () => {
|
||||
expect(component.exists('.section')).toBe(true);
|
||||
});
|
||||
|
||||
it('renders ClustersWidget', () => {
|
||||
expect(component.exists('Connect(ClustersWidget)')).toBe(true);
|
||||
});
|
||||
|
|
|
@ -1,30 +0,0 @@
|
|||
import React from 'react';
|
||||
import Breadcrumb, {
|
||||
BreadcrumbItem,
|
||||
} from 'components/common/Breadcrumb/Breadcrumb';
|
||||
import { clusterKsqlDbPath, clusterKsqlDbQueryPath } from 'lib/paths';
|
||||
import { useParams, useRouteMatch } from 'react-router';
|
||||
|
||||
interface RouteParams {
|
||||
clusterName: string;
|
||||
}
|
||||
|
||||
const Breadcrumbs: React.FC = () => {
|
||||
const { clusterName } = useParams<RouteParams>();
|
||||
const isQuery = useRouteMatch(clusterKsqlDbQueryPath(clusterName));
|
||||
|
||||
if (!isQuery) {
|
||||
return <Breadcrumb>KSQLDB</Breadcrumb>;
|
||||
}
|
||||
|
||||
const links: BreadcrumbItem[] = [
|
||||
{
|
||||
label: 'KSQLDB',
|
||||
href: clusterKsqlDbPath(clusterName),
|
||||
},
|
||||
];
|
||||
|
||||
return <Breadcrumb links={links}>Query</Breadcrumb>;
|
||||
};
|
||||
|
||||
export default Breadcrumbs;
|
|
@ -1,33 +0,0 @@
|
|||
import React from 'react';
|
||||
import { StaticRouter } from 'react-router';
|
||||
import Breadcrumbs from 'components/KsqlDb/BreadCrumbs/BreadCrumbs';
|
||||
import { mount } from 'enzyme';
|
||||
import { clusterKsqlDbPath, clusterKsqlDbQueryPath } from 'lib/paths';
|
||||
|
||||
describe('BreadCrumbs', () => {
|
||||
const clusterName = 'local';
|
||||
const rootPathname = clusterKsqlDbPath(clusterName);
|
||||
const queryPathname = clusterKsqlDbQueryPath(clusterName);
|
||||
|
||||
const setupComponent = (pathname: string) => (
|
||||
<StaticRouter location={{ pathname }} context={{}}>
|
||||
<Breadcrumbs />
|
||||
</StaticRouter>
|
||||
);
|
||||
|
||||
it('Renders root path', () => {
|
||||
const component = mount(setupComponent(rootPathname));
|
||||
|
||||
expect(component.find({ children: 'KSQLDB' }).exists()).toBeTruthy();
|
||||
expect(component.find({ children: 'Query' }).exists()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('Renders query path', () => {
|
||||
const component = mount(setupComponent(queryPathname));
|
||||
|
||||
expect(
|
||||
component.find('a').find({ children: 'KSQLDB' }).exists()
|
||||
).toBeTruthy();
|
||||
expect(component.find({ children: 'Query' }).exists()).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -3,19 +3,13 @@ import { Switch, Route } from 'react-router-dom';
|
|||
import { clusterKsqlDbPath, clusterKsqlDbQueryPath } from 'lib/paths';
|
||||
import List from 'components/KsqlDb/List/List';
|
||||
import Query from 'components/KsqlDb/Query/Query';
|
||||
import Breadcrumbs from 'components/KsqlDb/BreadCrumbs/BreadCrumbs';
|
||||
|
||||
const KsqlDb: React.FC = () => {
|
||||
return (
|
||||
<div className="section">
|
||||
<Switch>
|
||||
<Route path={clusterKsqlDbPath()} component={Breadcrumbs} />
|
||||
</Switch>
|
||||
<Switch>
|
||||
<Route exact path={clusterKsqlDbPath()} component={List} />
|
||||
<Route exact path={clusterKsqlDbQueryPath()} component={Query} />
|
||||
</Switch>
|
||||
</div>
|
||||
<Switch>
|
||||
<Route exact path={clusterKsqlDbPath()} component={List} />
|
||||
<Route exact path={clusterKsqlDbQueryPath()} component={Query} />
|
||||
</Switch>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import Indicator from 'components/common/Dashboard/Indicator';
|
||||
import MetricsWrapper from 'components/common/Dashboard/MetricsWrapper';
|
||||
import * as Metrics from 'components/common/Metrics';
|
||||
import PageLoader from 'components/common/PageLoader/PageLoader';
|
||||
import ListItem from 'components/KsqlDb/List/ListItem';
|
||||
import React, { FC, useEffect } from 'react';
|
||||
|
@ -7,8 +6,11 @@ import { useDispatch, useSelector } from 'react-redux';
|
|||
import { useParams } from 'react-router';
|
||||
import { fetchKsqlDbTables } from 'redux/actions/thunks/ksqlDb';
|
||||
import { getKsqlDbTables } from 'redux/reducers/ksqlDb/selectors';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { clusterKsqlDbQueryPath } from 'lib/paths';
|
||||
import PageHeading from 'components/common/PageHeading/PageHeading';
|
||||
import { Table } from 'components/common/table/Table/Table.styled';
|
||||
import TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeaderCell';
|
||||
import { Button } from 'components/common/Button/Button';
|
||||
|
||||
const headers = [
|
||||
{ Header: 'Type', accessor: 'type' },
|
||||
|
@ -34,42 +36,40 @@ const List: FC = () => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<MetricsWrapper wrapperClassName="is-justify-content-space-between">
|
||||
<div className="column is-flex m-0 p-0">
|
||||
<Indicator
|
||||
className="level-left is-one-third mr-3"
|
||||
label="Tables"
|
||||
title="Tables"
|
||||
fetching={fetching}
|
||||
>
|
||||
<PageHeading text="KSQL DB">
|
||||
<Button
|
||||
isLink
|
||||
to={clusterKsqlDbQueryPath(clusterName)}
|
||||
buttonType="primary"
|
||||
buttonSize="M"
|
||||
>
|
||||
Execute KSQL request
|
||||
</Button>
|
||||
</PageHeading>
|
||||
<Metrics.Wrapper>
|
||||
<Metrics.Section>
|
||||
<Metrics.Indicator label="Tables" title="Tables" fetching={fetching}>
|
||||
{tablesCount}
|
||||
</Indicator>
|
||||
<Indicator
|
||||
className="level-left is-one-third ml-3"
|
||||
</Metrics.Indicator>
|
||||
<Metrics.Indicator
|
||||
label="Streams"
|
||||
title="Streams"
|
||||
fetching={fetching}
|
||||
>
|
||||
{streamsCount}
|
||||
</Indicator>
|
||||
</div>
|
||||
<Link
|
||||
to={clusterKsqlDbQueryPath(clusterName)}
|
||||
className="button is-primary"
|
||||
>
|
||||
Execute ksql
|
||||
</Link>
|
||||
</MetricsWrapper>
|
||||
<div className="box">
|
||||
</Metrics.Indicator>
|
||||
</Metrics.Section>
|
||||
</Metrics.Wrapper>
|
||||
<div>
|
||||
{fetching ? (
|
||||
<PageLoader />
|
||||
) : (
|
||||
<table className="table is-fullwidth">
|
||||
<Table isFullwidth>
|
||||
<thead>
|
||||
<tr>
|
||||
<th> </th>
|
||||
{headers.map(({ Header, accessor }) => (
|
||||
<th key={accessor}>{Header}</th>
|
||||
<TableHeaderCell title={Header} key={accessor} />
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
|
@ -79,11 +79,13 @@ const List: FC = () => {
|
|||
))}
|
||||
{rows.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={headers.length}>No tables or streams found</td>
|
||||
<td colSpan={headers.length + 1}>
|
||||
No tables or streams found
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</Table>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import IconButtonWrapper from 'components/common/Icons/IconButtonWrapper';
|
||||
import MessageToggleIcon from 'components/common/Icons/MessageToggleIcon';
|
||||
import React from 'react';
|
||||
|
||||
interface Props {
|
||||
|
@ -16,13 +18,9 @@ const ListItem: React.FC<Props> = ({ accessors, data }) => {
|
|||
<>
|
||||
<tr>
|
||||
<td>
|
||||
<span
|
||||
className="icon has-text-link is-size-7 is-small is-clickable"
|
||||
onClick={toggleIsOpen}
|
||||
aria-hidden
|
||||
>
|
||||
<i className={`fas fa-${isOpen ? 'minus' : 'plus'}`} />
|
||||
</span>
|
||||
<IconButtonWrapper onClick={toggleIsOpen}>
|
||||
<MessageToggleIcon isOpen={isOpen} />
|
||||
</IconButtonWrapper>
|
||||
</td>
|
||||
{accessors.map((accessor) => (
|
||||
<td key={accessor}>{data[accessor]}</td>
|
||||
|
|
|
@ -1,63 +1,37 @@
|
|||
import React from 'react';
|
||||
import List from 'components/KsqlDb/List/List';
|
||||
import { mount } from 'enzyme';
|
||||
import { StaticRouter } from 'react-router';
|
||||
import configureStore from 'redux-mock-store';
|
||||
import { Provider } from 'react-redux';
|
||||
import { RootState } from 'redux/interfaces';
|
||||
import { fetchKsqlDbTablesPayload } from 'redux/reducers/ksqlDb/__test__/fixtures';
|
||||
import { Route, Router } from 'react-router';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import { clusterKsqlDbPath } from 'lib/paths';
|
||||
import { render } from 'lib/testHelpers';
|
||||
import fetchMock from 'fetch-mock';
|
||||
import { screen, waitForElementToBeRemoved } from '@testing-library/dom';
|
||||
|
||||
const emptyPlaceholder = 'No tables or streams found';
|
||||
const history = createMemoryHistory();
|
||||
const clusterName = 'local';
|
||||
|
||||
const mockStore = configureStore();
|
||||
const renderComponent = () => {
|
||||
history.push(clusterKsqlDbPath(clusterName));
|
||||
render(
|
||||
<Router history={history}>
|
||||
<Route path={clusterKsqlDbPath(':clusterName')}>
|
||||
<List />
|
||||
</Route>
|
||||
</Router>
|
||||
);
|
||||
};
|
||||
|
||||
describe('KsqlDb List', () => {
|
||||
const pathname = `ui/clusters/local/ksql-db`;
|
||||
|
||||
it('Renders placeholder on empty data', () => {
|
||||
const initialState: Partial<RootState> = {
|
||||
ksqlDb: {
|
||||
tables: [],
|
||||
streams: [],
|
||||
executionResult: null,
|
||||
afterEach(() => fetchMock.reset());
|
||||
it('renders placeholder on empty data', async () => {
|
||||
fetchMock.post(
|
||||
{
|
||||
url: `/api/clusters/${clusterName}/ksql`,
|
||||
},
|
||||
loader: {
|
||||
GET_KSQL_DB_TABLES_AND_STREAMS: 'fetched',
|
||||
},
|
||||
};
|
||||
const store = mockStore(initialState);
|
||||
|
||||
const component = mount(
|
||||
<StaticRouter location={{ pathname }} context={{}}>
|
||||
<Provider store={store}>
|
||||
<List />
|
||||
</Provider>
|
||||
</StaticRouter>
|
||||
{ data: [] }
|
||||
);
|
||||
|
||||
expect(
|
||||
component.find({ children: emptyPlaceholder }).exists()
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('Renders rows', () => {
|
||||
const initialState: Partial<RootState> = {
|
||||
ksqlDb: { ...fetchKsqlDbTablesPayload, executionResult: null },
|
||||
loader: {
|
||||
GET_KSQL_DB_TABLES_AND_STREAMS: 'fetched',
|
||||
},
|
||||
};
|
||||
const store = mockStore(initialState);
|
||||
|
||||
const component = mount(
|
||||
<StaticRouter location={{ pathname }} context={{}}>
|
||||
<Provider store={store}>
|
||||
<List />
|
||||
</Provider>
|
||||
</StaticRouter>
|
||||
);
|
||||
|
||||
// 2 streams, 2 tables and 1 head tr
|
||||
expect(component.find('tr').length).toEqual(5);
|
||||
renderComponent();
|
||||
await waitForElementToBeRemoved(() => screen.getByRole('progressbar'));
|
||||
expect(screen.getByText('No tables or streams found')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
import styled from 'styled-components';
|
||||
|
||||
export const QueryWrapper = styled.div`
|
||||
padding: 16px;
|
||||
`;
|
||||
|
||||
export const KSQLInputsWrapper = styled.div`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
|
||||
padding-bottom: 16px;
|
||||
& > div {
|
||||
flex-grow: 1;
|
||||
}
|
||||
`;
|
||||
|
||||
export const KSQLInputHeader = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
`;
|
||||
|
||||
export const KSQLButtons = styled.div`
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
`;
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue