diff --git a/.storybook/main.js b/.storybook/main.mjs
similarity index 77%
rename from .storybook/main.js
rename to .storybook/main.mjs
index e37d482385..c90b1ac53b 100644
--- a/.storybook/main.js
+++ b/.storybook/main.mjs
@@ -17,17 +17,17 @@ illegal under applicable law, and the grant of the foregoing license
under the Apache 2.0 license is conditioned upon your compliance with
such restriction.
*/
-const path = require('path')
-module.exports = {
- babel: async options => {
- options.plugins.push('babel-plugin-inline-react-svg')
- return options
+const config = {
+ framework: {
+ name: '@storybook/react-vite',
+ options: {}
},
stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
- addons: [
- '@storybook/addon-links',
- '@storybook/addon-essentials',
- '@storybook/preset-scss'
- ]
+ addons: ['@storybook/addon-links', '@storybook/preset-scss', '@storybook/addon-docs'],
+ core: {
+ builder: '@storybook/builder-vite'
+ }
}
+
+export default config
diff --git a/.stylelintrc.json b/.stylelintrc.json
index ddee2a574c..6b6282220c 100644
--- a/.stylelintrc.json
+++ b/.stylelintrc.json
@@ -1,10 +1,40 @@
{
- "extends": ["stylelint-config-standard", "stylelint-config-rational-order"],
+ "extends": ["stylelint-config-standard", "stylelint-config-standard-scss", "stylelint-config-rational-order"],
"plugins": [
"stylelint-scss"
],
"rules": {
"at-rule-no-unknown": null,
- "scss/at-rule-no-unknown": true
+ "selector-no-vendor-prefix": null,
+ "scss/at-mixin-argumentless-call-parentheses": null,
+ "scss/dollar-variable-empty-line-before": null,
+ "scss/at-rule-no-unknown": true,
+ "media-feature-range-notation": null,
+ "property-no-vendor-prefix": null,
+ "selector-class-pattern": [
+ "^[a-z0-9]+(?:-[a-z0-9]+)*(?:__(?:[a-z0-9]+(?:-[a-z0-9]+)*))*(?:_(?:[a-z0-9]+(?:-[a-z0-9]+)*)(?:_[a-z0-9]+(?:-[a-z0-9]+)*)?)?$",
+ {
+ "resolveNestedSelectors": true,
+ "message": "Expected kebab-case or BEM: block-name, block-name__elem-name(__sub-elem…), block-name_mod-name[_mod-val], block-name__elem-name_mod-name[_mod-val]"
+ }
+ ],
+ "scss/at-mixin-pattern": [
+ "^(?:[a-z][a-z0-9]*(?:-[a-z0-9]+)*|[a-z][a-z0-9]*(?:[A-Z][a-z0-9]*)*)$",
+ {
+ "message": "Expected mixin name to be kebab-case (my-mixin) or camelCase (myMixin)"
+ }
+ ],
+ "custom-property-pattern": [
+ "^(?:[a-z][a-z0-9]*(?:-[a-z0-9]+)*|[a-z][a-z0-9]*(?:[A-Z][a-z0-9]*)*)$",
+ {
+ "message": "Expected variable name to be kebab-case (my-variable) or camelCase (myVariable)"
+ }
+ ],
+ "scss/dollar-variable-pattern": [
+ "^(?:[a-z][a-z0-9]*(?:-[a-z0-9]+)*|[a-z][a-z0-9]*(?:[A-Z][a-z0-9]*)*)$",
+ {
+ "message": "Expected variable name to be kebab-case (my-variable) or camelCase (myVariable)"
+ }
+ ]
}
}
diff --git a/config/paths.js b/config/paths.js
index b17443273e..ea07637455 100644
--- a/config/paths.js
+++ b/config/paths.js
@@ -83,7 +83,6 @@ module.exports = {
appJsConfig: resolveApp('jsconfig.json'),
yarnLockFile: resolveApp('yarn.lock'),
testsSetup: resolveModule(resolveApp, 'src/setupTests'),
- proxySetup: resolveApp('src/setupProxy.js'),
appNodeModules: resolveApp('node_modules'),
appWebpackCache: resolveApp('node_modules/.cache'),
appTsBuildInfoFile: resolveApp('node_modules/.cache/tsconfig.tsbuildinfo'),
diff --git a/eslint.config.mjs b/eslint.config.mjs
index 7931152bd2..da24470c03 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -16,7 +16,7 @@ export default [
globals: {
...globals.browser,
...globals.jest,
- ...globals.node,
+ ...globals.node
},
parserOptions: {
ecmaFeatures: {
diff --git a/package.json b/package.json
index 3c321a5fd9..af141ee1c3 100644
--- a/package.json
+++ b/package.json
@@ -7,7 +7,7 @@
"@dagrejs/dagre": "^1.1.5",
"@monaco-editor/react": "^4.7.0",
"@reduxjs/toolkit": "^1.9.5",
- "axios": "1.12.0",
+ "axios": "1.12.2",
"bfj": "^7.0.2",
"camelcase": "^6.3.0",
"chart.js": "^4.4.2",
@@ -32,14 +32,14 @@
"prompts": "^2.4.2",
"prop-types": "^15.8.1",
"qs": "^6.9.6",
- "react": "^18.2.0",
- "react-dom": "^18.2.0",
+ "react": "^19.2.0",
+ "react-dom": "^19.2.0",
"react-final-form": "^6.5.9",
"react-final-form-arrays": "^3.1.4",
"react-modal-promise": "^1.0.2",
"react-redux": "^7.2.9",
"react-refresh": "^0.11.0",
- "react-router-dom": "6.22.3",
+ "react-router-dom": "7.9.4",
"react-text-mask": "^5.4.3",
"react-transition-group": "^4.4.5",
"reactflow": "^11.11.1",
@@ -57,12 +57,13 @@
"start": "vite",
"build": "vite build",
"lint": "eslint .",
+ "stylelint": "stylelint '**/*.{css,scss}'",
"preview": "vite preview",
"preinstall": "npx force-resolutions",
"test:coverage": "npm run test -- --coverage --watchAll=false",
"docker": "docker build -t ${MLRUN_DOCKER_REGISTRY}${MLRUN_DOCKER_REPO:-mlrun}/mlrun-ui:${MLRUN_DOCKER_TAG:-latest} --build-arg COMMIT_HASH=\"`git rev-parse --short HEAD`\" --build-arg DATE=\"`date -u`\" -f Dockerfile .",
"generate-rn": "./generate-release-notes.js ${MLRUN_OLD_VERSION} ${MLRUN_VERSION} ${MLRUN_RELEASE_BRANCH} ${MLRUN_RELEASE_TYPE}",
- "storybook": "start-storybook -p 6006",
+ "storybook": "storybook dev -p 6006",
"build-storybook": "build-storybook",
"mock-server": "node scripts/mockServer.js",
"mock-server:dev": "nodemon --watch tests/mockServer scripts/mockServer.js",
@@ -77,89 +78,78 @@
"nui": "npm unlink iguazio.dashboard-react-controls"
},
"devDependencies": {
- "@babel/core": "^7.16.0",
- "@babel/eslint-parser": "^7.24.1",
- "@babel/node": "^7.14.9",
- "@babel/plugin-proposal-logical-assignment-operators": "^7.12.1",
+ "@babel/core": "^7.28.4",
+ "@babel/node": "^7.28.0",
+ "@babel/plugin-transform-logical-assignment-operators": "^7.27.1",
"@babel/polyfill": "^7.12.1",
- "@babel/preset-env": "^7.13.12",
- "@babel/register": "^7.13.14",
+ "@babel/preset-env": "^7.28.3",
+ "@babel/preset-react": "^7.27.1",
+ "@babel/register": "^7.28.3",
"@cucumber/cucumber": "^10.3.1",
"@d4c/numjs": "^0.17.34",
- "@eslint/js": "^9.19.0",
- "@storybook/addon-actions": "^8.0.1",
- "@storybook/addon-essentials": "^8.0.1",
- "@storybook/addon-links": "^8.0.1",
+ "@eslint/js": "^9.37.0",
+ "@storybook/addon-docs": "9.1.10",
+ "@storybook/addon-links": "^9.1.10",
+ "@storybook/builder-vite": "^9.1.10",
"@storybook/preset-scss": "^1.0.3",
+ "@storybook/react-vite": "^9.1.10",
+ "@testing-library/dom": "^10.4.1",
+ "@testing-library/react": "^16.3.0",
"@storybook/react": "^8.0.1",
"@testing-library/jest-dom": "^6.9.1",
- "@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
"@vitejs/plugin-react": "^4.3.4",
"@vitejs/plugin-react-swc": "^3.8.0",
- "acorn": "^7.4.1",
- "babel-jest": "^29.7.0",
- "babel-loader": "^8.2.3",
+ "acorn": "^8.15.0",
+ "babel-jest": "^30.2.0",
"babel-node": "0.0.1-security",
- "babel-plugin-inline-react-svg": "^2.0.1",
- "babel-plugin-jest-hoist": "^26.2.0",
- "babel-plugin-named-asset-import": "^0.3.8",
- "babel-plugin-prismjs": "^2.1.0",
- "babel-plugin-react-remove-properties": "^0.3.0",
- "babel-preset-react-app": "^10.0.1",
+ "babel-plugin-inline-react-svg": "^2.0.2",
+ "babel-plugin-jest-hoist": "^30.0.1",
"babel-runtime": "^6.26.0",
- "body-parser": "^1.19.0",
+ "body-parser": "^2.2.0",
"chai": "^4.3.4",
- "chromedriver": "^142.0.0",
- "cross-env": "^7.0.3",
- "css-loader": "^6.5.1",
- "cucumber-html-reporter": "^5.3.0",
- "eslint": "^9.13.0",
- "eslint-config-prettier": "^9.1.0",
+ "chromedriver": "^142.0.4",
+ "cross-env": "^10.0.0",
+ "cucumber-html-reporter": "^7.2.0",
+ "eslint": "^9.37.0",
+ "eslint-config-prettier": "^10.1.8",
"eslint-plugin-babel": "^5.3.1",
"eslint-plugin-import": "^2.32.0",
- "eslint-plugin-prettier": "^5.1.3",
- "eslint-plugin-react": "^7.37.4",
- "eslint-plugin-react-hooks": "^5.1.0",
- "eslint-plugin-react-refresh": "^0.4.14",
- "express": "^4.17.1",
- "file-loader": "^6.2.0",
- "geckodriver": "^3.0.1",
- "globals": "^15.14.0",
- "http-proxy-middleware": "^2.0.3",
+ "eslint-plugin-prettier": "^5.5.4",
+ "eslint-plugin-react": "^7.37.5",
+ "eslint-plugin-react-hooks": "^7.0.0",
+ "eslint-plugin-react-refresh": "^0.4.23",
+ "eslint-plugin-storybook": "9.1.10",
+ "express": "^5.1.0",
+ "geckodriver": "^6.0.1",
+ "globals": "^16.4.0",
"jsdom": "^27.2.0",
- "mime-types": "^2.1.35",
+ "mime-types": "^3.0.1",
"node": "^21.6.2",
- "nodemon": "^3.1.2",
+ "nodemon": "^3.1.10",
"pandas-js": "^0.2.4",
- "postcss": "^8.4.36",
- "postcss-flexbugs-fixes": "^5.0.2",
- "postcss-normalize": "^10.0.1",
- "postcss-preset-env": "^9.5.2",
- "postcss-safe-parser": "7.0.0",
- "prettier": "^3.3.3",
+ "prettier": "^3.6.2",
"randexp": "^0.5.3",
"react-app-polyfill": "^3.0.0",
"react-dev-utils": "^12.0.1",
- "sass": "^1.72.0",
- "sass-loader": "^12.3.2",
+ "sass": "^1.93.2",
"selenium-webdriver": "^4.35.0",
- "source-map-loader": "^5.0.0",
- "stylelint": "^13.3.3",
+ "stylelint": "^16.24.0",
"stylelint-config-rational-order": "^0.1.2",
- "stylelint-config-standard": "^20.0.0",
- "stylelint-order": "^4.0.0",
- "stylelint-scss": "^3.17.2",
+ "stylelint-config-standard": "^39.0.0",
+ "stylelint-config-standard-scss": "^16.0.0",
+ "stylelint-order": "^7.0.0",
+ "stylelint-scss": "^6.12.1",
"url-loader": "4.1.1",
- "vite": "^6.2.0",
+ "vite": "^6.3.6",
"vite-plugin-commonjs": "^0.10.4",
"vite-plugin-eslint": "^1.8.1",
- "vite-plugin-svgr": "^4.3.0",
+ "vite-plugin-svgr": "^4.5.0",
"vitest": "^4.0.10"
},
"babel": {
"plugins": [
- "@babel/plugin-proposal-logical-assignment-operators"
+ "@babel/plugin-transform-logical-assignment-operators"
],
"presets": [
[
@@ -168,6 +158,10 @@
"useBuiltIns": "usage",
"corejs": "^3.23.3"
}
+ ],
+ [
+ "@babel/preset-react",
+ {}
]
]
}
diff --git a/src/common/ChipInput/chipInput.scss b/src/common/ChipInput/chipInput.scss
index 564960b6b2..b467c79301 100644
--- a/src/common/ChipInput/chipInput.scss
+++ b/src/common/ChipInput/chipInput.scss
@@ -19,11 +19,11 @@
left: 0;
z-index: 5;
width: 100%;
+ max-height: 200px;
+ overflow-y: auto;
background-color: colors.$white;
border: borders.$primaryBorder;
border-radius: variables.$mainBorderRadius;
- max-height: 200px;
- overflow-y: auto;
.suggestion-row {
display: flex;
diff --git a/src/common/CodeBlock/Button.stories.js b/src/common/CodeBlock/CodeBlock.stories.jsx
similarity index 96%
rename from src/common/CodeBlock/Button.stories.js
rename to src/common/CodeBlock/CodeBlock.stories.jsx
index b976ff22e6..586dd32928 100644
--- a/src/common/CodeBlock/Button.stories.js
+++ b/src/common/CodeBlock/CodeBlock.stories.jsx
@@ -21,6 +21,8 @@ import React from 'react'
import CodeBlock from './CodeBlock'
+import 'prismjs/components/prism-json'
+
export default {
title: 'Example/CodeBlock',
component: CodeBlock
diff --git a/src/common/CodeBlock/codeBlock.scss b/src/common/CodeBlock/codeBlock.scss
index 20f43fb612..2595b8e4d3 100644
--- a/src/common/CodeBlock/codeBlock.scss
+++ b/src/common/CodeBlock/codeBlock.scss
@@ -5,10 +5,10 @@
.code-block {
width: 100%;
height: 200px;
+ padding: 18px 16px;
overflow: auto;
border: borders.$primaryBorder;
border-radius: variables.$mainBorderRadius;
- padding: 18px 16px;
&__label {
margin-bottom: 5px;
diff --git a/src/common/Combobox/combobox.scss b/src/common/Combobox/combobox.scss
index c5540b2cc9..48622ec1c2 100644
--- a/src/common/Combobox/combobox.scss
+++ b/src/common/Combobox/combobox.scss
@@ -12,8 +12,8 @@
border-radius: 4px;
&_disabled {
- cursor: not-allowed;
border-color: colors.$spunPearl;
+ cursor: not-allowed;
}
&_invalid {
@@ -22,10 +22,10 @@
&-list {
min-width: 140px;
- margin: 0;
- list-style-type: none;
max-height: 300px;
+ margin: 0;
overflow: auto;
+ list-style-type: none;
&__option {
padding: 8px 15px;
@@ -90,8 +90,8 @@
top: 100%;
z-index: 5;
display: none;
- margin-top: 5px;
max-width: 220px;
+ margin-top: 5px;
background-color: colors.$white;
box-shadow: shadows.$previewBoxShadow;
@@ -167,8 +167,8 @@
}
&:disabled {
- background-color: transparent;
color: colors.$spunPearl;
+ background-color: transparent;
}
}
diff --git a/src/common/DatePicker/DatePicker.stories.js b/src/common/DatePicker/DatePicker.stories.jsx
similarity index 100%
rename from src/common/DatePicker/DatePicker.stories.js
rename to src/common/DatePicker/DatePicker.stories.jsx
diff --git a/src/common/DatePicker/DatePickerView.jsx b/src/common/DatePicker/DatePickerView.jsx
index 1b9f143d04..663151720c 100644
--- a/src/common/DatePicker/DatePickerView.jsx
+++ b/src/common/DatePicker/DatePickerView.jsx
@@ -104,13 +104,10 @@ const DatePickerView = React.forwardRef(
isInputInvalid && 'input_invalid'
)
const inputLabelClassNames = classnames('input__label', label && 'active-label')
+ const { datePickerRef, datePickerViewRef } = ref
return (
-
+
-
+
{datePickerOptions.map(option => (
-
+
{config.map(item => (
diff --git a/src/common/ExpandableText/ExpandableText.jsx b/src/common/ExpandableText/ExpandableText.jsx
index fe38a4260f..fce738e0ae 100644
--- a/src/common/ExpandableText/ExpandableText.jsx
+++ b/src/common/ExpandableText/ExpandableText.jsx
@@ -35,7 +35,9 @@ const ExpandableText = ({
useEffect(() => {
if (forceExpand || contextForceExpand) {
- setExpanded(forceExpand || contextForceExpand)
+ queueMicrotask(() => {
+ setExpanded(forceExpand || contextForceExpand)
+ })
}
}, [contextForceExpand, forceExpand])
diff --git a/src/common/ExpandableText/expandableText.scss b/src/common/ExpandableText/expandableText.scss
index 5f5a1672ad..4b29b9fb39 100644
--- a/src/common/ExpandableText/expandableText.scss
+++ b/src/common/ExpandableText/expandableText.scss
@@ -5,47 +5,47 @@
}
.expandable-text {
- display: block;
- transition: max-height 0.3s ease;
position: relative;
+ display: block;
overflow: hidden;
+ transition: max-height 0.3s ease;
}
.fade-overlay {
- content: '';
position: absolute;
+ right: 0;
bottom: 0;
left: 0;
- right: 0;
height: 40px;
- background: linear-gradient(to bottom, rgba(255, 255, 255, 0), #fff);
+ background: linear-gradient(to bottom, rgb(255 255 255 / 0%), #fff);
+ content: '';
pointer-events: none;
}
.see-more-button-overlay {
position: absolute;
- bottom: 4px;
right: 0;
- background: white;
- padding-left: 8px;
+ bottom: 4px;
display: flex;
- align-items: center;
gap: 4px;
+ align-items: center;
+ padding-left: 8px;
+ background: white;
}
.dots {
- font-weight: bold;
color: colors.$doveGrayTwo;
+ font-weight: bold;
}
.see-more-button,
.see-less-button {
- background: none;
- border: none;
+ padding: 0;
color: colors.$cornflowerBlue;
font-size: 14px;
+ background: none;
+ border: none;
cursor: pointer;
- padding: 0;
}
.see-less-container {
diff --git a/src/common/Input/Input.stories.js b/src/common/Input/Input.stories.jsx
similarity index 92%
rename from src/common/Input/Input.stories.js
rename to src/common/Input/Input.stories.jsx
index 081e5ce056..8fdcc35de3 100644
--- a/src/common/Input/Input.stories.js
+++ b/src/common/Input/Input.stories.jsx
@@ -98,8 +98,8 @@ ChunkyMandatory.args = {
requiredText: 'Field is required'
}
-export const withValidationRules = Template.bind({})
-withValidationRules.args = {
+export const WithValidationRules = Template.bind({})
+WithValidationRules.args = {
...commonArgs,
density: 'chunky',
invalid: true,
@@ -110,8 +110,8 @@ withValidationRules.args = {
value: ' test#2!'
}
-export const withStaticLink = Template.bind({})
-withStaticLink.args = {
+export const WithStaticLink = Template.bind({})
+WithStaticLink.args = {
...commonArgs,
label: 'label with static link',
link: {
@@ -121,9 +121,9 @@ withStaticLink.args = {
value: 'test'
}
-export const withDynamicLink = Template.bind({})
+export const WithDynamicLink = Template.bind({})
const value = 'some text'
-withDynamicLink.args = {
+WithDynamicLink.args = {
...commonArgs,
label: 'label with dynamic link',
link: {
diff --git a/src/common/Input/input.scss b/src/common/Input/input.scss
index b167982abf..85380f874e 100644
--- a/src/common/Input/input.scss
+++ b/src/common/Input/input.scss
@@ -50,10 +50,10 @@
}
&__warning {
- z-index: 2;
position: absolute;
top: 50%;
right: 10px;
+ z-index: 2;
display: block;
transform: translateY(-45%);
cursor: pointer;
@@ -139,8 +139,8 @@
}
&-mandatory {
- color: colors.$amaranth;
margin-left: 2px;
+ color: colors.$amaranth;
&_disabled {
color: colors.$spunPearl;
diff --git a/src/common/NoData/noData.scss b/src/common/NoData/noData.scss
index 489c58fafc..27c8a6623e 100644
--- a/src/common/NoData/noData.scss
+++ b/src/common/NoData/noData.scss
@@ -6,6 +6,6 @@
width: 100%;
height: 100%;
min-height: 150px;
- word-break: break-word;
text-align: center;
+ word-break: break-word;
}
diff --git a/src/common/Notifications/Notification.jsx b/src/common/Notifications/Notification.jsx
index 23ce82c3c3..bbd754e08c 100644
--- a/src/common/Notifications/Notification.jsx
+++ b/src/common/Notifications/Notification.jsx
@@ -42,7 +42,7 @@ const Notification = ({ notification, timeoutMs = 10000, ...rest }) => {
const nodeRef = useRef()
const { pauseTimeout, resumeTimeout, cancelTimeout } = useTimeout(
- () => handleRemoveNotification(notification.id),
+ () => dispatch(removeNotification(notification.id)),
timeoutMs
)
diff --git a/src/common/Notifications/notifications.scss b/src/common/Notifications/notifications.scss
index 33858b5642..592ea52400 100644
--- a/src/common/Notifications/notifications.scss
+++ b/src/common/Notifications/notifications.scss
@@ -1,10 +1,10 @@
.notifications-wrapper {
position: absolute;
right: 1.5em;
- margin-left: 1.5em;
bottom: 1em;
display: flex;
flex: 1 1;
flex-direction: column;
max-height: 90vh;
+ margin-left: 1.5em;
}
diff --git a/src/common/Pagination/Pagination.jsx b/src/common/Pagination/Pagination.jsx
index 1dedd9b324..1e32ce34af 100644
--- a/src/common/Pagination/Pagination.jsx
+++ b/src/common/Pagination/Pagination.jsx
@@ -57,8 +57,6 @@ const Pagination = ({
const [searchParams, setSearchParams] = useSearchParams()
const navigate = useNavigate()
const paginationPagesRef = useRef()
- const leftSideRef = useRef(0)
- const rightSideRef = useRef(0)
// Total pages are now calculated based on start and end pages
const totalPagesCount = useMemo(
@@ -120,9 +118,6 @@ const Pagination = ({
leftSide = lastPage - 4
}
- rightSideRef.current = rightSide
- leftSideRef.current = leftSide
-
if (leftSide > firstPage + 1) {
items.push(threeDotsString)
}
diff --git a/src/common/Pagination/pagination.scss b/src/common/Pagination/pagination.scss
index 69966f57cb..dfbf2e1a40 100644
--- a/src/common/Pagination/pagination.scss
+++ b/src/common/Pagination/pagination.scss
@@ -2,11 +2,11 @@
@use 'igz-controls/scss/variables';
.pagination {
+ z-index: 1;
display: flex;
- justify-content: space-between;
align-items: center;
+ justify-content: space-between;
padding: 10px 4px 4px;
- z-index: 1;
.pagination-items-count,
.pagination-items-selector {
@@ -19,8 +19,8 @@
}
.pagination-navigation {
- flex-grow: 1;
display: flex;
+ flex-grow: 1;
justify-content: center;
}
@@ -30,18 +30,18 @@
}
.pagination-btn {
- color: colors.$primary;
- cursor: default;
- box-sizing: border-box;
display: flex;
justify-content: center;
- border: none;
- padding: 4px 5px;
+ box-sizing: border-box;
margin: 0 2px;
- font-size: 14px;
+ padding: 4px 5px;
+ color: colors.$primary;
font-weight: normal;
+ font-size: 14px;
+ border: none;
+ cursor: default;
- &:hover:not(.pagination-btn_active):not(:disabled) {
+ &:hover:not(.pagination-btn_active, :disabled) {
cursor: pointer;
}
@@ -56,10 +56,10 @@
}
.pagination-page-number {
- padding: 2px 3px;
box-sizing: content-box;
- border-bottom: 2px solid transparent;
+ padding: 2px 3px;
border-top: 2px solid transparent;
+ border-bottom: 2px solid transparent;
}
&.pagination-page-btn {
@@ -71,8 +71,8 @@
}
&.pagination-dots {
- cursor: default;
display: inline-flex;
+ cursor: default;
}
}
diff --git a/src/common/RangeInput/RangeInput.stories.js b/src/common/RangeInput/RangeInput.stories.jsx
similarity index 100%
rename from src/common/RangeInput/RangeInput.stories.js
rename to src/common/RangeInput/RangeInput.stories.jsx
diff --git a/src/common/ReactFlow/MlReactFlow.jsx b/src/common/ReactFlow/MlReactFlow.jsx
index 7bade64c25..37fe506443 100644
--- a/src/common/ReactFlow/MlReactFlow.jsx
+++ b/src/common/ReactFlow/MlReactFlow.jsx
@@ -64,7 +64,9 @@ const MlReactFlow = ({ alignTriggerItem = '', edges, nodes, onNodeClick = () =>
useEffect(() => {
if (reactFlowInstance && !initialGraphViewGenerated && nodes.length > 0) {
- setInitialGraphViewGenerated(true)
+ queueMicrotask(() => {
+ setInitialGraphViewGenerated(true)
+ })
}
}, [nodes.length, initialGraphViewGenerated, reactFlowInstance])
diff --git a/src/common/ReactFlow/mlReactFlow.scss b/src/common/ReactFlow/mlReactFlow.scss
index 1927c379a6..54b1195f2e 100644
--- a/src/common/ReactFlow/mlReactFlow.scss
+++ b/src/common/ReactFlow/mlReactFlow.scss
@@ -4,19 +4,19 @@
@mixin inputNode {
color: colors.$white;
background-color: colors.$hotPink;
- fill: colors.$hotPink;
border-color: colors.$hotPink;
- stroke: colors.$hotPink;
box-shadow: 0 3px 14px rgba(colors.$hotPink, 0.34);
+ fill: colors.$hotPink;
+ stroke: colors.$hotPink;
}
@mixin outputNode {
color: colors.$white;
background-color: colors.$malibu;
- fill: colors.$malibu;
border-color: colors.$malibu;
- stroke: colors.$malibu;
box-shadow: 0 3px 14px rgba(colors.$malibu, 0.5);
+ fill: colors.$malibu;
+ stroke: colors.$malibu;
}
@mixin primaryNode {
@@ -80,20 +80,20 @@
@mixin selectedNode {
&.selected {
border-color: colors.$brightTurquoise;
- stroke: colors.$brightTurquoise;
box-shadow: 0 4px 20px rgba(colors.$brightTurquoise, 0.5);
+ stroke: colors.$brightTurquoise;
&.status-error,
&.status-failed {
border-color: colors.$burntSienna;
- stroke: colors.$burntSienna;
box-shadow: 0 4px 20px rgba(colors.$burntSienna, 0.5);
+ stroke: colors.$burntSienna;
}
&.status-running {
border-color: colors.$pictonBlue;
- stroke: colors.$pictonBlue;
box-shadow: 0 4px 20px rgba(colors.$pictonBlue, 0.5);
+ stroke: colors.$pictonBlue;
}
}
}
@@ -123,9 +123,9 @@
.react-flow__node-label {
position: relative;
width: 100%;
+ overflow: hidden;
font-size: 1.5rem;
text-align: center;
- overflow: hidden;
}
.react-flow__node-sub-label {
diff --git a/src/common/Search/Search.jsx b/src/common/Search/Search.jsx
index e8cd1fc587..4e4e386130 100644
--- a/src/common/Search/Search.jsx
+++ b/src/common/Search/Search.jsx
@@ -17,7 +17,7 @@ illegal under applicable law, and the grant of the foregoing license
under the Apache 2.0 license is conditioned upon your compliance with
such restriction.
*/
-import React, { useState, useEffect, useCallback, useRef } from 'react'
+import React, { useState, useEffect, useCallback, useRef, useLayoutEffect } from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
@@ -45,37 +45,30 @@ const Search = ({
wrapperClassName = ''
}) => {
const [searchValue, setSearchValue] = useState(value ?? '')
- const [label, setLabel] = useState('')
+ // const [label, setLabel] = useState('')
const [inputIsFocused, setInputFocused] = useState(false)
+ const [searchWidth, setSearchWidth] = useState(0)
+ const [hasMatch, setHasMatch] = useState(false)
const searchRef = useRef()
const popUpRef = useRef()
-
- const { width: searchWidth } = searchRef?.current?.getBoundingClientRect() || {}
-
const searchClassNames = classnames('search-container', className)
- const handleSearchOnBlur = useCallback(
- event => {
- if (
- (event.type === 'click' &&
- searchRef.current &&
- !searchRef.current.contains(event.target)) ||
- (event.type === 'scroll' && popUpRef.current && !popUpRef?.current.contains(event.target))
- ) {
- setInputFocused(false)
- }
- },
- [searchRef]
- )
+ // === Обчислення ширини інпута після монтування ===
+ useLayoutEffect(() => {
+ if (searchRef.current) {
+ const { width } = searchRef.current.getBoundingClientRect()
+ setSearchWidth(width)
+ }
+ }, [])
- useEffect(() => {
- if (matches.length > 0 && searchValue.length > 0) {
- setLabel(
- matches.find(item => item.toLocaleLowerCase().includes(searchValue.toLocaleLowerCase())) ??
- ''
- )
+ const handleSearchOnBlur = useCallback(event => {
+ if (
+ (event.type === 'click' && searchRef.current && !searchRef.current.contains(event.target)) ||
+ (event.type === 'scroll' && popUpRef.current && !popUpRef.current.contains(event.target))
+ ) {
+ setInputFocused(false)
}
- }, [matches, searchValue])
+ }, [])
useEffect(() => {
window.addEventListener('click', handleSearchOnBlur)
@@ -87,46 +80,53 @@ const Search = ({
}
}, [handleSearchOnBlur])
- const searchOnChange = value => {
- if (value.length === 0 && label.length > 0) {
- setLabel('')
- }
-
- onChange(value)
+ // === Основна логіка оновлення значення ===
+ const handleSearchChange = value => {
+ const cleanValue = deleteUnsafeHtml(value)
+ setSearchValue(cleanValue)
setInputFocused(true)
- setSearchValue(deleteUnsafeHtml(value))
+ onChange(cleanValue)
+
+ const matchExists = matches.some(item =>
+ item.toLocaleLowerCase().includes(cleanValue.toLocaleLowerCase())
+ )
+ setHasMatch(matchExists)
}
- const matchOnClick = item => {
- setLabel('')
+ // === Клік на елемент зі списку ===
+ const handleMatchClick = item => {
setSearchValue(item)
- onChange(item)
+ setHasMatch(false)
setInputFocused(false)
+ onChange(item)
}
- const handleSearchIconClick = event => {
+ // === Клік по іконці пошуку ===
+ const handleIconClick = event => {
event.stopPropagation()
- if (searchValue.length > 0) {
+ if (searchValue.trim().length > 0) {
onChange(searchValue)
setInputFocused(false)
}
}
useEffect(() => {
- if (searchValue.length > 0 && value !== searchValue) {
- setSearchValue(value)
+ if (value !== searchValue) {
+ queueMicrotask(() => setSearchValue(value ?? ''))
}
}, [searchValue, value])
+ const filteredMatches = inputIsFocused
+ ? matches.filter(item => item.toLocaleLowerCase().includes(searchValue.toLocaleLowerCase()))
+ : []
+
return (
{
- setInputFocused(true)
- }}
+ onClick={() => setInputFocused(true)}
>
}
iconClass="search-icon"
- iconOnClick={handleSearchIconClick}
- onChange={searchOnChange}
+ iconOnClick={handleIconClick}
+ onChange={handleSearchChange}
onFocus={onFocus}
focused={inputIsFocused}
onKeyDown={event => {
- if (event.key === 'Enter' && !searchWhileTyping && searchValue !== '') {
+ if (event.key === 'Enter' && !searchWhileTyping && searchValue.trim() !== '') {
onChange(searchValue)
setInputFocused(false)
}
@@ -150,7 +150,8 @@ const Search = ({
value={searchValue}
withoutBorder={withoutBorder}
/>
- {matches.length > 0 && label.length > 0 && inputIsFocused && (
+
+ {filteredMatches.length > 0 && hasMatch && (
- {matches.reduce((options, item, index) => {
- if (item?.toLocaleLowerCase().includes(searchValue.toLocaleLowerCase())) {
- options.push(
- (match ? `${match}` : match)
- )
- }}
- name={item}
- key={item + index}
- onClick={() => matchOnClick(item)}
- tabIndex={index}
- />
- )
- }
-
- return options
- }, [])}
+ {filteredMatches.map((item, index) => (
+ (match ? `${match}` : match)
+ )
+ }}
+ name={item}
+ onClick={() => handleMatchClick(item)}
+ tabIndex={index}
+ />
+ ))}
)}
diff --git a/src/common/Search/search.scss b/src/common/Search/search.scss
index 377b71fd9c..6d537e1384 100644
--- a/src/common/Search/search.scss
+++ b/src/common/Search/search.scss
@@ -19,6 +19,7 @@
&-input {
z-index: 2;
width: 100%;
+
// todo: delete `!important` after replacing `Input` with `FormInput`
padding: 10px 20px 10px 35px !important;
color: colors.$topaz;
@@ -41,9 +42,9 @@
}
&-matches {
+ max-height: 250px;
margin: 0;
padding: 0;
- max-height: 250px;
&__item {
padding: 7px 15px;
diff --git a/src/common/SearchNavigator/searchNavigator.scss b/src/common/SearchNavigator/searchNavigator.scss
index 020ed961fd..b8a810408c 100644
--- a/src/common/SearchNavigator/searchNavigator.scss
+++ b/src/common/SearchNavigator/searchNavigator.scss
@@ -10,20 +10,20 @@
&__counter {
display: flex;
- font-size: 13px;
color: colors.$topaz;
+ font-size: 13px;
- &:after {
+ &::after {
display: none;
- content: '';
width: 5px;
height: 15px;
margin: 0 5px 0 8px;
border-right: borders.$primaryBorder;
+ content: '';
}
&_with-divider {
- &:after {
+ &::after {
display: block;
}
}
diff --git a/src/common/Select/Select.jsx b/src/common/Select/Select.jsx
index 6a0b9f1840..528ecbd92f 100644
--- a/src/common/Select/Select.jsx
+++ b/src/common/Select/Select.jsx
@@ -17,7 +17,7 @@ illegal under applicable law, and the grant of the foregoing license
under the Apache 2.0 license is conditioned upon your compliance with
such restriction.
*/
-import React, { useState, useEffect, useCallback, useRef } from 'react'
+import React, { useState, useEffect, useCallback, useRef, useLayoutEffect } from 'react'
import PropTypes from 'prop-types'
import classNames from 'classnames'
@@ -56,7 +56,7 @@ const Select = ({
const [isConfirmDialogOpen, setConfirmDialogOpen] = useState(false)
const [isOpen, setOpen] = useState(false)
const [searchValue, setSearchValue] = useState('')
- const { width: dropdownWidth } = selectRef?.current?.getBoundingClientRect() || {}
+ const [dropdownWidth, setDropdownWidth] = useState(0)
const selectClassName = classNames(
'select',
className,
@@ -77,6 +77,18 @@ const Select = ({
)
const selectedOption = options.find(option => option.id === selectedId)
+ const clickHandler = event => {
+ if (selectRef.current !== event.target.closest('.select')) {
+ setOpen(false)
+ }
+ }
+
+ const handleScroll = event => {
+ if (!event.target.closest('.select__body')) {
+ setOpen(false)
+ }
+ }
+
useEffect(() => {
if (isOpen) {
window.addEventListener('scroll', handleScroll, true)
@@ -90,18 +102,6 @@ const Select = ({
}
}, [isOpen])
- const clickHandler = event => {
- if (selectRef.current !== event.target.closest('.select')) {
- setOpen(false)
- }
- }
-
- const handleScroll = event => {
- if (!event.target.closest('.select__body')) {
- setOpen(false)
- }
- }
-
const toggleOpen = () => {
!disabled && setOpen(!isOpen)
}
@@ -115,6 +115,13 @@ const Select = ({
}
}, [])
+ useLayoutEffect(() => {
+ if (selectRef.current) {
+ const { width } = selectRef.current.getBoundingClientRect()
+ setDropdownWidth(width)
+ }
+ }, [selectRef])
+
const handleSelectOptionClick = (selectedOption, option) => {
if (selectedOption !== selectedId) {
option.handler && option.handler()
diff --git a/src/common/Select/Select.stories.js b/src/common/Select/Select.stories.jsx
similarity index 100%
rename from src/common/Select/Select.stories.js
rename to src/common/Select/Select.stories.jsx
diff --git a/src/common/Sort/Sort.jsx b/src/common/Sort/Sort.jsx
index bc08550d2c..22c0774304 100644
--- a/src/common/Sort/Sort.jsx
+++ b/src/common/Sort/Sort.jsx
@@ -17,7 +17,7 @@ illegal under applicable law, and the grant of the foregoing license
under the Apache 2.0 license is conditioned upon your compliance with
such restriction.
*/
-import React, { useEffect, useState } from 'react'
+import React from 'react'
import PropTypes from 'prop-types'
import classNames from 'classnames'
@@ -35,16 +35,12 @@ const Sort = ({
selectedId,
setIsDescendingOrder
}) => {
- const [selectedOption, setSelectedOption] = useState(null)
+ const selectedOption = options.find(option => option.id === selectedId)
const arrowDirectionClassName = classNames(
'sort-icon',
isDescendingOrder ? 'sort-icon_down' : 'sort-icon_up'
)
- useEffect(() => {
- setSelectedOption(options.find(option => option.id === selectedId))
- }, [options, selectedId])
-
return (
* {
- fill: currentColor;
+ fill: currentcolor;
}
}
}
diff --git a/src/common/StatsCard/statsCard.scss b/src/common/StatsCard/statsCard.scss
index 6f34f01682..2deaed945a 100644
--- a/src/common/StatsCard/statsCard.scss
+++ b/src/common/StatsCard/statsCard.scss
@@ -69,9 +69,9 @@
transition: all 0.3s ease-in-out;
@media screen and (min-width: 1600px) {
+ top: -2px;
width: 20px;
height: 20px;
- top: -2px;
}
svg {
diff --git a/src/common/TargetPath/TargetPath.jsx b/src/common/TargetPath/TargetPath.jsx
index 84971086c1..d2f1e383d7 100644
--- a/src/common/TargetPath/TargetPath.jsx
+++ b/src/common/TargetPath/TargetPath.jsx
@@ -80,21 +80,35 @@ const TargetPath = ({
}
}
+ const handlePathChange = useCallback(
+ value => {
+ if (value.length !== 0) {
+ formState.form.change(`${formStateFieldInfo}.value`, value.replace(/[^:/]*:[/]{2,3}/, ''))
+ formState.form.change(`${formStateFieldInfo}.pathType`, value.match(/^\w*:[/]{2,3}/)[0])
+
+ if (formStateDataInputState) {
+ formState.form.change(`${formStateDataInputState}`, dataInputState)
+ }
+ }
+ },
+ [dataInputState, formState.form, formStateDataInputState, formStateFieldInfo]
+ )
+
const handleGetProjectsNames = useCallback(() => {
getProjectsNames(dispatch, setDataInputState, params.projectName)
- }, [dispatch, params.projectName])
+ }, [dispatch, setDataInputState, params.projectName])
const handleGetArtifacts = useCallback(() => {
getArtifacts(dispatch, dataInputState.project, dataInputState.storePathType, setDataInputState)
- }, [dataInputState.project, dataInputState.storePathType, dispatch])
+ }, [dataInputState.project, dataInputState.storePathType, dispatch, setDataInputState])
const handleGetFeatureVectors = useCallback(() => {
getFeatureVectors(dispatch, dataInputState.project, setDataInputState)
- }, [dataInputState.project, dispatch])
+ }, [dataInputState.project, dispatch, setDataInputState])
const handleGetArtifact = useCallback(() => {
getArtifact(dispatch, dataInputState.project, dataInputState.projectItem, setDataInputState)
- }, [dataInputState.project, dataInputState.projectItem, dispatch])
+ }, [dataInputState.project, dataInputState.projectItem, dispatch, setDataInputState])
const handleGetFeatureVector = useCallback(() => {
getFeatureVector(
@@ -103,7 +117,7 @@ const TargetPath = ({
dataInputState.projectItem,
setDataInputState
)
- }, [dataInputState.project, dataInputState.projectItem, dispatch])
+ }, [dataInputState.project, dataInputState.projectItem, dispatch, setDataInputState])
useEffect(() => {
if (dataInputState.inputStorePathTypeEntered && dataInputState.projects.length === 0) {
@@ -120,24 +134,27 @@ const TargetPath = ({
if (
get(formState.values, `${formStateFieldInfo}.pathType`) === MLRUN_STORAGE_INPUT_PATH_SCHEME
) {
- setDataInputState(prev => ({
- ...prev,
- comboboxMatches: generateComboboxMatchesList(
- dataInputState.artifacts,
- dataInputState.artifactsReferences,
- dataInputState.featureVectors,
- dataInputState.featureVectorsReferences,
- dataInputState.inputProjectItemPathEntered,
- dataInputState.inputProjectItemReferencePathEntered,
- dataInputState.inputProjectPathEntered,
- dataInputState.inputStorePathTypeEntered,
- dataInputState.project,
- dataInputState.projectItem,
- dataInputState.projectItemReference,
- dataInputState.projects,
- dataInputState.storePathType
- )
- }))
+ //TODO
+ setTimeout(() => {
+ setDataInputState(prev => ({
+ ...prev,
+ comboboxMatches: generateComboboxMatchesList(
+ dataInputState.artifacts,
+ dataInputState.artifactsReferences,
+ dataInputState.featureVectors,
+ dataInputState.featureVectorsReferences,
+ dataInputState.inputProjectItemPathEntered,
+ dataInputState.inputProjectItemReferencePathEntered,
+ dataInputState.inputProjectPathEntered,
+ dataInputState.inputStorePathTypeEntered,
+ dataInputState.project,
+ dataInputState.projectItem,
+ dataInputState.projectItemReference,
+ dataInputState.projects,
+ dataInputState.storePathType
+ )
+ }))
+ }, 0)
}
}, [
dataInputState.artifacts,
@@ -215,20 +232,6 @@ const TargetPath = ({
handleGetFeatureVector
])
- const handlePathChange = useCallback(
- value => {
- if (value.length !== 0) {
- formState.form.change(`${formStateFieldInfo}.value`, value.replace(/[^:/]*:[/]{2,3}/, ''))
- formState.form.change(`${formStateFieldInfo}.pathType`, value.match(/^\w*:[/]{2,3}/)[0])
-
- if (formStateDataInputState) {
- formState.form.change(`${formStateDataInputState}`, dataInputState)
- }
- }
- },
- [dataInputState, formState.form, formStateDataInputState, formStateFieldInfo]
- )
-
return (
<>
{
- const initialValues = {
+ return {
[AUTO_REFRESH_ID]: autoRefreshIsEnabled,
[INTERNAL_AUTO_REFRESH_ID]: internalAutoRefreshIsEnabled,
...formFiltersInitialValues
}
-
- return initialValues
}, [autoRefreshIsEnabled, formFiltersInitialValues, internalAutoRefreshIsEnabled])
- const formRef = React.useRef(
- createForm({
+ const [form] = useState(() => {
+ return createForm({
initialValues: formInitialValues,
mutators: { ...arrayMutators, setFieldState },
onSubmit: () => {}
})
- )
+ })
const filterMenuModalInitialState = useMemo(() => {
return mapValues(
@@ -187,7 +184,8 @@ const ActionBar = ({
const applyFilters = useCallback(
async (formValues, filters, actionCanBePerformedChecked) => {
- const actionCanBePerformed = actionCanBePerformedChecked || await performDetailsActionHelper(changes, dispatch, true)
+ const actionCanBePerformed =
+ actionCanBePerformedChecked || (await performDetailsActionHelper(changes, dispatch, true))
const newFilters = { ...filters, ...formValues }
if (actionCanBePerformed) {
@@ -277,14 +275,14 @@ const ActionBar = ({
}
useEffect(() => {
- if (!isEqual(formRef.current?.getState().values, filterMenu)) {
- formRef.current?.batch(() => {
+ if (!isEqual(form.getState().values, filterMenu)) {
+ form.batch(() => {
for (const filterName in filterMenu) {
- formRef.current?.change(filterName, filterMenu[filterName])
+ form.change(filterName, filterMenu[filterName])
}
})
}
- }, [filterMenu, filtersConfig])
+ }, [filterMenu, filtersConfig, form])
useEffect(() => {
if (
@@ -294,7 +292,7 @@ const ActionBar = ({
) {
const intervalId = setInterval(() => {
if (!autoRefreshIsStopped) {
- refresh(formRef.current.getState())
+ refresh(form.getState())
}
}, 30000)
@@ -306,19 +304,24 @@ const ActionBar = ({
refresh,
withInternalAutoRefresh,
filtersStore.internalAutoRefresh,
- filtersStore.autoRefresh
+ filtersStore.autoRefresh,
+ form
])
useEffect(() => {
if (autoRefreshStopTrigger && filtersStore.internalAutoRefresh) {
- formRef.current?.change(INTERNAL_AUTO_REFRESH_ID, false)
- setInternalAutoRefreshPrevValue(true)
+ form.change(INTERNAL_AUTO_REFRESH_ID, false)
+ queueMicrotask(() => {
+ setInternalAutoRefreshPrevValue(true)
+ })
dispatch(toggleInternalAutoRefresh(false))
handleAutoRefreshPrevValueChange && handleAutoRefreshPrevValueChange(true)
} else if (!autoRefreshStopTrigger && internalAutoRefreshPrevValue) {
- setInternalAutoRefreshPrevValue(false)
+ queueMicrotask(() => {
+ setInternalAutoRefreshPrevValue(false)
+ })
dispatch(toggleInternalAutoRefresh(true))
- formRef.current?.change(INTERNAL_AUTO_REFRESH_ID, true)
+ form.change(INTERNAL_AUTO_REFRESH_ID, true)
handleAutoRefreshPrevValueChange && handleAutoRefreshPrevValueChange(false)
}
}, [
@@ -326,7 +329,8 @@ const ActionBar = ({
autoRefreshStopTrigger,
handleAutoRefreshPrevValueChange,
dispatch,
- filtersStore.internalAutoRefresh
+ filtersStore.internalAutoRefresh,
+ form
])
useEffect(() => {
@@ -336,21 +340,21 @@ const ActionBar = ({
}, [])
useLayoutEffect(() => {
- const prevValues = formRef.current.getState().values
+ const prevValues = form.getState().values
const valuesToReset = {
[INTERNAL_AUTO_REFRESH_ID]: prevValues[INTERNAL_AUTO_REFRESH_ID],
[AUTO_REFRESH_ID]: prevValues[AUTO_REFRESH_ID],
...formFiltersInitialValues
}
- formRef.current.reset(valuesToReset)
- }, [formFiltersInitialValues])
+ form.reset(valuesToReset)
+ }, [formFiltersInitialValues, form])
useLayoutEffect(() => {
- formRef.current?.batch(() => {
- formRef.current?.change(AUTO_REFRESH_ID, autoRefreshIsEnabled)
- formRef.current?.change(INTERNAL_AUTO_REFRESH_ID, internalAutoRefreshIsEnabled)
+ form.batch(() => {
+ form.change(AUTO_REFRESH_ID, autoRefreshIsEnabled)
+ form.change(INTERNAL_AUTO_REFRESH_ID, internalAutoRefreshIsEnabled)
})
- }, [autoRefreshIsEnabled, internalAutoRefreshIsEnabled])
+ }, [autoRefreshIsEnabled, form, internalAutoRefreshIsEnabled])
useEffect(() => {
dispatch(toggleAutoRefresh(false))
@@ -358,7 +362,7 @@ const ActionBar = ({
}, [dispatch, params.projectName])
return (
-