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 000000000..8006b13f5
Binary files /dev/null and b/frontend/public/serviceImage.png differ
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 ;
+}
+
+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,