diff --git a/package-lock.json b/package-lock.json index f44d17452..8c8dbdd2e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "express": "^4.17.1", "graphql": "^15.5.2", "isnumber": "^1.0.0", + "keycloak-js": "^25.0.1", "qs": "^6.10.1", "react-apexcharts": "^1.4.0", "react-collapse": "^5.1.0", @@ -21694,6 +21695,11 @@ "@sideway/pinpoint": "^2.0.0" } }, + "node_modules/js-sha256": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.11.0.tgz", + "integrity": "sha512-6xNlKayMZvds9h1Y1VWc0fQHQ82BxTXizWPEtEeGvmOUYpBRy4gbWroHLpzowe6xiQhHpelCQiE7HEdznyBL9Q==" + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -22305,6 +22311,14 @@ "integrity": "sha512-Q8206k8pTY7krW32cdmPsP+DqqLgWx/hYPSj9/+7SYqSqz7UuwPbfSe07lQtvuuaVyiSJveXk0E5RydOuWwsEg==", "dev": true }, + "node_modules/jwt-decode": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", + "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", + "engines": { + "node": ">=18" + } + }, "node_modules/keycharm": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/keycharm/-/keycharm-0.4.0.tgz", @@ -22312,6 +22326,15 @@ "dev": true, "peer": true }, + "node_modules/keycloak-js": { + "version": "25.0.1", + "resolved": "https://registry.npmjs.org/keycloak-js/-/keycloak-js-25.0.1.tgz", + "integrity": "sha512-ns5sKQ5Iz3UyVGIKq2XBBXNZTlTCg9bzybR+JB2Vn+fDHIo9EGgAY4kzrBWMwbeFuegY+qJwGs05N+W9jgY9tg==", + "dependencies": { + "js-sha256": "^0.11.0", + "jwt-decode": "^4.0.0" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", diff --git a/package.json b/package.json index 5b8a878c8..9e03f3d93 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "express": "^4.17.1", "graphql": "^15.5.2", "isnumber": "^1.0.0", + "keycloak-js": "^25.0.1", "qs": "^6.10.1", "react-apexcharts": "^1.4.0", "react-collapse": "^5.1.0", diff --git a/src/Routes/Base/Header/HelpBar.react.js b/src/Routes/Base/Header/HelpBar.react.js index 7c24de7c0..322d8598f 100644 --- a/src/Routes/Base/Header/HelpBar.react.js +++ b/src/Routes/Base/Header/HelpBar.react.js @@ -6,6 +6,7 @@ import { FlexBox, Icons } from 'components/common'; import styled from 'styled-components'; import Settings from './Settings/Settings.react'; import InactiveModeTag from './InactiveMode'; +import IconUser from './IconUser'; import ExperimentPicker from './ExperimentPicker.react'; const Container = styled(FlexBox.Auto)` @@ -17,7 +18,7 @@ const HelpBar = () => ( - + } placement="bottomRight" trigger="click"> } /> diff --git a/src/Routes/Base/Header/IconUser.js b/src/Routes/Base/Header/IconUser.js new file mode 100644 index 000000000..8942e8465 --- /dev/null +++ b/src/Routes/Base/Header/IconUser.js @@ -0,0 +1,33 @@ +import React from 'react'; +import { Avatar, Popover, Tooltip } from 'antd'; +import styled from 'styled-components'; +import MenuUser from '../Login/MenuUser.react'; + +const CircleInitialWrapper = styled.div``; + +const StyledAvatar = styled(Avatar)` + background-color: #007bff; + color: white; + cursor: pointer; +`; +// eslint-disable-next-line react/prop-types +const IconUser = ({ name }) => { + const getInitial = nameStr => { + if (!nameStr) return ''; + return nameStr.toString().charAt(0).toUpperCase(); + }; + + const initial = getInitial(name); + + return ( + + } placement="bottomRight" trigger="click"> + + {initial}{' '} + + + + ); +}; + +export default IconUser; diff --git a/src/Routes/Base/Login/FormLogin.js b/src/Routes/Base/Login/FormLogin.js new file mode 100644 index 000000000..0d020f985 --- /dev/null +++ b/src/Routes/Base/Login/FormLogin.js @@ -0,0 +1,129 @@ +import React, { useState } from 'react'; +import { Button, Form, Input, Typography, Checkbox } from 'antd'; +import { FlexBox } from 'components/common'; +import KeycloakServices from '../../../keycloak'; + +const { Title } = Typography; + +const FormLogin = () => { + const { initKeycloak } = KeycloakServices; + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + + const onFinishFailed = errorInfo => { + // eslint-disable-next-line no-console + console.log('Failed:', errorInfo); + }; + + const onFinish = () => { + // event.preventDefault(); + /* keycloak + .init({ onLoad: 'login-required' }) + .then(authenticated => { + // console.log(authenticated); + if (authenticated) { + keycloak + .login({ + username, + password, + grantType: 'password', + clientId: keycloak.clientId, + }) + .catch(() => { + // console.error('Failed to login:', error); + }); + } else { + // console.log('User not authenticated'); + } + }) + .catch(error => { + console.error('Failed to initialize Keycloak:', error); + }); */ + + initKeycloak(<>login>, <>error>, username, password); + }; + + return ( + + + + Welcome to Hkube + + + please enter login details below. + + + + + + setUsername(e.target.value)} /> + + + + setPassword(e.target.value)} + /> + + + + Remember me + + + + + Login + + + + + ); +}; +export default FormLogin; diff --git a/src/Routes/Base/Login/LoginPage.js b/src/Routes/Base/Login/LoginPage.js new file mode 100644 index 000000000..573681f05 --- /dev/null +++ b/src/Routes/Base/Login/LoginPage.js @@ -0,0 +1,65 @@ +import React from 'react'; +import styled from 'styled-components'; +import FormLogin from './FormLogin'; +import bgImage from '../../../images/bgLogin.png'; + +const BackgroundWrapper = styled.div` + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + height: 100vh; + background-image: url(${bgImage}); + background-size: contain; + background-position: center; + position: relative; + overflow: hidden; + + &::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: inherit; + filter: blur(3px); + z-index: 1; + } +`; + +const LoginBox = styled.div` + width: 20%; + padding: 20px; + background: rgba(255, 255, 255, 1); + border-radius: 10px; + box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.3); + z-index: 2; + position: relative; + display: flex; + margin-top: -240px; +`; +/* +const TitleLogoBox = styled.div` + z-index: 3; + position: relative; + display: flex; + align-items: flex-start; + width: 22%; + justify-content: space-evenly; + height: 58px; +`; +*/ +const LoginPage = () => ( + + {/* + + + */} + + + + +); + +export default LoginPage; diff --git a/src/Routes/Base/Login/MenuUser.react.js b/src/Routes/Base/Login/MenuUser.react.js new file mode 100644 index 000000000..cabe88cf4 --- /dev/null +++ b/src/Routes/Base/Login/MenuUser.react.js @@ -0,0 +1,30 @@ +import React, { useCallback } from 'react'; +import { FlexBox, Icons } from 'components/common'; +import { useNavigate } from 'react-router-dom'; +import { LogoutOutlined } from '@ant-design/icons'; +import styled from 'styled-components'; +import { Typography } from 'antd'; + +const { Text } = Typography; +const MenuUser = () => { + const navigate = useNavigate(); + + const onLogoutClick = useCallback(() => { + navigate('/login'); + }, [navigate]); + + const TextLink = styled(Text)` + cursor: pointer; + `; + + return ( + + + } onClick={onLogoutClick} /> + + Logout + + + ); +}; +export default MenuUser; diff --git a/src/Routes/Tables/Algorithms/index.js b/src/Routes/Tables/Algorithms/index.js index c4016a3ac..8eddac2ef 100644 --- a/src/Routes/Tables/Algorithms/index.js +++ b/src/Routes/Tables/Algorithms/index.js @@ -55,9 +55,11 @@ const AlgorithmsTable = () => { */ const onSubmitFilter = () => {}; - if (query.loading && query.data?.algorithms?.list?.length === 0) + if ( + (query.loading && query.data?.algorithms?.list?.length === 0) || + query.error + ) return ; - if (query.error) return `Error! ${query.error.message}`; const getList = queryVal => { const filterValue = instanceFilter.algorithms.qAlgorithmName; diff --git a/src/Routes/Tables/Pipelines/index.js b/src/Routes/Tables/Pipelines/index.js index 96e82e6ee..e6eea4150 100644 --- a/src/Routes/Tables/Pipelines/index.js +++ b/src/Routes/Tables/Pipelines/index.js @@ -53,8 +53,8 @@ const PipelinesTable = () => { onSubmitFilter(instanceFilter.pipelines); }, [query.data?.pipelines?.pipelinesCount]); - if (query.loading && pipelineList.length === 0) return ; - if (query.error) return `Error! ${query.error.message}`; + if ((query.loading && pipelineList.length === 0) || query.error) + return ; return ( <> @@ -77,6 +77,9 @@ const PipelinesTable = () => { scroll={{ y: '80vh', }} + locale={{ + emptyText: , + }} /> diff --git a/src/Routes/index.js b/src/Routes/index.js index a63479acc..9cb43652a 100644 --- a/src/Routes/index.js +++ b/src/Routes/index.js @@ -16,6 +16,9 @@ import { Drawer as SiderBarRightDrawer } from './SidebarRight'; import SidebarLeft from './Base/SidebarLeft'; import UserGuide from './Base/UserGuide'; import LoadingScreen from './Base/LoadingScreen'; + +import LoginPage from './Base/Login/LoginPage'; + import Tables from './Tables'; const LayoutFullHeight = styled(Layout)` @@ -59,6 +62,8 @@ const RoutesNav = () => { }, []); const [isDataAvailable, setIsDataAvailable] = useState(false); + const [isLogin] = useState(false); + const { apolloClient, openNotification, @@ -90,7 +95,9 @@ const RoutesNav = () => { setIsNotificationErrorShow, ]); - return isDataAvailable ? ( + return !isLogin ? ( + + ) : isDataAvailable ? ( diff --git a/src/images/bgLogin.png b/src/images/bgLogin.png new file mode 100644 index 000000000..bdb79a676 Binary files /dev/null and b/src/images/bgLogin.png differ diff --git a/src/keycloak.js b/src/keycloak.js new file mode 100644 index 000000000..6436f7d26 --- /dev/null +++ b/src/keycloak.js @@ -0,0 +1,84 @@ +// src/keycloak.js +/* import Keycloak from 'keycloak-js'; + +const keycloak = new Keycloak({ + url: 'https://dev.hkube.org/hkube/keycloak', + realm: 'master', + clientId: 'api-server', + resource: 'master', + enableCors: true, + client_secret: '5mAAqUXMsFWAGCnhvhrGPVVYuZLWy7Am', +}); +// clientUId +export default keycloak; */ + +import Keycloak from 'keycloak-js'; + +const KeycloakConfig = { + clientId: 'api-server', // config.general('keycloak,authClientId', ','), + realm: 'browser', // config.general('keycloak,authRealm', ','), + url: 'https://cicd.hkube.org/hkube/keycloak/auth', // config.general('keycloak,authUrl', ','), + resource: 'browser', // config.general('keycloak,authResource', ','), + // enableCors: false, // config.general('keycloak,authEnableCORS', ','), + // clientUId: '5mAAqUXMsFWAGCnhvhrGPVVYuZLWy7Am', // config.general('keycloak,authClientUID', ',') + // client_secret: '5mAAqUXMsFWAGCnhvhrGPVVYuZLWy7Am', +}; + +const _kc = new Keycloak(KeycloakConfig); + +/** + * Initializes Keycloak instance and calls the provided callback function if successfully authenticated. + * + * @param onAuthenticatedCallback + */ +const initKeycloak = (appToRender, renderError, username, password) => { + _kc + .init({ + onLoad: 'login-required', + username, + password, + }) + .then(authenticated => { + if (!authenticated) { + // eslint-disable-next-line no-console + console.log('user is not authenticated..!'); + } + + appToRender(); + }) + .catch(authenticatedError => { + console.error(authenticatedError); + + // return renderError(authenticatedError); + }); +}; + +const doLogin = _kc.login; +const doLogout = _kc.logout; + +const getToken = () => _kc.token; + +const isLoggedIn = () => !!_kc.token; + +const updateToken = async (minSecValidity, successCallback) => + _kc.updateToken(minSecValidity).then(successCallback).catch(doLogin); + +const getUsername = () => _kc.tokenParsed?.preferred_username; + +const hasRole = roles => roles.some(role => _kc.hasRealmRole(role)); + +const isTokenExpired = minSecValidity => _kc.isTokenExpired(minSecValidity); + +const KeycloakServices = { + initKeycloak, + doLogin, + doLogout, + isLoggedIn, + getToken, + updateToken, + isTokenExpired, + getUsername, + hasRole, +}; + +export default KeycloakServices;