From 79d3ca9a15ab9cc2227b55ecfa0f3ed9d6ae410a Mon Sep 17 00:00:00 2001 From: Renat Kalimulin <103274228+Nilumilak@users.noreply.github.com> Date: Sat, 28 Dec 2024 07:19:29 +0300 Subject: [PATCH] FE: Impl custom auth page (#402) Co-authored-by: Roman Zabaluev --- .../main/resources/swagger/kafbat-ui-api.yaml | 3 + frontend/public/serviceImage.png | Bin 0 -> 7361 bytes frontend/src/components/App.tsx | 94 ++++++++-------- .../components/AuthPage/AuthPage.styled.tsx | 14 +++ frontend/src/components/AuthPage/AuthPage.tsx | 21 ++++ .../AuthPage/Header/Header.styled.tsx | 33 ++++++ .../src/components/AuthPage/Header/Header.tsx | 81 ++++++++++++++ .../components/AuthPage/Header/HeaderLogo.tsx | 29 +++++ .../SignIn/BasicSignIn/BasicSignIn.styled.tsx | 56 ++++++++++ .../SignIn/BasicSignIn/BasicSignIn.tsx | 101 ++++++++++++++++++ .../OAuthSignIn/AuthCard/AuthCard.styled.tsx | 66 ++++++++++++ .../SignIn/OAuthSignIn/AuthCard/AuthCard.tsx | 41 +++++++ .../SignIn/OAuthSignIn/OAuthSignIn.styled.tsx | 25 +++++ .../SignIn/OAuthSignIn/OAuthSignIn.tsx | 55 ++++++++++ .../AuthPage/SignIn/SignIn.styled.tsx | 19 ++++ .../src/components/AuthPage/SignIn/SignIn.tsx | 27 +++++ .../components/NavBar/UserInfo/UserInfo.tsx | 2 +- .../src/components/common/Button/Button.tsx | 3 +- .../src/components/common/Icons/AlertIcon.tsx | 22 ++++ .../components/common/Icons/CognitoIcon.tsx | 49 +++++++++ .../components/common/Icons/GoogleIcon.tsx | 32 ++++++ .../components/common/Icons/KeycloakIcon.tsx | 21 ++++ .../src/components/common/Icons/OktaIcon.tsx | 20 ++++ .../components/common/Icons/ServiceImage.tsx | 11 ++ .../contexts/GlobalSettingsContext.tsx | 28 +++-- frontend/src/lib/api.ts | 2 + frontend/src/lib/hooks/api/appConfig.ts | 47 ++++++-- frontend/src/theme/theme.ts | 72 +++++++++++++ frontend/vite.config.ts | 16 +++ 29 files changed, 929 insertions(+), 61 deletions(-) create mode 100644 frontend/public/serviceImage.png create mode 100644 frontend/src/components/AuthPage/AuthPage.styled.tsx create mode 100644 frontend/src/components/AuthPage/AuthPage.tsx create mode 100644 frontend/src/components/AuthPage/Header/Header.styled.tsx create mode 100644 frontend/src/components/AuthPage/Header/Header.tsx create mode 100644 frontend/src/components/AuthPage/Header/HeaderLogo.tsx create mode 100644 frontend/src/components/AuthPage/SignIn/BasicSignIn/BasicSignIn.styled.tsx create mode 100644 frontend/src/components/AuthPage/SignIn/BasicSignIn/BasicSignIn.tsx create mode 100644 frontend/src/components/AuthPage/SignIn/OAuthSignIn/AuthCard/AuthCard.styled.tsx create mode 100644 frontend/src/components/AuthPage/SignIn/OAuthSignIn/AuthCard/AuthCard.tsx create mode 100644 frontend/src/components/AuthPage/SignIn/OAuthSignIn/OAuthSignIn.styled.tsx create mode 100644 frontend/src/components/AuthPage/SignIn/OAuthSignIn/OAuthSignIn.tsx create mode 100644 frontend/src/components/AuthPage/SignIn/SignIn.styled.tsx create mode 100644 frontend/src/components/AuthPage/SignIn/SignIn.tsx create mode 100644 frontend/src/components/common/Icons/AlertIcon.tsx create mode 100644 frontend/src/components/common/Icons/CognitoIcon.tsx create mode 100644 frontend/src/components/common/Icons/GoogleIcon.tsx create mode 100644 frontend/src/components/common/Icons/KeycloakIcon.tsx create mode 100644 frontend/src/components/common/Icons/OktaIcon.tsx create mode 100644 frontend/src/components/common/Icons/ServiceImage.tsx diff --git a/contract/src/main/resources/swagger/kafbat-ui-api.yaml b/contract/src/main/resources/swagger/kafbat-ui-api.yaml index dff80b4ce..97d0e5b11 100644 --- a/contract/src/main/resources/swagger/kafbat-ui-api.yaml +++ b/contract/src/main/resources/swagger/kafbat-ui-api.yaml @@ -2256,7 +2256,10 @@ paths: /login: post: + tags: + - Unmapped summary: Authenticate + operationId: authenticate requestBody: required: true content: diff --git a/frontend/public/serviceImage.png b/frontend/public/serviceImage.png new file mode 100644 index 0000000000000000000000000000000000000000..8006b13f5c09b4cee6289790a38df545177b25dc GIT binary patch literal 7361 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 literal 0 HcmV?d00001 diff --git a/frontend/src/components/App.tsx b/frontend/src/components/App.tsx index 16dd1305d..d15cb95a0 100644 --- a/frontend/src/components/App.tsx +++ b/frontend/src/components/App.tsx @@ -1,5 +1,5 @@ import React, { Suspense, useContext } from 'react'; -import { Routes, Route, Navigate } from 'react-router-dom'; +import { Routes, Route, Navigate, useMatch } from 'react-router-dom'; import { accessErrorPage, clusterPath, @@ -24,6 +24,7 @@ import { GlobalSettingsProvider } from './contexts/GlobalSettingsContext'; import { UserInfoRolesAccessProvider } from './contexts/UserInfoRolesAccessContext'; import PageContainer from './PageContainer/PageContainer'; +const AuthPage = React.lazy(() => import('components/AuthPage/AuthPage')); const Dashboard = React.lazy(() => import('components/Dashboard/Dashboard')); const ClusterPage = React.lazy( () => import('components/ClusterPage/ClusterPage') @@ -49,54 +50,59 @@ const queryClient = new QueryClient({ }); const App: React.FC = () => { const { isDarkMode } = useContext(ThemeModeContext); + const isAuthRoute = useMatch('/login'); return ( - - - }> - - - - - - - {['/', '/ui', '/ui/clusters'].map((path) => ( + + {isAuthRoute ? ( + + ) : ( + + }> + + + + + + + {['/', '/ui', '/ui/clusters'].map((path) => ( + } + /> + ))} } + path={getNonExactPath(clusterNewConfigPath)} + element={} /> - ))} - } - /> - } - /> - - } - /> - } /> - } - /> - - - - - - - - - - + } + /> + + } + /> + } /> + } + /> + + + + + + + + + + )} + ); diff --git a/frontend/src/components/AuthPage/AuthPage.styled.tsx b/frontend/src/components/AuthPage/AuthPage.styled.tsx new file mode 100644 index 000000000..16f86f714 --- /dev/null +++ b/frontend/src/components/AuthPage/AuthPage.styled.tsx @@ -0,0 +1,14 @@ +import styled, { css } from 'styled-components'; + +export const AuthPageStyled = styled.div( + ({ theme }) => css` + display: flex; + flex-direction: column; + align-items: center; + justify-content: space-between; + min-height: 100vh; + background-color: ${theme.auth_page.backgroundColor}; + font-family: ${theme.auth_page.fontFamily}; + overflow-x: hidden; + ` +); diff --git a/frontend/src/components/AuthPage/AuthPage.tsx b/frontend/src/components/AuthPage/AuthPage.tsx new file mode 100644 index 000000000..ceae3069a --- /dev/null +++ b/frontend/src/components/AuthPage/AuthPage.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { useAuthSettings } from 'lib/hooks/api/appConfig'; + +import Header from './Header/Header'; +import SignIn from './SignIn/SignIn'; +import * as S from './AuthPage.styled'; + +function AuthPage() { + const { data } = useAuthSettings(); + + return ( + +
+ {data && ( + + )} + + ); +} + +export default AuthPage; diff --git a/frontend/src/components/AuthPage/Header/Header.styled.tsx b/frontend/src/components/AuthPage/Header/Header.styled.tsx new file mode 100644 index 000000000..4ba86f2bc --- /dev/null +++ b/frontend/src/components/AuthPage/Header/Header.styled.tsx @@ -0,0 +1,33 @@ +import styled, { css } from 'styled-components'; + +export const HeaderStyled = styled.div` + display: grid; + grid-template-columns: repeat(47, 41.11px); + grid-template-rows: repeat(4, 41.11px); + justify-content: center; + margin-bottom: 13.5px; +`; + +export const HeaderCell = styled.div<{ $sections?: number }>( + ({ theme, $sections }) => css` + border: 1.23px solid ${theme.auth_page.header.cellBorderColor}; + border-radius: 75.98px; + ${$sections && `grid-column: span ${$sections};`} + ` +); + +export const StyledSVG = styled.svg` + grid-column: span 3; +`; + +export const StyledRect = styled.rect( + ({ theme }) => css` + fill: ${theme.auth_page.header.LogoBgColor}; + ` +); + +export const StyledPath = styled.path( + ({ theme }) => css` + fill: ${theme.auth_page.header.LogoTextColor}; + ` +); diff --git a/frontend/src/components/AuthPage/Header/Header.tsx b/frontend/src/components/AuthPage/Header/Header.tsx new file mode 100644 index 000000000..16980af29 --- /dev/null +++ b/frontend/src/components/AuthPage/Header/Header.tsx @@ -0,0 +1,81 @@ +import React from 'react'; + +import * as S from './Header.styled'; +import HeaderLogo from './HeaderLogo'; + +function Header() { + return ( + + + {Array(2).fill()} + + {Array(2).fill()} + + {Array(2).fill()} + {Array(4).fill()} + {Array(2).fill()} + + {Array(2).fill()} + + {Array(3).fill()} + + {Array(2).fill()} + {Array(2).fill()} + {Array(2).fill()} + + + {Array(3).fill()} + {Array(8).fill()} + + {Array(2).fill()} + + {Array(3).fill()} + + {Array(6).fill()} + {Array(3).fill()} + + + {Array(2).fill()} + + + + + + + + {Array(2).fill()} + + + + {Array(3).fill()} + + + {Array(3).fill()} + + {Array(3).fill()} + {Array(3).fill()} + + + + + + {Array(2).fill()} + + {Array(2).fill()} + {Array(5).fill()} + {Array(2).fill()} + + + + {Array(5).fill()} + {Array(2).fill()} + + {Array(2).fill()} + + + + + ); +} + +export default Header; diff --git a/frontend/src/components/AuthPage/Header/HeaderLogo.tsx b/frontend/src/components/AuthPage/Header/HeaderLogo.tsx new file mode 100644 index 000000000..e5d9ca12d --- /dev/null +++ b/frontend/src/components/AuthPage/Header/HeaderLogo.tsx @@ -0,0 +1,29 @@ +import React from 'react'; + +import * as S from './Header.styled'; + +const HeaderLogo = () => ( + + + + + + + + + + +); + +export default HeaderLogo; diff --git a/frontend/src/components/AuthPage/SignIn/BasicSignIn/BasicSignIn.styled.tsx b/frontend/src/components/AuthPage/SignIn/BasicSignIn/BasicSignIn.styled.tsx new file mode 100644 index 000000000..da1388b0a --- /dev/null +++ b/frontend/src/components/AuthPage/SignIn/BasicSignIn/BasicSignIn.styled.tsx @@ -0,0 +1,56 @@ +import styled from 'styled-components'; + +export const Fieldset = styled.fieldset` + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 16px; + border: none; + width: 100%; +`; + +export const Form = styled.form` + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 40px; + width: 100%; + + ${Fieldset} div { + width: 100%; + } +`; + +export const Field = styled.div` + ${({ theme }) => theme.auth_page.signIn.label}; + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: flex-start; + gap: 4px; +`; + +export const Label = styled.label` + font-size: 12px; + font-weight: 500; + line-height: 16px; +`; + +export const ErrorMessage = styled.div` + display: flex; + column-gap: 2px; + align-items: center; + justify-content: center; + font-weight: 400; + font-size: 14px; + line-height: 20px; +`; + +export const ErrorMessageText = styled.span` + ${({ theme }) => theme.auth_page.signIn.errorMessage}; + font-weight: 400; + font-size: 14px; + line-height: 20px; +`; diff --git a/frontend/src/components/AuthPage/SignIn/BasicSignIn/BasicSignIn.tsx b/frontend/src/components/AuthPage/SignIn/BasicSignIn/BasicSignIn.tsx new file mode 100644 index 000000000..044f4781b --- /dev/null +++ b/frontend/src/components/AuthPage/SignIn/BasicSignIn/BasicSignIn.tsx @@ -0,0 +1,101 @@ +import React from 'react'; +import { Button } from 'components/common/Button/Button'; +import Input from 'components/common/Input/Input'; +import { Controller, FormProvider, useForm } from 'react-hook-form'; +import { useAuthenticate } from 'lib/hooks/api/appConfig'; +import AlertIcon from 'components/common/Icons/AlertIcon'; +import { useNavigate } from 'react-router-dom'; +import { useQueryClient } from '@tanstack/react-query'; + +import * as S from './BasicSignIn.styled'; + +interface FormValues { + username: string; + password: string; +} + +function BasicSignIn() { + const methods = useForm({ + defaultValues: { username: '', password: '' }, + }); + const navigate = useNavigate(); + const { mutateAsync, isLoading } = useAuthenticate(); + const client = useQueryClient(); + + const onSubmit = async (data: FormValues) => { + await mutateAsync(data, { + onSuccess: async (response) => { + if (response.raw.url.includes('error')) { + methods.setError('root', { message: 'error' }); + } else { + await client.invalidateQueries({ queryKey: ['app', 'info'] }); + navigate('/'); + } + }, + }); + }; + + return ( + + + + {methods.formState.errors.root && ( + + + + Username or password entered incorrectly + + + )} + ( + + Username + + + )} + /> + ( + + Password + + + )} + /> + + + + + ); +} + +export default BasicSignIn; diff --git a/frontend/src/components/AuthPage/SignIn/OAuthSignIn/AuthCard/AuthCard.styled.tsx b/frontend/src/components/AuthPage/SignIn/OAuthSignIn/AuthCard/AuthCard.styled.tsx new file mode 100644 index 000000000..d1eae050f --- /dev/null +++ b/frontend/src/components/AuthPage/SignIn/OAuthSignIn/AuthCard/AuthCard.styled.tsx @@ -0,0 +1,66 @@ +import styled, { css } from 'styled-components'; +import GitHubIcon from 'components/common/Icons/GitHubIcon'; +import { Button } from 'components/common/Button/Button'; + +export const AuthCardStyled = styled.div( + ({ theme }) => css` + display: flex; + flex-direction: column; + gap: 16px; + padding: 16px; + width: 400px; + border: 1px solid black; + border: 1px solid ${theme.auth_page.signIn.authCard.borderColor}; + border-radius: ${theme.auth_page.signIn.authCard.borderRadius}; + background-color: ${theme.auth_page.signIn.authCard.backgroundColor}; + ` +); + +export const ServiceData = styled.div( + ({ theme }) => css` + display: flex; + gap: 8px; + align-items: center; + + svg, + img { + margin: 8px; + width: 48px; + height: 48px; + } + + ${GitHubIcon} { + fill: ${theme.auth_page.icons.githubColor}; + } + ` +); + +export const ServiceDataTextContainer = styled.div` + display: flex; + flex-direction: column; +`; + +export const ServiceNameStyled = styled.span( + ({ theme }) => css` + color: ${theme.auth_page.signIn.authCard.serviceNamecolor}; + font-size: 16px; + font-weight: 500; + line-height: 24px; + ` +); + +export const ServiceTextStyled = styled.span( + ({ theme }) => css` + color: ${theme.auth_page.signIn.authCard.serviceTextColor}; + font-size: 12px; + font-weight: 500; + line-height: 16px; + ` +); + +export const ServiceButton = styled(Button)` + width: 100%; + border-radius: 8px; + font-size: 14px; + text-decoration: none; +`; diff --git a/frontend/src/components/AuthPage/SignIn/OAuthSignIn/AuthCard/AuthCard.tsx b/frontend/src/components/AuthPage/SignIn/OAuthSignIn/AuthCard/AuthCard.tsx new file mode 100644 index 000000000..b9a09812b --- /dev/null +++ b/frontend/src/components/AuthPage/SignIn/OAuthSignIn/AuthCard/AuthCard.tsx @@ -0,0 +1,41 @@ +import React, { ElementType, useState } from 'react'; +import ServiceImage from 'components/common/Icons/ServiceImage'; + +import * as S from './AuthCard.styled'; + +interface Props { + serviceName: string; + authPath: string | undefined; + Icon?: ElementType; +} + +function AuthCard({ serviceName, authPath, Icon = ServiceImage }: Props) { + const [isLoading, setIsLoading] = useState(false); + + return ( + + + + + {serviceName} + + Use an account issued by the organization + + + + { + setIsLoading(true); + window.location.replace(`${window.basePath}${authPath}`); + }} + inProgress={isLoading} + > + {!isLoading && `Log in with ${serviceName}`} + + + ); +} + +export default AuthCard; diff --git a/frontend/src/components/AuthPage/SignIn/OAuthSignIn/OAuthSignIn.styled.tsx b/frontend/src/components/AuthPage/SignIn/OAuthSignIn/OAuthSignIn.styled.tsx new file mode 100644 index 000000000..bf238e9b2 --- /dev/null +++ b/frontend/src/components/AuthPage/SignIn/OAuthSignIn/OAuthSignIn.styled.tsx @@ -0,0 +1,25 @@ +import styled from 'styled-components'; + +export const OAuthSignInStyled = styled.div` + display: flex; + flex-direction: column; + gap: 8px; +`; + +export const ErrorMessage = styled.div` + display: flex; + column-gap: 2px; + align-items: center; + justify-content: center; + font-weight: 400; + font-size: 14px; + line-height: 20px; + margin-bottom: 8px; +`; + +export const ErrorMessageText = styled.span` + ${({ theme }) => theme.auth_page.signIn.errorMessage}; + font-weight: 400; + font-size: 14px; + line-height: 20px; +`; diff --git a/frontend/src/components/AuthPage/SignIn/OAuthSignIn/OAuthSignIn.tsx b/frontend/src/components/AuthPage/SignIn/OAuthSignIn/OAuthSignIn.tsx new file mode 100644 index 000000000..fca5b4925 --- /dev/null +++ b/frontend/src/components/AuthPage/SignIn/OAuthSignIn/OAuthSignIn.tsx @@ -0,0 +1,55 @@ +import React, { ElementType } from 'react'; +import GitHubIcon from 'components/common/Icons/GitHubIcon'; +import GoogleIcon from 'components/common/Icons/GoogleIcon'; +import CognitoIcon from 'components/common/Icons/CognitoIcon'; +import OktaIcon from 'components/common/Icons/OktaIcon'; +import KeycloakIcon from 'components/common/Icons/KeycloakIcon'; +import ServiceImage from 'components/common/Icons/ServiceImage'; +import { OAuthProvider } from 'generated-sources'; +import { useLocation } from 'react-router-dom'; +import AlertIcon from 'components/common/Icons/AlertIcon'; + +import * as S from './OAuthSignIn.styled'; +import AuthCard from './AuthCard/AuthCard'; + +interface Props { + oAuthProviders: OAuthProvider[] | undefined; +} + +const ServiceIconMap: Record = { + github: GitHubIcon, + google: GoogleIcon, + cognito: CognitoIcon, + keycloak: KeycloakIcon, + okta: OktaIcon, + unknownService: ServiceImage, +}; + +function OAuthSignIn({ oAuthProviders }: Props) { + const { search } = useLocation(); + + return ( + + {search.includes('error') && ( + + + Invalid credentials + + )} + {oAuthProviders?.map((provider) => ( + + ))} + + ); +} + +export default OAuthSignIn; diff --git a/frontend/src/components/AuthPage/SignIn/SignIn.styled.tsx b/frontend/src/components/AuthPage/SignIn/SignIn.styled.tsx new file mode 100644 index 000000000..0f24b45fd --- /dev/null +++ b/frontend/src/components/AuthPage/SignIn/SignIn.styled.tsx @@ -0,0 +1,19 @@ +import styled, { css } from 'styled-components'; + +export const SignInStyled = styled.div` + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 320px; + gap: 56px; + flex-grow: 1; +`; + +export const SignInTitle = styled.span( + ({ theme }) => css` + color: ${theme.auth_page.signIn.titleColor}; + font-size: 24px; + font-weight: 600; + ` +); diff --git a/frontend/src/components/AuthPage/SignIn/SignIn.tsx b/frontend/src/components/AuthPage/SignIn/SignIn.tsx new file mode 100644 index 000000000..987ee5ebf --- /dev/null +++ b/frontend/src/components/AuthPage/SignIn/SignIn.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { AuthType, OAuthProvider } from 'generated-sources'; + +import BasicSignIn from './BasicSignIn/BasicSignIn'; +import * as S from './SignIn.styled'; +import OAuthSignIn from './OAuthSignIn/OAuthSignIn'; + +interface Props { + authType?: AuthType; + oAuthProviders?: OAuthProvider[]; +} + +function SignInForm({ authType, oAuthProviders }: Props) { + return ( + + Sign in + {(authType === AuthType.LDAP || authType === AuthType.LOGIN_FORM) && ( + + )} + {authType === AuthType.OAUTH2 && ( + + )} + + ); +} + +export default SignInForm; diff --git a/frontend/src/components/NavBar/UserInfo/UserInfo.tsx b/frontend/src/components/NavBar/UserInfo/UserInfo.tsx index dae43364c..b52cc7631 100644 --- a/frontend/src/components/NavBar/UserInfo/UserInfo.tsx +++ b/frontend/src/components/NavBar/UserInfo/UserInfo.tsx @@ -19,7 +19,7 @@ const UserInfo = () => { } > - + Log out diff --git a/frontend/src/components/common/Button/Button.tsx b/frontend/src/components/common/Button/Button.tsx index 828b5d301..8964b6e17 100644 --- a/frontend/src/components/common/Button/Button.tsx +++ b/frontend/src/components/common/Button/Button.tsx @@ -9,6 +9,7 @@ export interface Props ButtonProps { to?: string | object; inProgress?: boolean; + className?: string; } export const Button: FC = ({ @@ -20,7 +21,7 @@ export const Button: FC = ({ }) => { if (to) { return ( - + {children} diff --git a/frontend/src/components/common/Icons/AlertIcon.tsx b/frontend/src/components/common/Icons/AlertIcon.tsx new file mode 100644 index 000000000..3c79f78e6 --- /dev/null +++ b/frontend/src/components/common/Icons/AlertIcon.tsx @@ -0,0 +1,22 @@ +import React from 'react'; + +const AlertIcon: React.FC = () => { + return ( + + + + ); +}; + +export default AlertIcon; diff --git a/frontend/src/components/common/Icons/CognitoIcon.tsx b/frontend/src/components/common/Icons/CognitoIcon.tsx new file mode 100644 index 000000000..2d0b0d38a --- /dev/null +++ b/frontend/src/components/common/Icons/CognitoIcon.tsx @@ -0,0 +1,49 @@ +import React from 'react'; +import styled from 'styled-components'; + +function CognitoIcon() { + return ( + + + + + + + + + + + + + + + ); +} + +export default styled(CognitoIcon)``; diff --git a/frontend/src/components/common/Icons/GoogleIcon.tsx b/frontend/src/components/common/Icons/GoogleIcon.tsx new file mode 100644 index 000000000..2e569dbfe --- /dev/null +++ b/frontend/src/components/common/Icons/GoogleIcon.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import styled from 'styled-components'; + +function GoogleIcon() { + return ( + + + + + + + ); +} + +export default styled(GoogleIcon)``; diff --git a/frontend/src/components/common/Icons/KeycloakIcon.tsx b/frontend/src/components/common/Icons/KeycloakIcon.tsx new file mode 100644 index 000000000..e6b45ef69 --- /dev/null +++ b/frontend/src/components/common/Icons/KeycloakIcon.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import styled from 'styled-components'; + +function KeycloakIcon() { + return ( + + + + + ); +} + +export default styled(KeycloakIcon)``; diff --git a/frontend/src/components/common/Icons/OktaIcon.tsx b/frontend/src/components/common/Icons/OktaIcon.tsx new file mode 100644 index 000000000..a9d6871b0 --- /dev/null +++ b/frontend/src/components/common/Icons/OktaIcon.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import styled from 'styled-components'; + +function OktaIcon() { + return ( + + + + ); +} + +export default styled(OktaIcon)``; diff --git a/frontend/src/components/common/Icons/ServiceImage.tsx b/frontend/src/components/common/Icons/ServiceImage.tsx new file mode 100644 index 000000000..9311334f1 --- /dev/null +++ b/frontend/src/components/common/Icons/ServiceImage.tsx @@ -0,0 +1,11 @@ +import React from 'react'; + +interface Props { + serviceName: string; +} + +function ServiceImage({ serviceName }: Props) { + return {serviceName}; +} + +export default ServiceImage; diff --git a/frontend/src/components/contexts/GlobalSettingsContext.tsx b/frontend/src/components/contexts/GlobalSettingsContext.tsx index 4de05307b..5e906c292 100644 --- a/frontend/src/components/contexts/GlobalSettingsContext.tsx +++ b/frontend/src/components/contexts/GlobalSettingsContext.tsx @@ -1,6 +1,7 @@ import { useAppInfo } from 'lib/hooks/api/appConfig'; import React from 'react'; import { ApplicationInfoEnabledFeaturesEnum } from 'generated-sources'; +import { useNavigate } from 'react-router-dom'; interface GlobalSettingsContextProps { hasDynamicConfig: boolean; @@ -15,13 +16,26 @@ export const GlobalSettingsProvider: React.FC< React.PropsWithChildren > = ({ children }) => { const info = useAppInfo(); - const value = React.useMemo(() => { - const features = info.data?.enabledFeatures || []; - return { - hasDynamicConfig: features.includes( - ApplicationInfoEnabledFeaturesEnum.DYNAMIC_CONFIG - ), - }; + const navigate = useNavigate(); + const [value, setValue] = React.useState({ + hasDynamicConfig: false, + }); + + React.useEffect(() => { + if (info.data?.redirect && !info.isFetching) { + navigate('login'); + return; + } + + const features = info?.data?.response?.enabledFeatures; + + if (features) { + setValue({ + hasDynamicConfig: features.includes( + ApplicationInfoEnabledFeaturesEnum.DYNAMIC_CONFIG + ), + }); + } }, [info.data]); return ( diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 19423d2ac..d6f409ea2 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -11,6 +11,7 @@ import { AuthorizationApi, ApplicationConfigApi, AclsApi, + UnmappedApi, } from 'generated-sources'; import { BASE_PARAMS } from 'lib/constants'; @@ -27,3 +28,4 @@ export const consumerGroupsApiClient = new ConsumerGroupsApi(apiClientConf); export const authApiClient = new AuthorizationApi(apiClientConf); export const appConfigApiClient = new ApplicationConfigApi(apiClientConf); export const aclApiClient = new AclsApi(apiClientConf); +export const internalApiClient = new UnmappedApi(apiClientConf); diff --git a/frontend/src/lib/hooks/api/appConfig.ts b/frontend/src/lib/hooks/api/appConfig.ts index e3ee0fdcb..a91c6eb4b 100644 --- a/frontend/src/lib/hooks/api/appConfig.ts +++ b/frontend/src/lib/hooks/api/appConfig.ts @@ -1,21 +1,52 @@ -import { appConfigApiClient as api } from 'lib/api'; +import { + appConfigApiClient as appConfig, + internalApiClient as internalApi, +} from 'lib/api'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { ApplicationConfig, ApplicationConfigPropertiesKafkaClusters, + ApplicationInfo, } from 'generated-sources'; import { QUERY_REFETCH_OFF_OPTIONS } from 'lib/constants'; -export function useAppInfo() { +export function useAuthSettings() { return useQuery( - ['app', 'info'], - () => api.getApplicationInfo(), + ['app', 'authSettings'], + () => appConfig.getAuthenticationSettings(), QUERY_REFETCH_OFF_OPTIONS ); } +export function useAuthenticate() { + return useMutation({ + mutationFn: (params: { username: string; password: string }) => + internalApi.authenticateRaw(params, { + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + }), + }); +} + +export function useAppInfo() { + return useQuery(['app', 'info'], async () => { + const data = await appConfig.getApplicationInfoRaw(); + + let response: ApplicationInfo = {}; + try { + response = await data.value(); + } catch { + response = {}; + } + + return { + redirect: data.raw.url.includes('auth'), + response, + }; + }); +} + export function useAppConfig() { - return useQuery(['app', 'config'], () => api.getCurrentConfig()); + return useQuery(['app', 'config'], () => appConfig.getCurrentConfig()); } function aggregateClusters( @@ -47,7 +78,7 @@ export function useUpdateAppConfig({ const client = useQueryClient(); return useMutation( async (cluster: ApplicationConfigPropertiesKafkaClusters) => { - const existingConfig = await api.getCurrentConfig(); + const existingConfig = await appConfig.getCurrentConfig(); const clusters = aggregateClusters( cluster, @@ -63,7 +94,7 @@ export function useUpdateAppConfig({ kafka: { clusters }, }, }; - return api.restartWithConfig({ restartRequest: { config } }); + return appConfig.restartWithConfig({ restartRequest: { config } }); }, { onSuccess: () => client.invalidateQueries(['app', 'config']), @@ -82,7 +113,7 @@ export function useAppConfigFilesUpload() { export function useValidateAppConfig() { return useMutation((config: ApplicationConfigPropertiesKafkaClusters) => - api.validateConfig({ + appConfig.validateConfig({ applicationConfig: { properties: { kafka: { clusters: [config] } } }, }) ); diff --git a/frontend/src/theme/theme.ts b/frontend/src/theme/theme.ts index f6cd2bacc..bdfe93271 100644 --- a/frontend/src/theme/theme.ts +++ b/frontend/src/theme/theme.ts @@ -57,6 +57,7 @@ const Colors = { '10': '#FAD1D1', '20': '#F5A3A3', '50': '#E51A1A', + '52': '#E63B19', '55': '#CF1717', '60': '#B81414', }, @@ -79,6 +80,45 @@ const Colors = { const baseTheme = { defaultIconColor: Colors.neutral[50], + auth_page: { + backgroundColor: Colors.brand[0], + fontFamily: 'Inter, sans-serif', + header: { + cellBorderColor: Colors.brand[10], + LogoBgColor: Colors.brand[90], + LogoTextColor: Colors.brand[0], + }, + signIn: { + titleColor: Colors.brand[90], + errorMessage: { + color: Colors.red[52], + }, + label: { + color: Colors.brand[70], + }, + authCard: { + borderRadius: '16px', + borderColor: Colors.brand[10], + backgroundColor: Colors.brand[0], + serviceNamecolor: Colors.brand[90], + serviceTextColor: Colors.brand[50], + }, + }, + footer: { + fontSize: '12px', + span: { + color: Colors.brand[70], + fontWeight: 500, + }, + p: { + color: Colors.brand[50], + fontWeight: 400, + }, + }, + icons: { + githubColor: Colors.brand[90], + }, + }, heading: { h1: { color: Colors.neutral[90], @@ -821,6 +861,38 @@ export type ThemeType = typeof theme; export const darkTheme: ThemeType = { ...baseTheme, + auth_page: { + backgroundColor: Colors.neutral[90], + fontFamily: baseTheme.auth_page.fontFamily, + header: { + cellBorderColor: Colors.brand[80], + LogoBgColor: Colors.brand[0], + LogoTextColor: Colors.brand[90], + }, + signIn: { + ...baseTheme.auth_page.signIn, + titleColor: Colors.brand[0], + label: { + color: Colors.brand[30], + }, + authCard: { + ...baseTheme.auth_page.signIn.authCard, + borderColor: Colors.brand[80], + backgroundColor: Colors.brand[85], + serviceNamecolor: Colors.brand[0], + }, + }, + footer: { + ...baseTheme.auth_page.footer, + span: { + color: Colors.brand[10], + fontWeight: 500, + }, + }, + icons: { + githubColor: Colors.brand[0], + }, + }, logo: { color: '#FDFDFD', }, diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 3a4e861e9..455ef39ae 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -3,6 +3,7 @@ import react from '@vitejs/plugin-react-swc'; import tsconfigPaths from 'vite-tsconfig-paths'; import { ViteEjsPlugin } from 'vite-plugin-ejs'; import checker from 'vite-plugin-checker'; +import { IncomingMessage } from 'http'; export default defineConfig(({ mode }) => { process.env = { ...process.env, ...loadEnv(mode, process.cwd()) }; @@ -87,6 +88,21 @@ export default defineConfig(({ mode }) => { ...defaultConfig.server, open: true, proxy: { + '/login': { + target: isProxy, + changeOrigin: true, + secure: false, + bypass: (req: IncomingMessage) => { + if (req.method === 'GET') { + return req.url; + } + }, + }, + '/logout': { + target: isProxy, + changeOrigin: true, + secure: false, + }, '/api': { target: isProxy, changeOrigin: true,