diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/system/ConfigResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/system/ConfigResource.java
index 916f8892ae7..b58fe8d9cf1 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/system/ConfigResource.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/system/ConfigResource.java
@@ -24,13 +24,16 @@ import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import org.openmetadata.api.configuration.ApplicationConfiguration;
+import org.openmetadata.api.configuration.LogoConfiguration;
import org.openmetadata.catalog.security.client.SamlSSOClientConfig;
import org.openmetadata.catalog.type.IdentityProviderConfig;
import org.openmetadata.schema.api.security.AuthenticationConfiguration;
import org.openmetadata.schema.api.security.AuthorizerConfiguration;
+import org.openmetadata.schema.settings.SettingsType;
import org.openmetadata.service.OpenMetadataApplicationConfig;
import org.openmetadata.service.clients.pipeline.PipelineServiceAPIClientConfig;
import org.openmetadata.service.resources.Collection;
+import org.openmetadata.service.resources.settings.SettingsCache;
import org.openmetadata.service.security.jwt.JWKSResponse;
import org.openmetadata.service.security.jwt.JWTTokenGenerator;
@@ -84,6 +87,24 @@ public class ConfigResource {
return authenticationConfiguration;
}
+ @GET
+ @Path(("/customLogoConfiguration"))
+ @Operation(
+ operationId = "getCustomLogoConfiguration",
+ summary = "Get Custom Logo configuration",
+ responses = {
+ @ApiResponse(
+ responseCode = "200",
+ description = "Logo Configuration",
+ content =
+ @Content(
+ mediaType = "application/json",
+ schema = @Schema(implementation = AuthenticationConfiguration.class)))
+ })
+ public LogoConfiguration getCustomLogoConfig() {
+ return SettingsCache.getInstance().getSetting(SettingsType.CUSTOM_LOGO_CONFIGURATION, LogoConfiguration.class);
+ }
+
@GET
@Path(("/authorizer"))
@Operation(
diff --git a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/CustomLogoConfig.spec.js b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/CustomLogoConfig.spec.js
new file mode 100644
index 00000000000..dec41beac14
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/CustomLogoConfig.spec.js
@@ -0,0 +1,119 @@
+/*
+ * 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 { interceptURL, verifyResponseStatusCode } from '../../common/common';
+
+const config = {
+ logo: 'https://custom-logo.png',
+ monogram: 'https://custom-monogram.png',
+ logoError: 'Logo URL is not valid url',
+ monogramError: 'Monogram URL is not valid url',
+};
+
+describe('Custom Logo Config', () => {
+ beforeEach(() => {
+ cy.login();
+
+ cy.get('[data-testid="appbar-item-settings"]')
+ .should('exist')
+ .and('be.visible')
+ .click();
+
+ interceptURL(
+ 'GET',
+ 'api/v1/system/settings/customLogoConfiguration',
+ 'customLogoConfiguration'
+ );
+
+ cy.get('[data-testid="global-setting-left-panel"]')
+ .contains('Custom Logo')
+ .scrollIntoView()
+ .should('be.visible')
+ .and('exist')
+ .click();
+
+ verifyResponseStatusCode('@customLogoConfiguration', 200);
+ });
+
+ it('Should have default config', () => {
+ cy.get('[data-testid="sub-heading"]')
+ .should('be.visible')
+ .contains('Configure The Application Logo and Monogram.');
+ cy.get('[data-testid="logo-url"]').should('be.visible').contains('--');
+ cy.get('[data-testid="monogram-url"]').should('be.visible').contains('--');
+ cy.get('[data-testid="edit-button"]').should('be.visible');
+ });
+
+ it('Should update the config', () => {
+ interceptURL(
+ 'GET',
+ 'api/v1/system/settings/customLogoConfiguration',
+ 'customLogoConfiguration'
+ );
+ cy.get('[data-testid="edit-button"]').should('be.visible').click();
+ verifyResponseStatusCode('@customLogoConfiguration', 200);
+
+ cy.get('[data-testid="customLogoUrlPath"]')
+ .scrollIntoView()
+ .should('be.visible')
+ .click()
+ .clear()
+ .type('incorrect url');
+
+ // validation should work
+ cy.get('[role="alert"]').should('contain', config.logoError);
+
+ cy.get('[data-testid="customLogoUrlPath"]')
+ .scrollIntoView()
+ .should('be.visible')
+ .click()
+ .clear()
+ .type(config.logo);
+
+ cy.get('[data-testid="customMonogramUrlPath"]')
+ .scrollIntoView()
+ .should('be.visible')
+ .click()
+ .clear()
+ .type('incorrect url');
+
+ // validation should work
+ cy.get('[role="alert"]').should('contain', config.monogramError);
+
+ cy.get('[data-testid="customMonogramUrlPath"]')
+ .scrollIntoView()
+ .should('be.visible')
+ .click()
+ .clear()
+ .type(config.monogram);
+
+ interceptURL('PUT', 'api/v1/system/settings', 'updatedConfig');
+
+ interceptURL(
+ 'GET',
+ 'api/v1/system/settings/customLogoConfiguration',
+ 'updatedCustomLogoConfiguration'
+ );
+
+ cy.get('[data-testid="save-button"]').click();
+
+ verifyResponseStatusCode('@updatedConfig', 200);
+ verifyResponseStatusCode('@updatedCustomLogoConfiguration', 200);
+
+ cy.get('[data-testid="logo-url"]')
+ .should('be.visible')
+ .contains(config.logo);
+ cy.get('[data-testid="monogram-url"]')
+ .should('be.visible')
+ .contains(config.monogram);
+ });
+});
diff --git a/openmetadata-ui/src/main/resources/ui/public/locales/en-US/OpenMetadata/CustomLogoConfiguration.md b/openmetadata-ui/src/main/resources/ui/public/locales/en-US/OpenMetadata/CustomLogoConfiguration.md
new file mode 100644
index 00000000000..e5d2efe5a63
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/public/locales/en-US/OpenMetadata/CustomLogoConfiguration.md
@@ -0,0 +1,31 @@
+# Custom Logo Configuration
+
+To change the Logo for the application, we need to update logo at two locations.
+
+$$note
+It might take a few minutes to reflect changes.
+$$
+
+Following configuration is needed to allow OpenMetadata to update logo.
+
+$$section
+
+### Logo URL $(id="customLogoUrlPath")
+
+URL path for the login page logo.
+$$
+
+$$note
+Logo aspect ratio should be 5:2 and Recommended size should be 150 x 60 px
+$$
+
+$$section
+
+### Monogram URL $(id="customMonogramUrlPath")
+
+URL path for the navbar logo.
+$$
+
+$$note
+Monogram aspect ratio should be 1:1 and Recommended size should be 30 x 30 px
+$$
diff --git a/openmetadata-ui/src/main/resources/ui/public/locales/en-US/OpenMetadata/EmailConfiguration.md b/openmetadata-ui/src/main/resources/ui/public/locales/en-US/OpenMetadata/EmailConfiguration.md
index 3acbc91c934..eaa96d5a04a 100644
--- a/openmetadata-ui/src/main/resources/ui/public/locales/en-US/OpenMetadata/EmailConfiguration.md
+++ b/openmetadata-ui/src/main/resources/ui/public/locales/en-US/OpenMetadata/EmailConfiguration.md
@@ -1,8 +1,8 @@
# Email Configuration
-Openmetadata is able to send Emails on a various steps like Signup, Forgot Password , Reset Password, Change Event updates.
-
-Following configuration is needed to allow Openmetadata to send Emails.
+OpenMetadata is able to send Emails on a various steps like SignUp, Forgot Password , Reset Password, Change Event updates.
+
+Following configuration is needed to allow OpenMetadata to send Emails.
$$section
@@ -29,7 +29,7 @@ $$section
### OpenMetadata URL $(id="openMetadataUrl")
-Url of the Openmetadata Server, in case of Docker or K8s this needs to be the external Url used to access the UI.
+Url of the OpenMetadata Server, in case of Docker or K8s this needs to be the external Url used to access the UI.
$$
$$section
@@ -45,12 +45,11 @@ $$section
Port of the SMTP Server, this depends on the transportation strategy below.
Following is the mapping between port and the transportation strategy.
-
-
+
**SMTP:**- If SMTP port is 25 use this
-
+
**SMTPS:**- If SMTP port is 465 use this
-
+
**SMTP_TLS:**- If SMTP port is 587 use this
$$
@@ -58,9 +57,9 @@ $$section
### Emailing entity $(id="emailingEntity")
-This defines the entity that's sending Email. By default, it's `Openmetadata`.
-
-If your company name is `JohnDoe` setting it up will update subject line, content line so that mails have `JohnDoe` inplace of `Openmetadata`.
+This defines the entity that's sending Email. By default, it's `OpenMetadata`.
+
+If your company name is `JohnDoe` setting it up will update subject line, content line so that mails have `JohnDoe` inplace of `OpenMetadata`.
$$
@@ -76,9 +75,9 @@ $$section
### Support URL $(id="supportUrl")
A support Url link is created in the mails to allow the users to reach in case of issues.
-
+
If you have your internal channels / groups this can be updated here.
-
+
Default: `https://slack.open-metadata.org`.
$$
@@ -87,6 +86,6 @@ $$section
### Transportation strategy $(id="transportationStrategy")
-Possible values: `SMTP`, `SMTPS`, `SMTP_TLS`. Depends as per the `port` above.
+Possible values: `SMTP`, `SMTPS`, `SMTP_TLS`. Depends as per the `port` above.
$$
diff --git a/openmetadata-ui/src/main/resources/ui/src/App.tsx b/openmetadata-ui/src/main/resources/ui/src/App.tsx
index a636773012b..9678d2bb807 100644
--- a/openmetadata-ui/src/main/resources/ui/src/App.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/App.tsx
@@ -36,8 +36,8 @@ const App: FunctionComponent = () => {
-
-
+
+
@@ -50,8 +50,8 @@ const App: FunctionComponent = () => {
-
-
+
+
diff --git a/openmetadata-ui/src/main/resources/ui/src/assets/svg/ic-custom-logo.svg b/openmetadata-ui/src/main/resources/ui/src/assets/svg/ic-custom-logo.svg
new file mode 100644
index 00000000000..c6dcec024e3
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/assets/svg/ic-custom-logo.svg
@@ -0,0 +1,21 @@
+
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
index 23ec10aafc6..e621c4cdb71 100644
--- 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
@@ -12,26 +12,18 @@
*/
import { act, render, screen } from '@testing-library/react';
import React from 'react';
-import { getApplicationConfig } from 'rest/miscAPI';
+import { getCustomLogoConfig } from 'rest/settingConfigAPI';
import ApplicationConfigProvider, {
useApplicationConfigProvider,
} from './ApplicationConfigProvider';
const mockApplicationConfig = {
- logoConfig: {
- customLogoUrlPath: 'https://customlink.source',
-
- customMonogramUrlPath: 'https://customlink.source',
- },
- loginConfig: {
- maxLoginFailAttempts: 3,
- accessBlockTime: 600,
- jwtTokenExpiryTime: 3600,
- },
+ customLogoUrlPath: 'https://customlink.source',
+ customMonogramUrlPath: 'https://customlink.source',
};
-jest.mock('rest/miscAPI', () => ({
- getApplicationConfig: jest
+jest.mock('rest/settingConfigAPI', () => ({
+ getCustomLogoConfig: jest
.fn()
.mockImplementation(() => Promise.resolve(mockApplicationConfig)),
}));
@@ -55,9 +47,9 @@ describe('ApplicationConfigProvider', () => {
it('fetch the application config on mount and set in the context', async () => {
function TestComponent() {
- const { logoConfig } = useApplicationConfigProvider();
+ const { customLogoUrlPath } = useApplicationConfigProvider();
- return
{logoConfig?.customLogoUrlPath}
;
+ return
{customLogoUrlPath}
;
}
await act(async () => {
@@ -72,6 +64,6 @@ describe('ApplicationConfigProvider', () => {
await screen.findByText('https://customlink.source')
).toBeInTheDocument();
- expect(getApplicationConfig).toHaveBeenCalledTimes(1);
+ expect(getCustomLogoConfig).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
index 29364ec24af..74667e87856 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/ApplicationConfigProvider/ApplicationConfigProvider.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/ApplicationConfigProvider/ApplicationConfigProvider.tsx
@@ -10,7 +10,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import { ApplicationConfiguration } from 'generated/configuration/applicationConfiguration';
+import { LogoConfiguration } from 'generated/configuration/applicationConfiguration';
import React, {
createContext,
FC,
@@ -19,10 +19,10 @@ import React, {
useEffect,
useState,
} from 'react';
-import { getApplicationConfig } from 'rest/miscAPI';
+import { getCustomLogoConfig } from 'rest/settingConfigAPI';
-export const ApplicationConfigContext = createContext(
- {} as ApplicationConfiguration
+export const ApplicationConfigContext = createContext(
+ {} as LogoConfiguration
);
export const useApplicationConfigProvider = () =>
@@ -35,14 +35,17 @@ interface ApplicationConfigProviderProps {
const ApplicationConfigProvider: FC = ({
children,
}) => {
- const [applicationConfig, setApplicationConfig] =
- useState({} as ApplicationConfiguration);
+ const [applicationConfig, setApplicationConfig] = useState(
+ {} as LogoConfiguration
+ );
const fetchApplicationConfig = async () => {
try {
- const response = await getApplicationConfig();
+ const data = await getCustomLogoConfig();
- setApplicationConfig(response);
+ setApplicationConfig({
+ ...data,
+ });
} catch (error) {
// eslint-disable-next-line no-console
console.error(error);
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/BrandImage/BrandImage.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/BrandImage/BrandImage.test.tsx
new file mode 100644
index 00000000000..94a189eb5bf
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/common/BrandImage/BrandImage.test.tsx
@@ -0,0 +1,99 @@
+/*
+ * 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 { render, screen } from '@testing-library/react';
+import React from 'react';
+import BrandImage from './BrandImage';
+
+jest.mock(
+ 'components/ApplicationConfigProvider/ApplicationConfigProvider',
+ () => ({
+ useApplicationConfigProvider: jest.fn().mockImplementation(() => ({
+ customLogoUrlPath: 'https://custom-logo.png',
+ customMonogramUrlPath: 'https://custom-monogram.png',
+ })),
+ })
+);
+
+describe('Test Brand Logo', () => {
+ it('Should render the brand logo with default props value', () => {
+ render();
+
+ const image = screen.getByTestId('brand-logo-image');
+
+ expect(image).toBeInTheDocument();
+ expect(image).toHaveAttribute('alt', 'OpenMetadata Logo');
+ expect(image).toHaveAttribute('height', 'auto');
+ expect(image).toHaveAttribute('width', '152');
+ });
+
+ it('Should render the brand logo with passed props value', () => {
+ render(
+
+ );
+
+ const image = screen.getByTestId('brand-monogram');
+
+ expect(image).toBeInTheDocument();
+ expect(image).toHaveAttribute('alt', 'brand-monogram');
+ expect(image).toHaveAttribute('height', '30');
+ expect(image).toHaveAttribute('width', '30');
+ expect(image).toHaveClass('m-auto');
+ });
+
+ it('Should render the brand logo based on custom logo config', () => {
+ render(
+
+ );
+
+ const image = screen.getByTestId('brand-monogram');
+
+ expect(image).toBeInTheDocument();
+ expect(image).toHaveAttribute('alt', 'brand-monogram');
+ expect(image).toHaveAttribute('height', 'auto');
+ expect(image).toHaveAttribute('width', '152');
+ expect(image).toHaveAttribute('src', 'https://custom-logo.png');
+ expect(image).toHaveClass('m-auto');
+ });
+
+ it('Should render the monogram if isMonoGram is true', () => {
+ render(
+
+ );
+
+ const image = screen.getByTestId('brand-monogram');
+
+ expect(image).toBeInTheDocument();
+ expect(image).toHaveAttribute('alt', 'brand-monogram');
+ expect(image).toHaveAttribute('height', '30');
+ expect(image).toHaveAttribute('width', '30');
+ expect(image).toHaveAttribute('src', 'https://custom-monogram.png');
+ });
+});
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/BrandImage/BrandImage.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/BrandImage/BrandImage.tsx
new file mode 100644
index 00000000000..c852dc94bd1
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/common/BrandImage/BrandImage.tsx
@@ -0,0 +1,55 @@
+/*
+ * 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 MonoGram from 'assets/svg/logo-monogram.svg';
+import Logo from 'assets/svg/logo.svg';
+import { useApplicationConfigProvider } from 'components/ApplicationConfigProvider/ApplicationConfigProvider';
+import React, { FC } from 'react';
+
+interface BrandImageProps {
+ dataTestId?: string;
+ className?: string;
+ alt?: string;
+ width: number | string;
+ height: number | string;
+ isMonoGram?: boolean;
+}
+
+const BrandImage: FC = ({
+ dataTestId,
+ alt,
+ width,
+ height,
+ className,
+ isMonoGram = false,
+}) => {
+ const { customLogoUrlPath = '', customMonogramUrlPath = '' } =
+ useApplicationConfigProvider();
+
+ const logoSource = isMonoGram
+ ? customMonogramUrlPath || MonoGram
+ : customLogoUrlPath || Logo;
+
+ return (
+
+ );
+};
+
+export default BrandImage;
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 07ddb9e6996..5f36e8e3b35 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
@@ -22,7 +22,7 @@ import {
Tooltip,
} from 'antd';
import { ReactComponent as DropDownIcon } from 'assets/svg/DropDown.svg';
-import { useApplicationConfigProvider } from 'components/ApplicationConfigProvider/ApplicationConfigProvider';
+import BrandImage from 'components/common/BrandImage/BrandImage';
import { useGlobalSearchProvider } from 'components/GlobalSearchProvider/GlobalSearchProvider';
import WhatsNewAlert from 'components/Modals/WhatsNewModal/WhatsNewAlert/WhatsNewAlert.component';
import { CookieStorage } from 'cookie-storage';
@@ -95,7 +95,6 @@ const NavBar = ({
handleOnClick,
handleClear,
}: NavBarProps) => {
- const { logoConfig } = useApplicationConfigProvider();
const { searchCriteria, updateSearchCriteria } = useGlobalSearchProvider();
// get current user details
@@ -361,22 +360,18 @@ const NavBar = ({
[AppState]
);
- const brandLogoUrl = useMemo(() => {
- return logoConfig?.customMonogramUrlPath || Logo;
- }, [logoConfig]);
-
return (
<>
-
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/router/AuthenticatedAppRouter.tsx b/openmetadata-ui/src/main/resources/ui/src/components/router/AuthenticatedAppRouter.tsx
index 09dbeaa4f30..9bb0131fd19 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/router/AuthenticatedAppRouter.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/router/AuthenticatedAppRouter.tsx
@@ -196,6 +196,9 @@ const EditEmailConfigPage = withSuspenseFallback(
() => import('pages/EditEmailConfigPage/EditEmailConfigPage.component')
)
);
+const EditCustomLogoConfigPage = withSuspenseFallback(
+ React.lazy(() => import('pages/EditCustomLogoConfig/EditCustomLogoConfig'))
+);
const AddRulePage = withSuspenseFallback(
React.lazy(() => import('pages/PoliciesPage/PoliciesDetailPage/AddRulePage'))
@@ -548,6 +551,12 @@ const AuthenticatedAppRouter: FunctionComponent = () => {
hasPermission={false}
path={ROUTES.SETTINGS_EDIT_EMAIL_CONFIG}
/>
+
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/router/GlobalSettingRouter.tsx b/openmetadata-ui/src/main/resources/ui/src/components/router/GlobalSettingRouter.tsx
index c7a3cae112f..eedb8d07523 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/router/GlobalSettingRouter.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/router/GlobalSettingRouter.tsx
@@ -116,6 +116,12 @@ const EmailConfigSettingsPage = withSuspenseFallback(
import('pages/EmailConfigSettingsPage/EmailConfigSettingsPage.component')
)
);
+const CustomLogoConfigSettingsPage = withSuspenseFallback(
+ React.lazy(
+ () =>
+ import('pages/CustomLogoConfigSettingsPage/CustomLogoConfigSettingsPage')
+ )
+);
const GlobalSettingRouter = () => {
const { permissions } = usePermissionProvider();
@@ -248,6 +254,15 @@ const GlobalSettingRouter = () => {
GlobalSettingOptions.EMAIL
)}
/>
+ ({
+ getSettingsConfigFromConfigType: jest.fn().mockImplementation(() =>
+ Promise.resolve({
+ data: {
+ config_value: {
+ customLogoUrlPath: 'https://custom-logo.png',
+ customMonogramUrlPath: 'https://custom-monogram.png',
+ },
+ },
+ })
+ ),
+}));
+
+jest.mock('react-router-dom', () => ({
+ useHistory: jest.fn().mockImplementation(() => ({
+ push: mockPush,
+ })),
+}));
+
+describe('Test Custom Logo Config Page', () => {
+ it('Should render the config details', async () => {
+ render();
+
+ await waitForElementToBeRemoved(() => screen.getByTestId('loader'));
+
+ // page header
+ expect(screen.getByText('label.custom-logo')).toBeInTheDocument();
+ expect(
+ screen.getByText('message.custom-logo-configuration-message')
+ ).toBeInTheDocument();
+
+ expect(screen.getByTestId('edit-button')).toBeInTheDocument();
+
+ // card header
+ expect(
+ screen.getByText('label.custom-logo-configuration')
+ ).toBeInTheDocument();
+
+ // logo
+ expect(screen.getByText('label.logo-url')).toBeInTheDocument();
+ expect(screen.getByTestId('logo-url-info')).toBeInTheDocument();
+ expect(screen.getByTestId('logo-url')).toHaveTextContent(
+ 'https://custom-logo.png'
+ );
+
+ // monogram
+ expect(screen.getByText('label.monogram-url')).toBeInTheDocument();
+ expect(screen.getByTestId('monogram-url-info')).toBeInTheDocument();
+ expect(screen.getByTestId('monogram-url')).toHaveTextContent(
+ 'https://custom-monogram.png'
+ );
+ });
+
+ it('Should render the error placeholder if api fails', async () => {
+ (getSettingsConfigFromConfigType as jest.Mock).mockImplementationOnce(() =>
+ Promise.reject()
+ );
+ render();
+
+ await waitForElementToBeRemoved(() => screen.getByTestId('loader'));
+
+ expect(
+ screen.getByTestId('create-error-placeholder-label.custom-logo')
+ ).toBeInTheDocument();
+
+ expect(screen.getByTestId('add-placeholder-button')).toBeInTheDocument();
+ });
+
+ it('Edit button should work', async () => {
+ render();
+
+ await waitForElementToBeRemoved(() => screen.getByTestId('loader'));
+
+ const editButton = screen.getByTestId('edit-button');
+
+ expect(editButton).toBeInTheDocument();
+
+ userEvent.click(editButton);
+
+ expect(mockPush).toHaveBeenCalled();
+ });
+});
diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/CustomLogoConfigSettingsPage/CustomLogoConfigSettingsPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/CustomLogoConfigSettingsPage/CustomLogoConfigSettingsPage.tsx
new file mode 100644
index 00000000000..2c75c11e33b
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/pages/CustomLogoConfigSettingsPage/CustomLogoConfigSettingsPage.tsx
@@ -0,0 +1,168 @@
+/*
+ * 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 { InfoCircleOutlined } from '@ant-design/icons';
+import Icon from '@ant-design/icons/lib/components/Icon';
+import { Button, Card, Col, Row, Tooltip, Typography } from 'antd';
+import { ReactComponent as IconEdit } from 'assets/svg/edit-new.svg';
+import { AxiosError } from 'axios';
+import ErrorPlaceHolder from 'components/common/error-with-placeholder/ErrorPlaceHolder';
+import PageHeader from 'components/header/PageHeader.component';
+import Loader from 'components/Loader/Loader';
+import { GRAYED_OUT_COLOR, ROUTES } from 'constants/constants';
+import { CUSTOM_LOGO_DOCS } from 'constants/docs.constants';
+import { ERROR_PLACEHOLDER_TYPE } from 'enums/common.enum';
+import { LogoConfiguration } from 'generated/configuration/applicationConfiguration';
+import { SettingType } from 'generated/settings/settings';
+import { isEmpty, isUndefined } from 'lodash';
+import React, { useEffect, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { useHistory } from 'react-router-dom';
+import { getSettingsConfigFromConfigType } from 'rest/settingConfigAPI';
+import { showErrorToast } from 'utils/ToastUtils';
+
+const CustomLogoConfigSettingsPage = () => {
+ const { t } = useTranslation();
+ const history = useHistory();
+
+ const [loading, setLoading] = useState(false);
+ const [config, setConfig] = useState();
+
+ const fetchCustomLogoConfig = async () => {
+ try {
+ setLoading(true);
+
+ const { data } = await getSettingsConfigFromConfigType(
+ SettingType.CustomLogoConfiguration
+ );
+
+ setConfig(data.config_value as LogoConfiguration);
+ } catch (error) {
+ showErrorToast(error as AxiosError);
+ } finally {
+ setLoading(false);
+ }
+ };
+ const handleEditClick = () => {
+ history.push(ROUTES.SETTINGS_EDIT_CUSTOM_LOGO_CONFIG);
+ };
+
+ useEffect(() => {
+ fetchCustomLogoConfig();
+ }, []);
+
+ if (loading) {
+ return ;
+ }
+
+ if (isUndefined(config)) {
+ return (
+
+ );
+ }
+
+ return (
+
+