diff --git a/conf/openmetadata.yaml b/conf/openmetadata.yaml index 6f41b5e55a4..487d7b474dc 100644 --- a/conf/openmetadata.yaml +++ b/conf/openmetadata.yaml @@ -267,13 +267,10 @@ email: applicationConfig: logoConfig: - logoLocationType: ${OM_LOGO_LOCATION_TYPE:-openmetadata} #either "openmetadata' or { "url" or "filePath" , based on this specify either '*AbsoluteFilePath' or '*LogoUrlPath' } - loginPageLogoAbsoluteFilePath: ${OM_LOGO_LOGIN_LOCATION_FILE_PATH:-""} #login page logo , work in "filePath" mode - loginPageLogoUrlPath: ${OM_LOGO_LOGIN_LOCATION_URL_PATH:-""} #login page logo , work in "url" mode - navBarLogoAbsoluteFilePath: ${OM_LOGO_NAVBAR_LOCATION_FILE_PATH:-""} #nav bar logo , work in "filePath" mode - navBarLogoUrlPath: ${OM_LOGO_NAVBAR_LOCATION_URL_PATH:-""} #nav bar logo , work in "url" mode + customLogoUrlPath: ${OM_CUSTOM_LOGO_URL_PATH:-""} #login page logo + customMonogramUrlPath: ${OM_CUSTOM_MONOGRAM_URL_PATH:-""} #nav bar logo loginConfig: maxLoginFailAttempts: ${OM_MAX_FAILED_LOGIN_ATTEMPTS:-3} - accessBlockTime: ${OM_LOGIN_ACCESS_BLOCKTIME:-600} + accessBlockTime: ${OM_LOGIN_ACCESS_BLOCK_TIME:-600} jwtTokenExpiryTime: ${OM_JWT_EXPIRY_TIME:-3600} diff --git a/docker/local-metadata/docker-compose-postgres.yml b/docker/local-metadata/docker-compose-postgres.yml index e40c9179eed..93b1c5b1ca3 100644 --- a/docker/local-metadata/docker-compose-postgres.yml +++ b/docker/local-metadata/docker-compose-postgres.yml @@ -109,6 +109,12 @@ services: ELASTICSEARCH_PASSWORD: ${ELASTICSEARCH_PASSWORD:-""} # Heap OPTS Configurations OPENMETADATA_HEAP_OPTS: ${OPENMETADATA_HEAP_OPTS:--Xmx1G -Xms1G} + # Application Config + CUSTOM_LOGO_URL_PATH: ${CUSTOM_LOGO_URL_PATH:-""} + CUSTOM_MONOGRAM_URL_PATH: ${CUSTOM_MONOGRAM_URL_PATH:-""} + OM_MAX_FAILED_LOGIN_ATTEMPTS: ${OM_MAX_FAILED_LOGIN_ATTEMPTS:-3} + OM_LOGIN_ACCESS_BLOCK_TIME: ${OM_LOGIN_ACCESS_BLOCK_TIME:-600} + OM_JWT_EXPIRY_TIME: ${OM_JWT_EXPIRY_TIME:-3600} expose: - 8585 - 8586 diff --git a/docker/local-metadata/docker-compose.yml b/docker/local-metadata/docker-compose.yml index dc8914b8ee2..21a1098d294 100644 --- a/docker/local-metadata/docker-compose.yml +++ b/docker/local-metadata/docker-compose.yml @@ -108,6 +108,12 @@ services: ELASTICSEARCH_PASSWORD: ${ELASTICSEARCH_PASSWORD:-""} # Heap OPTS Configurations OPENMETADATA_HEAP_OPTS: ${OPENMETADATA_HEAP_OPTS:--Xmx1G -Xms1G} + # Application Config + CUSTOM_LOGO_URL_PATH: ${CUSTOM_LOGO_URL_PATH:-""} + CUSTOM_MONOGRAM_URL_PATH: ${CUSTOM_MONOGRAM_URL_PATH:-""} + OM_MAX_FAILED_LOGIN_ATTEMPTS: ${OM_MAX_FAILED_LOGIN_ATTEMPTS:-3} + OM_LOGIN_ACCESS_BLOCK_TIME: ${OM_LOGIN_ACCESS_BLOCK_TIME:-600} + OM_JWT_EXPIRY_TIME: ${OM_JWT_EXPIRY_TIME:-3600} expose: - 8585 - 8586 diff --git a/docker/metadata/docker-compose-postgres.yml b/docker/metadata/docker-compose-postgres.yml index cfe87a57350..f50bf4df4ca 100644 --- a/docker/metadata/docker-compose-postgres.yml +++ b/docker/metadata/docker-compose-postgres.yml @@ -101,6 +101,12 @@ services: ELASTICSEARCH_PASSWORD: ${ELASTICSEARCH_PASSWORD:-""} # Heap OPTS Configurations OPENMETADATA_HEAP_OPTS: ${OPENMETADATA_HEAP_OPTS:--Xmx1G -Xms1G} + # Application Config + CUSTOM_LOGO_URL_PATH: ${CUSTOM_LOGO_URL_PATH:-""} + CUSTOM_MONOGRAM_URL_PATH: ${CUSTOM_MONOGRAM_URL_PATH:-""} + OM_MAX_FAILED_LOGIN_ATTEMPTS: ${OM_MAX_FAILED_LOGIN_ATTEMPTS:-3} + OM_LOGIN_ACCESS_BLOCK_TIME: ${OM_LOGIN_ACCESS_BLOCK_TIME:-600} + OM_JWT_EXPIRY_TIME: ${OM_JWT_EXPIRY_TIME:-3600} expose: - 8585 diff --git a/docker/metadata/docker-compose.yml b/docker/metadata/docker-compose.yml index 9cff7a3c12d..55ef671beb0 100644 --- a/docker/metadata/docker-compose.yml +++ b/docker/metadata/docker-compose.yml @@ -99,6 +99,12 @@ services: ELASTICSEARCH_PASSWORD: ${ELASTICSEARCH_PASSWORD:-""} # Heap OPTS Configurations OPENMETADATA_HEAP_OPTS: ${OPENMETADATA_HEAP_OPTS:--Xmx1G -Xms1G} + # Application Config + CUSTOM_LOGO_URL_PATH: ${CUSTOM_LOGO_URL_PATH:-""} + CUSTOM_MONOGRAM_URL_PATH: ${CUSTOM_MONOGRAM_URL_PATH:-""} + OM_MAX_FAILED_LOGIN_ATTEMPTS: ${OM_MAX_FAILED_LOGIN_ATTEMPTS:-3} + OM_LOGIN_ACCESS_BLOCK_TIME: ${OM_LOGIN_ACCESS_BLOCK_TIME:-600} + OM_JWT_EXPIRY_TIME: ${OM_JWT_EXPIRY_TIME:-3600} expose: - 8585 diff --git a/openmetadata-docs/content/how-to-guides/custom-logo/how-to-add-custom-logo.md b/openmetadata-docs/content/how-to-guides/custom-logo/how-to-add-custom-logo.md index 9842fb19db7..902036bcbb2 100644 --- a/openmetadata-docs/content/how-to-guides/custom-logo/how-to-add-custom-logo.md +++ b/openmetadata-docs/content/how-to-guides/custom-logo/how-to-add-custom-logo.md @@ -3,6 +3,8 @@ title: How to change the Login Page and Nav Bar Logo slug: /how-to-guides/custom-logo/how-to-add-custom-logo --- +# How to add a custom logo for the application + To change the Logo for the application, we need to update logo at two locations. 1. Login Page @@ -13,36 +15,24 @@ To change the Logo for the application, we need to update logo at two locations. navBar-image -# How to add a custom logo for the application - ### Step 1: Get the image size as per the following formats. -- Login Page (1 px x 2px) -- Navigation Bar (1 px x 2px) +- Monogram aspect ratio should be 1:1 and Recommended size should be 30 x 30 px +- Logo aspect ratio should be 5:2 and Recommended size should be 150 x 60 px ### Step 2: Configure 'openmetadata.yaml' or the corresponding environment variables ```yaml applicationConfig: logoConfig: - logoLocationType: ${OM_LOGO_LOCATION_TYPE:-openmetadata} #either "openmetadata' or { "url" or "filePath" , based on this specify either '*AbsoluteFilePath' or '*LogoUrlPath' } - loginPageLogoAbsoluteFilePath: ${OM_LOGO_LOGIN_LOCATION_FILE_PATH:-""} #login page logo , work in "filePath" mode - loginPageLogoUrlPath: ${OM_LOGO_LOGIN_LOCATION_URL_PATH:-""} #login page logo , work in "url" mode - navBarLogoAbsoluteFilePath: ${OM_LOGO_NAVBAR_LOCATION_FILE_PATH:-""} #nav bar logo , work in "filePath" mode - navBarLogoUrlPath: ${OM_LOGO_NAVBAR_LOCATION_URL_PATH:-""} #nav bar logo , work in "url" mode + customLogoUrlPath: ${OM_CUSTOM_LOGO_URL_PATH:-""} #login page logo + customMonogramUrlPath: ${OM_CUSTOM_MONOGRAM_URL_PATH:-""} #nav bar logo ``` -1. `logoLocationType` set to 'openmetadata' - - In this case it will take the default OM logo, we don't need to configure anything else. +1. `customLogoUrlPath` -2. `logoLocationType` set to 'filePath' + - URL path for the login page logo. - - In this case it will take the custom logo from the specified absolute paths, need to configure following as well. - - `loginPageLogoAbsoluteFilePath` -> This is the path for Login Page Logo Image. - - `navBarLogoAbsoluteFilePath` -> This is the path for Navigation Bar Logo Image. +2. `customMonogramUrlPath` -3. `logoLocationType` set to 'url' - - - In this case it will take the custom logo from the specified url, need to configure following as well. - - `loginPageLogoUrlPath` -> This is the url for Login Page Logo Image. - - `navBarLogoUrlPath` -> This is the url for Navigation Bar Logo Image. + - URL path for the navbar logo. diff --git a/openmetadata-service/src/test/resources/openmetadata-secure-test.yaml b/openmetadata-service/src/test/resources/openmetadata-secure-test.yaml index 1353c6866f8..df7e191fcff 100644 --- a/openmetadata-service/src/test/resources/openmetadata-secure-test.yaml +++ b/openmetadata-service/src/test/resources/openmetadata-secure-test.yaml @@ -193,11 +193,8 @@ email: applicationConfig: logoConfig: - logoLocationType: "openmetadata" - loginPageLogoAbsoluteFilePath: "" - loginPageLogoUrlPath: "" - navBarLogoAbsoluteFilePath: "" - navBarLogoUrlPath: "" + customLogoUrlPath: "" + customMonogramUrlPath: "" loginConfig: maxLoginFailAttempts: 3 accessBlockTime: 600 diff --git a/openmetadata-spec/src/main/resources/json/schema/configuration/applicationConfiguration.json b/openmetadata-spec/src/main/resources/json/schema/configuration/applicationConfiguration.json index deaac816724..75feb41820d 100644 --- a/openmetadata-spec/src/main/resources/json/schema/configuration/applicationConfiguration.json +++ b/openmetadata-spec/src/main/resources/json/schema/configuration/applicationConfiguration.json @@ -11,29 +11,15 @@ "type": "object", "javaType": "org.openmetadata.api.configuration.LogoConfiguration", "properties": { - "logoLocationType": { - "description": "Type of Logo Location", - "type": "string", - "enum": ["openmetadata","filePath","url"] - }, - "loginPageLogoAbsoluteFilePath": { - "description": "Login Page Absolute File Path For Logo Image", - "type": "string" - }, - "loginPageLogoUrlPath": { + "customLogoUrlPath": { "description": "Login Page Logo Image Url", "type": "string" }, - "navBarLogoAbsoluteFilePath": { - "description": "Navigation Bar Absolute File Path For Logo Image", - "type": "string" - }, - "navBarLogoUrlPath": { + "customMonogramUrlPath": { "description": "Navigation Bar Logo Image Url", "type": "string" } }, - "required": ["logoLocationType"], "additionalProperties": false }, "loginConfiguration": { diff --git a/openmetadata-ui/src/main/resources/ui/src/App.tsx b/openmetadata-ui/src/main/resources/ui/src/App.tsx index 8ee18a2e3f9..a636773012b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/App.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/App.tsx @@ -12,6 +12,7 @@ */ import Appbar from 'components/app-bar/Appbar'; +import ApplicationConfigProvider from 'components/ApplicationConfigProvider/ApplicationConfigProvider'; import { AuthProvider } from 'components/authentication/auth-provider/AuthProvider'; import ErrorBoundry from 'components/ErrorBoundry/ErrorBoundry'; import GlobalSearchProvider from 'components/GlobalSearchProvider/GlobalSearchProvider'; @@ -36,18 +37,20 @@ const App: FunctionComponent = () => { - - - - - - - - - - - - + + + + + + + + + + + + + + diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ApplicationConfigProvider/ApplicationConfigProvider.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ApplicationConfigProvider/ApplicationConfigProvider.test.tsx new file mode 100644 index 00000000000..23ec10aafc6 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/ApplicationConfigProvider/ApplicationConfigProvider.test.tsx @@ -0,0 +1,77 @@ +/* + * Copyright 2023 Collate. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { act, render, screen } from '@testing-library/react'; +import React from 'react'; +import { getApplicationConfig } from 'rest/miscAPI'; +import ApplicationConfigProvider, { + useApplicationConfigProvider, +} from './ApplicationConfigProvider'; + +const mockApplicationConfig = { + logoConfig: { + customLogoUrlPath: 'https://customlink.source', + + customMonogramUrlPath: 'https://customlink.source', + }, + loginConfig: { + maxLoginFailAttempts: 3, + accessBlockTime: 600, + jwtTokenExpiryTime: 3600, + }, +}; + +jest.mock('rest/miscAPI', () => ({ + getApplicationConfig: jest + .fn() + .mockImplementation(() => Promise.resolve(mockApplicationConfig)), +})); + +describe('ApplicationConfigProvider', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders the children components', async () => { + await act(async () => { + render( + +
Test Children
+
+ ); + }); + + expect(screen.getByText('Test Children')).toBeInTheDocument(); + }); + + it('fetch the application config on mount and set in the context', async () => { + function TestComponent() { + const { logoConfig } = useApplicationConfigProvider(); + + return
{logoConfig?.customLogoUrlPath}
; + } + + await act(async () => { + render( + + + + ); + }); + + expect( + await screen.findByText('https://customlink.source') + ).toBeInTheDocument(); + + expect(getApplicationConfig).toHaveBeenCalledTimes(1); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ApplicationConfigProvider/ApplicationConfigProvider.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ApplicationConfigProvider/ApplicationConfigProvider.tsx new file mode 100644 index 00000000000..29364ec24af --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/ApplicationConfigProvider/ApplicationConfigProvider.tsx @@ -0,0 +1,63 @@ +/* + * Copyright 2023 Collate. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { ApplicationConfiguration } from 'generated/configuration/applicationConfiguration'; +import React, { + createContext, + FC, + ReactNode, + useContext, + useEffect, + useState, +} from 'react'; +import { getApplicationConfig } from 'rest/miscAPI'; + +export const ApplicationConfigContext = createContext( + {} as ApplicationConfiguration +); + +export const useApplicationConfigProvider = () => + useContext(ApplicationConfigContext); + +interface ApplicationConfigProviderProps { + children: ReactNode; +} + +const ApplicationConfigProvider: FC = ({ + children, +}) => { + const [applicationConfig, setApplicationConfig] = + useState({} as ApplicationConfiguration); + + const fetchApplicationConfig = async () => { + try { + const response = await getApplicationConfig(); + + setApplicationConfig(response); + } catch (error) { + // eslint-disable-next-line no-console + console.error(error); + } + }; + + useEffect(() => { + fetchApplicationConfig(); + }, []); + + return ( + + {children} + + ); +}; + +export default ApplicationConfigProvider; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/nav-bar/NavBar.tsx b/openmetadata-ui/src/main/resources/ui/src/components/nav-bar/NavBar.tsx index f27b636f15c..8b3b3153ebd 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/nav-bar/NavBar.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/nav-bar/NavBar.tsx @@ -12,6 +12,7 @@ */ import { Badge, Dropdown, Image, Input, Select, Space, Tooltip } from 'antd'; +import { useApplicationConfigProvider } from 'components/ApplicationConfigProvider/ApplicationConfigProvider'; import { CookieStorage } from 'cookie-storage'; import i18next from 'i18next'; import { debounce, toString } from 'lodash'; @@ -20,6 +21,7 @@ import { useTranslation } from 'react-i18next'; import { NavLink, useHistory } from 'react-router-dom'; import AppState from '../../AppState'; import Logo from '../../assets/svg/logo-monogram.svg'; + import { NOTIFICATION_READ_TIMER, ROUTES, @@ -73,6 +75,8 @@ const NavBar = ({ handleKeyDown, handleOnClick, }: NavBarProps) => { + const { logoConfig } = useApplicationConfigProvider(); + // get current user details const currentUser = useMemo( () => AppState.getCurrentUserDetails(), @@ -295,16 +299,23 @@ const NavBar = ({ [AppState] ); + const brandLogoUrl = useMemo(() => { + return logoConfig?.customMonogramUrlPath ?? Logo; + }, [logoConfig]); + return ( <>
diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/login/index.test.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/login/index.test.tsx index 76033bbcb41..cd323dd4918 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/login/index.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/login/index.test.tsx @@ -11,7 +11,12 @@ * limitations under the License. */ -import { findByTestId, findByText, render } from '@testing-library/react'; +import { + findByTestId, + findByText, + render, + screen, +} from '@testing-library/react'; import { useAuthContext } from 'components/authentication/auth-provider/AuthProvider'; import React from 'react'; import { MemoryRouter } from 'react-router-dom'; @@ -26,6 +31,18 @@ jest.mock('react-router-dom', () => ({ jest.mock('components/authentication/auth-provider/AuthProvider', () => ({ useAuthContext: jest.fn(), })); +jest.mock( + 'components/ApplicationConfigProvider/ApplicationConfigProvider', + () => ({ + useApplicationConfigProvider: jest.fn().mockImplementation(() => ({ + logoConfig: { + customLogoUrlPath: 'https://customlink.source', + + customMonogramUrlPath: 'https://customlink.source', + }, + })), + }) +); jest.mock( 'components/containers/PageContainer', @@ -112,4 +129,23 @@ describe('Test SigninPage Component', () => { expect(signinButton).toBeInTheDocument(); }); + + it('Page should render the correct logo image', async () => { + mockUseAuthContext.mockReturnValue({ + isAuthDisabled: false, + authConfig: { provider: 'custom-oidc', providerName: 'Custom OIDC' }, + onLoginHandler: jest.fn(), + onLogoutHandler: jest.fn(), + }); + render(, { + wrapper: MemoryRouter, + }); + + const brandLogoImage = await screen.findByTestId('brand-logo-image'); + const logoImage = brandLogoImage.querySelector('img') as HTMLImageElement; + + expect(brandLogoImage).toBeInTheDocument(); + + expect(logoImage.src).toEqual('https://customlink.source/'); + }); }); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/login/index.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/login/index.tsx index 5d1c6dca7c7..d757b2e5593 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/login/index.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/login/index.tsx @@ -11,8 +11,19 @@ * limitations under the License. */ -import { Button, Col, Divider, Form, Input, Row, Typography } from 'antd'; +import { + Button, + Col, + Divider, + Form, + Image, + Input, + Row, + Typography, +} from 'antd'; +import Logo from 'assets/svg/logo.svg'; import classNames from 'classnames'; +import { useApplicationConfigProvider } from 'components/ApplicationConfigProvider/ApplicationConfigProvider'; import { useAuthContext } from 'components/authentication/auth-provider/AuthProvider'; import { useBasicAuth } from 'components/authentication/auth-provider/basic-auth.provider'; import Loader from 'components/Loader/Loader'; @@ -32,6 +43,7 @@ import './login.style.less'; import LoginCarousel from './LoginCarousel'; const SigninPage = () => { + const { logoConfig } = useApplicationConfigProvider(); const [loading, setLoading] = useState(false); const [form] = Form.useForm(); @@ -66,6 +78,10 @@ const SigninPage = () => { return isAuthDisabled || isAuthenticated; }, [isAuthDisabled, isAuthenticated]); + const brandLogoUrl = useMemo(() => { + return logoConfig?.customLogoUrlPath ?? Logo; + }, [logoConfig]); + const isTokenExpired = () => { const token = localState.getOidcToken(); if (token) { @@ -194,7 +210,14 @@ const SigninPage = () => { className={classNames('mt-24 text-center flex-center flex-col', { 'sso-container': !isAuthProviderBasic, })}> - + OpenMetadata Logo {t('message.om-description')}{' '} diff --git a/openmetadata-ui/src/main/resources/ui/src/rest/miscAPI.ts b/openmetadata-ui/src/main/resources/ui/src/rest/miscAPI.ts index 0fcc0967f46..05154662f76 100644 --- a/openmetadata-ui/src/main/resources/ui/src/rest/miscAPI.ts +++ b/openmetadata-ui/src/main/resources/ui/src/rest/miscAPI.ts @@ -14,6 +14,7 @@ import { AxiosResponse } from 'axios'; import { Edge } from 'components/EntityLineage/EntityLineage.interface'; import { ExploreSearchIndex } from 'components/Explore/explore.interface'; +import { ApplicationConfiguration } from 'generated/configuration/applicationConfiguration'; import { AuthorizerConfiguration } from 'generated/configuration/authorizerConfiguration'; import { SearchIndex } from '../enums/search.enum'; import { AuthenticationConfiguration } from '../generated/configuration/authenticationConfiguration'; @@ -71,6 +72,13 @@ export const fetchAuthenticationConfig = async () => { return response.data; }; +export const getApplicationConfig = async () => { + const response = await APIClient.get( + '/system/config/applicationConfig' + ); + + return response.data; +}; export const fetchAuthorizerConfig = async () => { const response = await APIClient.get(