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 ( + {alt + ); +}; + +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 ( <>
- OpenMetadata Logo 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 ( + + + + + + + + + + + + + + <> + + {t('label.custom-logo-configuration')} + + + + + + + {t('label.logo-url')} + + + + + + + + {isEmpty(config?.customLogoUrlPath) + ? '--' + : config?.customLogoUrlPath} + + + + + + + + + {t('label.monogram-url')} + + + + + + + + {isEmpty(config?.customMonogramUrlPath) + ? '--' + : config?.customMonogramUrlPath} + + + + + + + + + + ); +}; + +export default CustomLogoConfigSettingsPage; diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/EditCustomLogoConfig/EditCustomLogoConfig.test.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/EditCustomLogoConfig/EditCustomLogoConfig.test.tsx new file mode 100644 index 00000000000..34feb0df82e --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/pages/EditCustomLogoConfig/EditCustomLogoConfig.test.tsx @@ -0,0 +1,138 @@ +/* + * 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, + waitForElementToBeRemoved, +} from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; +import { updateSettingsConfig } from 'rest/settingConfigAPI'; +import EditCustomLogoConfig from './EditCustomLogoConfig'; + +const mockPush = jest.fn(); +const mockGoBack = jest.fn(); + +jest.mock('react-router-dom', () => ({ + useHistory: jest.fn().mockImplementation(() => ({ + push: mockPush, + goBack: mockGoBack, + })), +})); + +jest.mock('rest/settingConfigAPI', () => ({ + getSettingsConfigFromConfigType: jest.fn().mockImplementation(() => + Promise.resolve({ + data: { + config_value: { + customLogoUrlPath: 'https://custom-logo.png', + customMonogramUrlPath: 'https://custom-monogram.png', + }, + }, + }) + ), + updateSettingsConfig: jest.fn().mockImplementation(() => Promise.resolve()), +})); + +jest.mock('components/common/title-breadcrumb/title-breadcrumb.component', () => + jest.fn().mockImplementation(() =>
BreadCrumb.component
) +); +jest.mock('components/common/ServiceDocPanel/ServiceDocPanel', () => + jest.fn().mockImplementation(() =>
ServiceDocPanel.component
) +); + +jest.mock('components/common/ResizablePanels/ResizablePanels', () => + jest.fn().mockImplementation(({ firstPanel, secondPanel }) => ( + <> +
{firstPanel.children}
+
{secondPanel.children}
+ + )) +); + +describe('Test Custom Logo Config Form', () => { + it('Should render the child components', async () => { + render(); + + await waitForElementToBeRemoved(() => screen.getByTestId('loader')); + + // breadcrumb + expect(screen.getByText('BreadCrumb.component')).toBeInTheDocument(); + + // service doc panel + expect(screen.getByText('ServiceDocPanel.component')).toBeInTheDocument(); + + // form + expect(screen.getByTestId('custom-logo-config-form')).toBeInTheDocument(); + }); + + it('Should render the form with default values', async () => { + render(); + + await waitForElementToBeRemoved(() => screen.getByTestId('loader')); + + // form + expect(screen.getByTestId('custom-logo-config-form')).toBeInTheDocument(); + + // logo url input + expect(screen.getByTestId('customLogoUrlPath')).toHaveValue( + 'https://custom-logo.png' + ); + + // monogram url input + expect(screen.getByTestId('customMonogramUrlPath')).toHaveValue( + 'https://custom-monogram.png' + ); + }); + + it('Cancel button should work', async () => { + render(); + + await waitForElementToBeRemoved(() => screen.getByTestId('loader')); + + const cancelButton = screen.getByTestId('cancel-button'); + + userEvent.click(cancelButton); + + expect(mockGoBack).toHaveBeenCalled(); + }); + + it('Save button should work', async () => { + render(); + + await waitForElementToBeRemoved(() => screen.getByTestId('loader')); + + const logoInput = screen.getByTestId('customLogoUrlPath'); + const monogramInput = screen.getByTestId('customMonogramUrlPath'); + + await act(async () => { + userEvent.type(logoInput, 'https://custom-logo-1.png'); + userEvent.type(monogramInput, 'https://custom-monogram-1.png'); + }); + + const saveButton = screen.getByTestId('save-button'); + + await act(async () => { + userEvent.click(saveButton); + }); + + expect(updateSettingsConfig).toHaveBeenCalledWith({ + config_type: 'customLogoConfiguration', + config_value: { + customLogoUrlPath: 'https://custom-logo-1.png', + customMonogramUrlPath: 'https://custom-monogram-1.png', + }, + }); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/EditCustomLogoConfig/EditCustomLogoConfig.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/EditCustomLogoConfig/EditCustomLogoConfig.tsx new file mode 100644 index 00000000000..b96ed702dcc --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/pages/EditCustomLogoConfig/EditCustomLogoConfig.tsx @@ -0,0 +1,226 @@ +/* + * 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 { Button, Card, Col, Form, Row } from 'antd'; +import { AxiosError } from 'axios'; +import ResizablePanels from 'components/common/ResizablePanels/ResizablePanels'; +import ServiceDocPanel from 'components/common/ServiceDocPanel/ServiceDocPanel'; +import TitleBreadcrumb from 'components/common/title-breadcrumb/title-breadcrumb.component'; +import PageContainerV1 from 'components/containers/PageContainerV1'; +import Loader from 'components/Loader/Loader'; +import { + GlobalSettingOptions, + GlobalSettingsMenuCategory, +} from 'constants/GlobalSettings.constants'; +import { + CUSTOM_LOGO_CONFIG_SERVICE_CATEGORY, + OPEN_METADATA, +} from 'constants/service-guide.constant'; +import { ServiceCategory } from 'enums/service.enum'; +import { LogoConfiguration } from 'generated/configuration/applicationConfiguration'; +import { Settings, SettingType } from 'generated/settings/settings'; +import React, { useEffect, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useHistory } from 'react-router-dom'; +import { + getSettingsConfigFromConfigType, + updateSettingsConfig, +} from 'rest/settingConfigAPI'; +import { FieldProp, FieldTypes, generateFormFields } from 'utils/formUtils'; +import { getSettingPath } from 'utils/RouterUtils'; +import { showErrorToast, showSuccessToast } from 'utils/ToastUtils'; + +const EditCustomLogoConfig = () => { + const { t } = useTranslation(); + const history = useHistory(); + const [form] = Form.useForm(); + const [activeField, setActiveField] = useState(''); + const [loading, setLoading] = useState(false); + const [updating, setUpdating] = useState(false); + + const fetchCustomLogoConfig = async () => { + try { + setLoading(true); + + const { data } = await getSettingsConfigFromConfigType( + SettingType.CustomLogoConfiguration + ); + + form.setFieldsValue({ ...(data.config_value ?? {}) }); + } catch (error) { + showErrorToast(error as AxiosError); + } finally { + setLoading(false); + } + }; + + const breadcrumb = useMemo( + () => [ + { + name: t('label.setting-plural'), + url: getSettingPath(), + }, + { + name: t('label.custom-logo'), + url: getSettingPath( + GlobalSettingsMenuCategory.OPEN_METADATA, + GlobalSettingOptions.CUSTOM_LOGO + ), + }, + { + name: t('label.edit-entity', { + entity: t('label.custom-logo-configuration'), + }), + url: '', + }, + ], + [] + ); + + const formFields: FieldProp[] = [ + { + name: 'customLogoUrlPath', + label: t('label.logo-url'), + type: FieldTypes.TEXT, + required: false, + id: 'root/customLogoUrlPath', + props: { + 'data-testid': 'customLogoUrlPath', + }, + rules: [ + { + type: 'url', + message: t('message.entity-is-not-valid-url', { + entity: t('label.logo-url'), + }), + }, + ], + }, + { + name: 'customMonogramUrlPath', + label: t('label.monogram-url'), + type: FieldTypes.TEXT, + required: false, + id: 'root/customMonogramUrlPath', + props: { + 'data-testid': 'customMonogramUrlPath', + }, + rules: [ + { + type: 'url', + message: t('message.entity-is-not-valid-url', { + entity: t('label.monogram-url'), + }), + }, + ], + }, + ]; + + const handleGoBack = () => history.goBack(); + + const handleSubmit = async (configValues: LogoConfiguration) => { + try { + setUpdating(true); + const configData = { + config_type: SettingType.CustomLogoConfiguration, + config_value: configValues, + }; + await updateSettingsConfig(configData as Settings); + showSuccessToast( + t('server.update-entity-success', { + entity: t('label.custom-logo-configuration'), + }) + ); + handleGoBack(); + } catch (error) { + showErrorToast(error as AxiosError); + } finally { + setUpdating(false); + } + }; + + const firstPanelChildren = ( +
+ + +
{ + e.preventDefault(); + e.stopPropagation(); + setActiveField(e.target.id); + }}> + {generateFormFields(formFields)} + + + + + + + + +
+
+
+ ); + + const secondPanelChildren = ( + + ); + + useEffect(() => { + fetchCustomLogoConfig(); + }, []); + + if (loading) { + return ; + } + + return ( + + + + ); +}; + +export default EditCustomLogoConfig; diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/EditEmailConfigPage/EditEmailConfigPage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/EditEmailConfigPage/EditEmailConfigPage.component.tsx index 5d5c42a25eb..cea01296c7e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/EditEmailConfigPage/EditEmailConfigPage.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/EditEmailConfigPage/EditEmailConfigPage.component.tsx @@ -41,7 +41,7 @@ import { useHistory } from 'react-router-dom'; import { getSettingsConfigFromConfigType, updateSettingsConfig, -} from 'rest/emailConfigAPI'; +} from 'rest/settingConfigAPI'; import { getSettingPath } from 'utils/RouterUtils'; import { showErrorToast, showSuccessToast } from 'utils/ToastUtils'; diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/EmailConfigSettingsPage/EmailConfigSettingsPage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/EmailConfigSettingsPage/EmailConfigSettingsPage.component.tsx index 3a7ae74e760..1ca5a4b0adb 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/EmailConfigSettingsPage/EmailConfigSettingsPage.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/EmailConfigSettingsPage/EmailConfigSettingsPage.component.tsx @@ -24,7 +24,7 @@ import { isBoolean, isEmpty, isNumber, isUndefined } from 'lodash'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useHistory } from 'react-router-dom'; -import { getSettingsConfigFromConfigType } from 'rest/emailConfigAPI'; +import { getSettingsConfigFromConfigType } from 'rest/settingConfigAPI'; import { getEmailConfigFieldLabels } from 'utils/EmailConfigUtils'; import { showErrorToast } from 'utils/ToastUtils'; import { ReactComponent as IconEdit } from '../../assets/svg/edit-new.svg'; diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/forgot-password/forgot-password.component.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/forgot-password/forgot-password.component.tsx index 11c6fe6e55c..77037a0a822 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/forgot-password/forgot-password.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/forgot-password/forgot-password.component.tsx @@ -13,6 +13,7 @@ import { Button, Card, Col, Divider, Form, Input, Row, Typography } from 'antd'; import { useBasicAuth } from 'components/authentication/auth-provider/basic-auth.provider'; +import BrandImage from 'components/common/BrandImage/BrandImage'; import React, { useCallback, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useHistory } from 'react-router-dom'; @@ -51,11 +52,7 @@ const ForgotPassword = () => { style={{ maxWidth: '430px' }}> - + 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 cd323dd4918..778728e2673 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 @@ -35,11 +35,9 @@ jest.mock( 'components/ApplicationConfigProvider/ApplicationConfigProvider', () => ({ useApplicationConfigProvider: jest.fn().mockImplementation(() => ({ - logoConfig: { - customLogoUrlPath: 'https://customlink.source', + customLogoUrlPath: 'https://customlink.source', - customMonogramUrlPath: 'https://customlink.source', - }, + customMonogramUrlPath: 'https://customlink.source', })), }) ); @@ -142,10 +140,9 @@ describe('Test SigninPage Component', () => { }); 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/'); + expect(brandLogoImage).toHaveAttribute('src', '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 38635810e6a..a6fecd0522c 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,21 +11,11 @@ * limitations under the License. */ -import { - Button, - Col, - Divider, - Form, - Image, - Input, - Row, - Typography, -} from 'antd'; -import Logo from 'assets/svg/logo.svg'; +import { Button, Col, Divider, Form, Input, Row, Typography } from 'antd'; 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 BrandImage from 'components/common/BrandImage/BrandImage'; import Loader from 'components/Loader/Loader'; import LoginButton from 'components/LoginButton/LoginButton'; import jwtDecode, { JwtPayload } from 'jwt-decode'; @@ -42,7 +32,6 @@ import './login.style.less'; import LoginCarousel from './LoginCarousel'; const SigninPage = () => { - const { logoConfig } = useApplicationConfigProvider(); const [loading, setLoading] = useState(false); const [form] = Form.useForm(); @@ -77,10 +66,6 @@ const SigninPage = () => { return isAuthDisabled || isAuthenticated; }, [isAuthDisabled, isAuthenticated]); - const brandLogoUrl = useMemo(() => { - return logoConfig?.customLogoUrlPath ?? Logo; - }, [logoConfig]); - const isTokenExpired = () => { const token = localState.getOidcToken(); if (token) { @@ -216,14 +201,7 @@ 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/pages/reset-password/reset-password.component.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/reset-password/reset-password.component.tsx index e9cad97abcd..139b2434b84 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/reset-password/reset-password.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/reset-password/reset-password.component.tsx @@ -14,13 +14,13 @@ import { Alert, Button, Card, Col, Form, Input, Row, Typography } from 'antd'; import { AxiosError } from 'axios'; import { useBasicAuth } from 'components/authentication/auth-provider/basic-auth.provider'; +import BrandImage from 'components/common/BrandImage/BrandImage'; import React, { useEffect, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useHistory, useLocation } from 'react-router-dom'; import { ROUTES, VALIDATION_MESSAGES } from '../../constants/constants'; import { passwordRegex } from '../../constants/regex.constants'; import { PasswordResetRequest } from '../../generated/auth/passwordResetRequest'; -import SVGIcons, { Icons } from '../../utils/SvgUtils'; import { showErrorToast } from '../../utils/ToastUtils'; import './reset-password.style.less'; import { getUserNameAndToken } from './reset-password.utils'; @@ -97,7 +97,7 @@ const ResetPassword = () => { style={{ maxWidth: '450px' }}> - + diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/signup/basic-signup.component.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/signup/basic-signup.component.tsx index 1393897247e..8e9f58466a0 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/signup/basic-signup.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/signup/basic-signup.component.tsx @@ -14,6 +14,7 @@ import { Button, Col, Divider, Form, Input, Row, Typography } from 'antd'; import { useAuthContext } from 'components/authentication/auth-provider/AuthProvider'; import { useBasicAuth } from 'components/authentication/auth-provider/basic-auth.provider'; +import BrandImage from 'components/common/BrandImage/BrandImage'; import { isEmpty } from 'lodash'; import React, { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -22,7 +23,6 @@ import loginBG from '../../assets/img/login-bg.png'; import { ROUTES, VALIDATION_MESSAGES } from '../../constants/constants'; import { passwordRegex } from '../../constants/regex.constants'; import { AuthTypes } from '../../enums/signin.enum'; -import SVGIcons, { Icons } from '../../utils/SvgUtils'; import LoginCarousel from '../login/LoginCarousel'; import './../login/login.style.less'; @@ -70,7 +70,7 @@ const BasicSignUp = () => {
- + {t('message.om-description')} diff --git a/openmetadata-ui/src/main/resources/ui/src/rest/emailConfigAPI.ts b/openmetadata-ui/src/main/resources/ui/src/rest/settingConfigAPI.ts similarity index 80% rename from openmetadata-ui/src/main/resources/ui/src/rest/emailConfigAPI.ts rename to openmetadata-ui/src/main/resources/ui/src/rest/settingConfigAPI.ts index b38ef19cad9..acf3c1d1c23 100644 --- a/openmetadata-ui/src/main/resources/ui/src/rest/emailConfigAPI.ts +++ b/openmetadata-ui/src/main/resources/ui/src/rest/settingConfigAPI.ts @@ -12,6 +12,7 @@ */ import { AxiosResponse } from 'axios'; +import { LogoConfiguration } from 'generated/configuration/applicationConfiguration'; import { Settings, SettingType } from 'generated/settings/settings'; import axiosClient from 'rest'; @@ -30,3 +31,11 @@ export const updateSettingsConfig = async (payload: Settings) => { return response; }; + +export const getCustomLogoConfig = async () => { + const response = await axiosClient.get( + `system/config/customLogoConfiguration` + ); + + return response.data; +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/GlobalSettingsUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/GlobalSettingsUtils.tsx index ce36b06c59d..80de8bd0eb7 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/GlobalSettingsUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/GlobalSettingsUtils.tsx @@ -38,6 +38,7 @@ import { ReactComponent as TableIcon } from '../../src/assets/svg/table-grey.svg import { ReactComponent as TeamsIcon } from '../../src/assets/svg/teams-grey.svg'; import { ReactComponent as TopicIcon } from '../../src/assets/svg/topic-grey.svg'; import { ReactComponent as UsersIcon } from '../../src/assets/svg/user.svg'; +import { ReactComponent as CustomLogoIcon } from '../assets/svg/ic-custom-logo.svg'; import { ReactComponent as StorageIcon } from '../assets/svg/ic-storage.svg'; import { userPermissions } from '../utils/PermissionsUtils'; @@ -301,6 +302,12 @@ export const getGlobalSettingsMenuWithPermission = ( key: 'openMetadata.email', icon: , }, + { + label: i18next.t('label.custom-logo'), + isProtected: Boolean(isAdminUser), + key: 'openMetadata.customLogo', + icon: , + }, ], }, {