feat(#11596): add support for custom logo config in settings (#11597)

* feat(#11596): add support for custom logo config in settings

* fix: edit custom logo config

* chore: update application config provider

* fix: unit test

* fate: create separate brand logo component

* Add Accessible Logo Config From ConfigResource

* update custom logo api

* minor change

* fix: styling and use brand logo component

* fix(#11503) Custom Login Logo does not change after adding new custom logo

* restored delete files

* doc: update custom logo config doc

* update the icon

* test: add unit test for brand logo

* test: add unit test for custom logo config setting page

* test: add unit test for custom logo config form

* test: fix cy test

* fix: url input validation

* test: add cypress test

* test: update cy test to check for validation

* update the doc

* test: fix cypress test

* chore: make height and width required in BrandLogo component

* chore: update component name

---------

Co-authored-by: mohitdeuex <mohit.y@deuexsolutions.com>
This commit is contained in:
Sachin Chaurasiya 2023-05-17 11:26:34 +05:30 committed by GitHub
parent c40c5dc478
commit 4df64ef6fe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 1133 additions and 93 deletions

View File

@ -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(

View File

@ -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);
});
});

View File

@ -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
$$

View File

@ -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.
<br/>
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.
<br/>
<br/>
**SMTP:**- If SMTP port is 25 use this
<br/>
**SMTPS:**- If SMTP port is 465 use this
<br/>
**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`.
<br/>
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.
<br/>
If you have your internal channels / groups this can be updated here.
<br/>
Default: `https://slack.open-metadata.org`.
$$
@ -87,6 +86,6 @@ $$section
### Transportation strategy $(id="transportationStrategy")
Possible values: `SMTP`, `SMTPS`, `SMTP_TLS`. <br/> Depends as per the `port` above.
Possible values: `SMTP`, `SMTPS`, `SMTP_TLS`. Depends as per the `port` above.
$$

View File

@ -36,8 +36,8 @@ const App: FunctionComponent = () => {
<Router>
<I18nextProvider i18n={i18n}>
<ErrorBoundry>
<AuthProvider childComponentType={AppRouter}>
<ApplicationConfigProvider>
<ApplicationConfigProvider>
<AuthProvider childComponentType={AppRouter}>
<HelmetProvider>
<WebAnalyticsProvider>
<PermissionProvider>
@ -50,8 +50,8 @@ const App: FunctionComponent = () => {
</PermissionProvider>
</WebAnalyticsProvider>
</HelmetProvider>
</ApplicationConfigProvider>
</AuthProvider>
</AuthProvider>
</ApplicationConfigProvider>
</ErrorBoundry>
</I18nextProvider>
</Router>

View File

@ -0,0 +1,21 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1032_4367)">
<path d="M7.1997 8.06164C7.04228 8.19582 6.93792 8.37732 6.90472 8.5747C6.87151 8.77208 6.91155 8.97288 7.0179 9.14229C7.12424 9.3117 7.29018 9.43905 7.48696 9.50226C7.68374 9.56546 7.89894 9.56053 8.09526 9.48834C8.29158 9.41614 8.45664 9.28121 8.56183 9.10696C8.66702 8.9327 8.7057 8.7301 8.67118 8.53428C8.63665 8.33845 8.56434 8.17149 8.40605 8.04455" stroke="currentColor" stroke-width="0.7"/>
<path d="M13.9062 6.89062C13.8333 5.46875 12.725 2.4625 8.875 1.8125" stroke="currentColor" stroke-width="0.7"/>
<rect x="13.25" y="6.6875" width="1.75" height="1.625" fill="currentColor" stroke="currentColor" stroke-width="0.7"/>
<rect x="1" y="6.6875" width="1.75" height="1.625" stroke="currentColor" stroke-width="0.7"/>
<path d="M2.75 1.8125H13.0312" stroke="currentColor" stroke-width="0.7"/>
<ellipse cx="2.75" cy="1.8125" rx="0.875" ry="0.8125" fill="currentColor" stroke="currentColor" stroke-width="0.7"/>
<ellipse cx="13.6875" cy="1.8125" rx="0.875" ry="0.8125" fill="currentColor" stroke="currentColor" stroke-width="0.7"/>
<path d="M5.375 15.625V13.5938H10.1875V15.625" stroke="currentColor" stroke-width="0.7" stroke-linejoin="round"/>
<rect x="4.5875" y="12.025" width="6.3875" height="1.91875" fill="currentColor" stroke="currentColor" stroke-width="0.7" stroke-linejoin="round"/>
<path d="M1.875 6.89062C1.94792 5.40104 3.05625 2.3 6.90625 1.8125" stroke="currentColor" stroke-width="0.7" stroke-linejoin="round"/>
<rect x="7.125" y="1" width="1.75" height="1.625" fill="currentColor" stroke="currentColor" stroke-width="0.7"/>
<path d="M7.34375 8.10938V3.84375L4.5 9.9375L5.8125 12.1719H9.75L11.0625 9.9375L8.4375 3.84375V8.10938" stroke="currentColor" stroke-width="0.7" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_1032_4367">
<rect width="16" height="16" fill="currentColor"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -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 <div>{logoConfig?.customLogoUrlPath}</div>;
return <div>{customLogoUrlPath}</div>;
}
await act(async () => {
@ -72,6 +64,6 @@ describe('ApplicationConfigProvider', () => {
await screen.findByText('https://customlink.source')
).toBeInTheDocument();
expect(getApplicationConfig).toHaveBeenCalledTimes(1);
expect(getCustomLogoConfig).toHaveBeenCalledTimes(1);
});
});

View File

@ -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<ApplicationConfiguration>(
{} as ApplicationConfiguration
export const ApplicationConfigContext = createContext<LogoConfiguration>(
{} as LogoConfiguration
);
export const useApplicationConfigProvider = () =>
@ -35,14 +35,17 @@ interface ApplicationConfigProviderProps {
const ApplicationConfigProvider: FC<ApplicationConfigProviderProps> = ({
children,
}) => {
const [applicationConfig, setApplicationConfig] =
useState<ApplicationConfiguration>({} as ApplicationConfiguration);
const [applicationConfig, setApplicationConfig] = useState<LogoConfiguration>(
{} 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);

View File

@ -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(<BrandImage height="auto" width={152} />);
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(
<BrandImage
alt="brand-monogram"
className="m-auto"
dataTestId="brand-monogram"
height={30}
width={30}
/>
);
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(
<BrandImage
alt="brand-monogram"
className="m-auto"
dataTestId="brand-monogram"
height="auto"
width={152}
/>
);
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(
<BrandImage
isMonoGram
alt="brand-monogram"
dataTestId="brand-monogram"
height={30}
width={30}
/>
);
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');
});
});

View File

@ -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<BrandImageProps> = ({
dataTestId,
alt,
width,
height,
className,
isMonoGram = false,
}) => {
const { customLogoUrlPath = '', customMonogramUrlPath = '' } =
useApplicationConfigProvider();
const logoSource = isMonoGram
? customMonogramUrlPath || MonoGram
: customLogoUrlPath || Logo;
return (
<img
alt={alt ?? 'OpenMetadata Logo'}
className={className}
data-testid={dataTestId ?? 'brand-logo-image'}
height={height}
id="brand-image"
src={logoSource}
width={width}
/>
);
};
export default BrandImage;

View File

@ -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 (
<>
<div className="tw-h-16 tw-py-3 tw-border-b-2 tw-border-separator tw-bg-white">
<div className="tw-flex tw-items-center tw-flex-row tw-justify-between tw-flex-nowrap tw-px-6">
<div className="tw-flex tw-items-center tw-flex-row tw-justify-between tw-flex-nowrap">
<Link className="tw-flex-shrink-0" id="openmetadata_logo" to="/">
<img
<BrandImage
isMonoGram
alt="OpenMetadata Logo"
className="vertical-middle"
data-testid="image"
dataTestId="image"
height={30}
src={brandLogoUrl}
width={30}
/>
</Link>

View File

@ -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}
/>
<AdminProtectedRoute
exact
component={EditCustomLogoConfigPage}
hasPermission={false}
path={ROUTES.SETTINGS_EDIT_CUSTOM_LOGO_CONFIG}
/>
<Route exact component={EditRulePage} path={ROUTES.EDIT_POLICY_RULE} />
<Route exact component={GlobalSettingPage} path={ROUTES.SETTINGS} />

View File

@ -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
)}
/>
<AdminProtectedRoute
exact
component={CustomLogoConfigSettingsPage}
hasPermission={false}
path={getSettingPath(
GlobalSettingsMenuCategory.OPEN_METADATA,
GlobalSettingOptions.CUSTOM_LOGO
)}
/>
<Route
exact

View File

@ -53,6 +53,7 @@ export enum GlobalSettingOptions {
DATA_INSIGHT_REPORT_ALERT = 'dataInsightReport',
ADD_DATA_INSIGHT_REPORT_ALERT = 'add-data-insight-report',
EDIT_DATA_INSIGHT_REPORT_ALERT = 'edit-data-insight-report',
CUSTOM_LOGO = 'customLogo',
}
export const GLOBAL_SETTING_PERMISSION_RESOURCES = [

View File

@ -292,6 +292,8 @@ export const ROUTES = {
CONTAINER_DETAILS: `/container/${PLACEHOLDER_ROUTE_ENTITY_FQN}`,
CONTAINER_DETAILS_WITH_TAB: `/container/${PLACEHOLDER_ROUTE_ENTITY_FQN}/${PLACEHOLDER_ROUTE_TAB}`,
SETTINGS_EDIT_CUSTOM_LOGO_CONFIG: `/settings/OpenMetadata/customLogo/edit-custom-logo-configuration`,
};
export const SOCKET_EVENTS = {

View File

@ -70,3 +70,6 @@ export const TAGS_DOCS =
'https://docs.open-metadata.org/main-concepts/metadata-standard/schemas/api/tags';
export const AIRFLOW_DOCS = 'https://docs.open-metadata.org/deployment/airflow';
export const CUSTOM_LOGO_DOCS =
'https://docs.open-metadata.org/how-to-guides/how-to-add-custom-logo';

View File

@ -185,4 +185,5 @@ export const addDBTIngestionGuide = [
];
export const EMAIL_CONFIG_SERVICE_CATEGORY = 'EmailConfiguration';
export const CUSTOM_LOGO_CONFIG_SERVICE_CATEGORY = 'CustomLogoConfiguration';
export const OPEN_METADATA = 'OpenMetadata';

View File

@ -162,6 +162,8 @@
"criteria": "Criteria",
"custom-attribute-plural": "Custom Attributes",
"custom-entity": "Custom entity",
"custom-logo": "Custom Logo",
"custom-logo-configuration": "Custom Logo Configuration",
"custom-oidc": "CustomOidc",
"custom-property": "Custom property",
"custom-property-plural": "Custom Properties",
@ -474,6 +476,7 @@
"log-plural": "Logs",
"logged-in-user-lowercase": "logged-in user",
"login": "Login",
"logo-url": "Logo URL",
"logout": "Logout",
"major": "Major",
"manage-entity": "Manage {{entity}}",
@ -521,6 +524,7 @@
"model-plural": "Models",
"model-store": "Model Store",
"monday": "Monday",
"monogram-url": "Monogram URL",
"month": "Month",
"more": "More",
"more-help": "More Help",
@ -1029,6 +1033,9 @@
"create-or-update-email-account-for-bot": "Changing the account email will update or create a new bot user.",
"created-this-task-lowercase": "created this task",
"custom-classification-name-dbt-tags": "Custom OpenMetadata Classification name for dbt tags ",
"custom-logo-configuration-message": "Configure The Application Logo and Monogram.",
"custom-logo-url-path-message": "URL path for the login page logo.",
"custom-monogram-url-path-message": "URL path for the navbar logo.",
"data-asset-has-been-action-type": "Data Asset has been {{actionType}}",
"data-insight-alert-destination-description": "Send email notifications to admins or teams.",
"data-insight-alert-trigger-description": "Trigger for real time or schedule it for daily, weekly or monthly.",
@ -1089,6 +1096,7 @@
"entity-does-not-have-followers": "{{entityName}} doesn't have any followers yet",
"entity-ingestion-added-successfully": "{{entity}} Ingestion Added Successfully",
"entity-is-not-valid": "{{entity}} is not valid",
"entity-is-not-valid-url": "{{entity}} is not valid url",
"entity-maximum-size": "{{entity}} can be a maximum of {{max}} characters",
"entity-not-contain-whitespace": "{{entity}} should not contain white space",
"entity-owned-by-name": "This entity is owned by {{entityOwner}}",

View File

@ -162,6 +162,8 @@
"criteria": "Criterio",
"custom-attribute-plural": "Atributos personalizados",
"custom-entity": "Custom entity",
"custom-logo": "Custom Logo",
"custom-logo-configuration": "Custom Logo Configuration",
"custom-oidc": "OIDC personalizado",
"custom-property": "Propiedad personalizada",
"custom-property-plural": "Propiedades personalizadas",
@ -474,6 +476,7 @@
"log-plural": "Registros",
"logged-in-user-lowercase": "usuario conectado",
"login": "Iniciar sesión",
"logo-url": "Logo URL",
"logout": "Cerrar sesión",
"major": "Principal",
"manage-entity": "Administrar {{entity}}",
@ -521,6 +524,7 @@
"model-plural": "Models",
"model-store": "Almacenamiento de Modelos",
"monday": "Lunes",
"monogram-url": "Monogram URL",
"month": "Mes",
"more": "Más",
"more-help": "Más Ayuda",
@ -1029,6 +1033,9 @@
"create-or-update-email-account-for-bot": "Cambiar el correo electrónico de la cuenta actualizará o creará un nuevo bot.",
"created-this-task-lowercase": "creó esta tarea",
"custom-classification-name-dbt-tags": "Nombre personalizado de clasificación de OpenMetadata para tags de dbt",
"custom-logo-configuration-message": "Configure The Application Logo and Monogram.",
"custom-logo-url-path-message": "URL path for the login page logo.",
"custom-monogram-url-path-message": "URL path for the navbar logo.",
"data-asset-has-been-action-type": "El activo de datos ha sido {{actionType}}",
"data-insight-alert-destination-description": "Send email notifications to admins or teams.",
"data-insight-alert-trigger-description": "Trigger for real time or schedule it for daily, weekly or monthly.",
@ -1089,6 +1096,7 @@
"entity-does-not-have-followers": "{{entityName}} aún no tiene seguidores",
"entity-ingestion-added-successfully": "{{entity}} Ingestión agregada exitosamente",
"entity-is-not-valid": "{{entity}} no es válido",
"entity-is-not-valid-url": "{{entity}} is not valid url",
"entity-maximum-size": "{{entity}} can be a maximum of {{max}} characters",
"entity-not-contain-whitespace": "{{entity}} no debe contener espacios en blanco",
"entity-owned-by-name": "Esta entidad es propiedad de {{entityOwner}}",

View File

@ -162,6 +162,8 @@
"criteria": "Critères",
"custom-attribute-plural": "Propriétés Personalisées",
"custom-entity": "Entité Personalisées",
"custom-logo": "Custom Logo",
"custom-logo-configuration": "Custom Logo Configuration",
"custom-oidc": "CustomOidc",
"custom-property": "Propriétés Personalisées",
"custom-property-plural": "Propriétés Personalisées",
@ -474,6 +476,7 @@
"log-plural": "Journal",
"logged-in-user-lowercase": "Utilisateur Connecté",
"login": "Se Connecter",
"logo-url": "Logo URL",
"logout": "Se Déconnecter",
"major": "Major",
"manage-entity": "Gérer {{entity}}",
@ -521,6 +524,7 @@
"model-plural": "Modèles",
"model-store": "Magasin de Modèles",
"monday": "Lundi",
"monogram-url": "Monogram URL",
"month": "Mois",
"more": "Plus",
"more-help": "Plus d'Aide",
@ -1029,6 +1033,9 @@
"create-or-update-email-account-for-bot": "Changer l'email créera un nouveau ou mettra à jour l'agent numérique",
"created-this-task-lowercase": "a créé cette tâche",
"custom-classification-name-dbt-tags": "Nom personnalisé de la classification OpenMetadata pour les tags dbt ",
"custom-logo-configuration-message": "Configure The Application Logo and Monogram.",
"custom-logo-url-path-message": "URL path for the login page logo.",
"custom-monogram-url-path-message": "URL path for the navbar logo.",
"data-asset-has-been-action-type": "l'actif de donnée a été {{actionType}}",
"data-insight-alert-destination-description": "Send email notifications to admins or teams.",
"data-insight-alert-trigger-description": "Trigger for real time or schedule it for daily, weekly or monthly.",
@ -1089,6 +1096,7 @@
"entity-does-not-have-followers": "{{entityName}} n'a pas de followers pour l'instant",
"entity-ingestion-added-successfully": "{{entity}} Ingestion ajouté avec succès",
"entity-is-not-valid": "{{entity}} n'est pas valide",
"entity-is-not-valid-url": "{{entity}} is not valid url",
"entity-maximum-size": "{{entity}} peut avoir un nombre maximum de characters de {{max}}",
"entity-not-contain-whitespace": "{{entity}} ne doit pas contenir d'espace",
"entity-owned-by-name": "Cette Resource appartient à {{entityOwner}}",

View File

@ -162,6 +162,8 @@
"criteria": "クライテリア",
"custom-attribute-plural": "カスタム属性",
"custom-entity": "Custom entity",
"custom-logo": "Custom Logo",
"custom-logo-configuration": "Custom Logo Configuration",
"custom-oidc": "CustomOidc",
"custom-property": "カスタムプロパティ",
"custom-property-plural": "カスタムプロパティ",
@ -474,6 +476,7 @@
"log-plural": "ログ",
"logged-in-user-lowercase": "logged-in user",
"login": "ログイン",
"logo-url": "Logo URL",
"logout": "ログアウト",
"major": "メジャー",
"manage-entity": "{{entity}}の管理",
@ -521,6 +524,7 @@
"model-plural": "Models",
"model-store": "モデルストア",
"monday": "月曜日",
"monogram-url": "Monogram URL",
"month": "月",
"more": "More",
"more-help": "より詳細なヘルプ",
@ -1029,6 +1033,9 @@
"create-or-update-email-account-for-bot": "Changing the account email will update or create a new bot user.",
"created-this-task-lowercase": "このタスクを作成する",
"custom-classification-name-dbt-tags": "Custom OpenMetadata Classification name for dbt tags ",
"custom-logo-configuration-message": "Configure The Application Logo and Monogram.",
"custom-logo-url-path-message": "URL path for the login page logo.",
"custom-monogram-url-path-message": "URL path for the navbar logo.",
"data-asset-has-been-action-type": "データアセットが{{actionType}}されました",
"data-insight-alert-destination-description": "Send email notifications to admins or teams.",
"data-insight-alert-trigger-description": "Trigger for real time or schedule it for daily, weekly or monthly.",
@ -1089,6 +1096,7 @@
"entity-does-not-have-followers": "{{entityName}}はフォロワーがいません",
"entity-ingestion-added-successfully": "{{entity}}から抽出した情報は正常に追加されました",
"entity-is-not-valid": "{{entity}}は正しくありません",
"entity-is-not-valid-url": "{{entity}} is not valid url",
"entity-maximum-size": "{{entity}} can be a maximum of {{max}} characters",
"entity-not-contain-whitespace": "{{entity}}は空白を含んではいけません",
"entity-owned-by-name": "このエンティティは{{entityOwner}}が所有しています",

View File

@ -162,6 +162,8 @@
"criteria": "Critério",
"custom-attribute-plural": "Atributos personalizados",
"custom-entity": "Custom entity",
"custom-logo": "Custom Logo",
"custom-logo-configuration": "Custom Logo Configuration",
"custom-oidc": "OIDC customizado",
"custom-property": "Propriedade customizada",
"custom-property-plural": "Propriedades customizadas",
@ -474,6 +476,7 @@
"log-plural": "Logs",
"logged-in-user-lowercase": "conectar com usuário",
"login": "Entrar",
"logo-url": "Logo URL",
"logout": "Sair",
"major": "Principal",
"manage-entity": "Gerenciar {{numberOfDays}}",
@ -521,6 +524,7 @@
"model-plural": "Models",
"model-store": "Estoque modelo",
"monday": "Segunda-feira",
"monogram-url": "Monogram URL",
"month": "Mês",
"more": "Mais",
"more-help": "Mais ajuda",
@ -1029,6 +1033,9 @@
"create-or-update-email-account-for-bot": "Alterar o e-mail da conta atualizará ou criará um novo usuário de bot.",
"created-this-task-lowercase": "criou esta tarefa",
"custom-classification-name-dbt-tags": "Nome personalizado da Classificação OpenMetadata para tags dbt",
"custom-logo-configuration-message": "Configure The Application Logo and Monogram.",
"custom-logo-url-path-message": "URL path for the login page logo.",
"custom-monogram-url-path-message": "URL path for the navbar logo.",
"data-asset-has-been-action-type": "O ativo de dados foi {{actionType}}",
"data-insight-alert-destination-description": "Send email notifications to admins or teams.",
"data-insight-alert-trigger-description": "Trigger for real time or schedule it for daily, weekly or monthly.",
@ -1089,6 +1096,7 @@
"entity-does-not-have-followers": "{{entityName}} ainda não tem seguidores",
"entity-ingestion-added-successfully": "{{entity}} Ingestão Adicionada com Sucesso",
"entity-is-not-valid": "{{entity}} não é válido",
"entity-is-not-valid-url": "{{entity}} is not valid url",
"entity-maximum-size": "{{entity}} can be a maximum of {{max}} characters",
"entity-not-contain-whitespace": "{{entity}} não deve conter espaço em branco",
"entity-owned-by-name": "Esta entidade é de propriedade de {{entityOwner}}",

View File

@ -162,6 +162,8 @@
"criteria": "标准",
"custom-attribute-plural": "自定义属性",
"custom-entity": "自定义条目",
"custom-logo": "Custom Logo",
"custom-logo-configuration": "Custom Logo Configuration",
"custom-oidc": "自定义 OIDC",
"custom-property": "自定义属性",
"custom-property-plural": "自定义属性",
@ -474,6 +476,7 @@
"log-plural": "日志",
"logged-in-user-lowercase": "已登录用户",
"login": "登录",
"logo-url": "Logo URL",
"logout": "退出",
"major": "主要",
"manage-entity": "管理{{entity}}",
@ -521,6 +524,7 @@
"model-plural": "模型",
"model-store": "模型 Store",
"monday": "星期一",
"monogram-url": "Monogram URL",
"month": "月",
"more": "更多",
"more-help": "更多帮助",
@ -1029,6 +1033,9 @@
"create-or-update-email-account-for-bot": "更改帐号电子邮箱将更新或创建一个新的机器人用户",
"created-this-task-lowercase": "创建了此任务",
"custom-classification-name-dbt-tags": "dbt 标签的自定义 OpenMetadata 分类名称",
"custom-logo-configuration-message": "Configure The Application Logo and Monogram.",
"custom-logo-url-path-message": "URL path for the login page logo.",
"custom-monogram-url-path-message": "URL path for the navbar logo.",
"data-asset-has-been-action-type": "数据资产已{{actionType}}",
"data-insight-alert-destination-description": "Send email notifications to admins or teams.",
"data-insight-alert-trigger-description": "Trigger for real time or schedule it for daily, weekly or monthly.",
@ -1089,6 +1096,7 @@
"entity-does-not-have-followers": "{{entityName}}尚无任何关注者",
"entity-ingestion-added-successfully": "{{entity}}提取成功添加",
"entity-is-not-valid": "{{entity}}无效",
"entity-is-not-valid-url": "{{entity}} is not valid url",
"entity-maximum-size": "{{entity}}最多只能包含{{max}}个字符",
"entity-not-contain-whitespace": "{{entity}}不应包含空格",
"entity-owned-by-name": "此实体归{{entityOwner}}所有",

View File

@ -0,0 +1,106 @@
/*
* 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,
waitForElementToBeRemoved,
} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { getSettingsConfigFromConfigType } from 'rest/settingConfigAPI';
import CustomLogoConfigSettingsPage from './CustomLogoConfigSettingsPage';
const mockPush = jest.fn();
jest.mock('rest/settingConfigAPI', () => ({
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(<CustomLogoConfigSettingsPage />);
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(<CustomLogoConfigSettingsPage />);
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(<CustomLogoConfigSettingsPage />);
await waitForElementToBeRemoved(() => screen.getByTestId('loader'));
const editButton = screen.getByTestId('edit-button');
expect(editButton).toBeInTheDocument();
userEvent.click(editButton);
expect(mockPush).toHaveBeenCalled();
});
});

View File

@ -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<boolean>(false);
const [config, setConfig] = useState<LogoConfiguration>();
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 <Loader />;
}
if (isUndefined(config)) {
return (
<ErrorPlaceHolder
permission
doc={CUSTOM_LOGO_DOCS}
heading={t('label.custom-logo')}
type={ERROR_PLACEHOLDER_TYPE.CREATE}
onClick={handleEditClick}
/>
);
}
return (
<Row align="middle" gutter={[16, 16]}>
<Col span={24}>
<Row align="middle" justify="space-between">
<Col>
<PageHeader
data={{
header: t('label.custom-logo'),
subHeader: t('message.custom-logo-configuration-message'),
}}
/>
</Col>
<Col>
<Button
data-testid="edit-button"
icon={<Icon component={IconEdit} size={12} />}
onClick={handleEditClick}>
{t('label.edit')}
</Button>
</Col>
</Row>
</Col>
<Col span={24}>
<Card>
<>
<Typography.Title level={5}>
{t('label.custom-logo-configuration')}
</Typography.Title>
<Row align="middle" className="m-t-md" gutter={[16, 16]}>
<Col span={12}>
<Row align="middle">
<Col span={24}>
<Typography.Text className="m-0 text-grey-muted">
{t('label.logo-url')}
<Tooltip
placement="top"
title={t('message.custom-logo-url-path-message')}
trigger="hover">
<InfoCircleOutlined
className="m-x-xss"
data-testid="logo-url-info"
style={{ color: GRAYED_OUT_COLOR }}
/>
</Tooltip>
</Typography.Text>
</Col>
<Col span={24}>
<Typography.Text data-testid="logo-url">
{isEmpty(config?.customLogoUrlPath)
? '--'
: config?.customLogoUrlPath}
</Typography.Text>
</Col>
</Row>
</Col>
<Col span={12}>
<Row align="middle">
<Col span={24}>
<Typography.Text className="m-0 text-grey-muted">
{t('label.monogram-url')}
<Tooltip
placement="top"
title={t('message.custom-monogram-url-path-message')}
trigger="hover">
<InfoCircleOutlined
className="m-x-xss"
data-testid="monogram-url-info"
style={{ color: GRAYED_OUT_COLOR }}
/>
</Tooltip>
</Typography.Text>
</Col>
<Col span={24}>
<Typography.Text data-testid="monogram-url">
{isEmpty(config?.customMonogramUrlPath)
? '--'
: config?.customMonogramUrlPath}
</Typography.Text>
</Col>
</Row>
</Col>
</Row>
</>
</Card>
</Col>
</Row>
);
};
export default CustomLogoConfigSettingsPage;

View File

@ -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(() => <div>BreadCrumb.component</div>)
);
jest.mock('components/common/ServiceDocPanel/ServiceDocPanel', () =>
jest.fn().mockImplementation(() => <div>ServiceDocPanel.component</div>)
);
jest.mock('components/common/ResizablePanels/ResizablePanels', () =>
jest.fn().mockImplementation(({ firstPanel, secondPanel }) => (
<>
<div>{firstPanel.children}</div>
<div>{secondPanel.children}</div>
</>
))
);
describe('Test Custom Logo Config Form', () => {
it('Should render the child components', async () => {
render(<EditCustomLogoConfig />);
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(<EditCustomLogoConfig />);
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(<EditCustomLogoConfig />);
await waitForElementToBeRemoved(() => screen.getByTestId('loader'));
const cancelButton = screen.getByTestId('cancel-button');
userEvent.click(cancelButton);
expect(mockGoBack).toHaveBeenCalled();
});
it('Save button should work', async () => {
render(<EditCustomLogoConfig />);
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',
},
});
});
});

View File

@ -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<string>('');
const [loading, setLoading] = useState<boolean>(false);
const [updating, setUpdating] = useState<boolean>(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 = (
<div className="max-width-md w-9/10 service-form-container">
<TitleBreadcrumb titleLinks={breadcrumb} />
<Card className="p-lg m-t-md">
<Form
data-testid="custom-logo-config-form"
form={form}
layout="vertical"
onFinish={handleSubmit}
onFocus={(e) => {
e.preventDefault();
e.stopPropagation();
setActiveField(e.target.id);
}}>
{generateFormFields(formFields)}
<Row justify="end">
<Col>
<Button
data-testid="cancel-button"
type="link"
onClick={handleGoBack}>
{t('label.cancel')}
</Button>
</Col>
<Col>
<Button
data-testid="save-button"
htmlType="submit"
loading={updating}
type="primary">
{t('label.save')}
</Button>
</Col>
</Row>
</Form>
</Card>
</div>
);
const secondPanelChildren = (
<ServiceDocPanel
activeField={activeField}
serviceName={CUSTOM_LOGO_CONFIG_SERVICE_CATEGORY}
serviceType={OPEN_METADATA as ServiceCategory}
/>
);
useEffect(() => {
fetchCustomLogoConfig();
}, []);
if (loading) {
return <Loader />;
}
return (
<PageContainerV1>
<ResizablePanels
firstPanel={{ children: firstPanelChildren, minWidth: 700, flex: 0.7 }}
pageTitle={t('label.edit-entity', { entity: t('label.service') })}
secondPanel={{
children: secondPanelChildren,
className: 'service-doc-panel',
minWidth: 60,
overlay: {
displayThreshold: 200,
header: t('label.setup-guide'),
rotation: 'counter-clockwise',
},
}}
/>
</PageContainerV1>
);
};
export default EditCustomLogoConfig;

View File

@ -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';

View File

@ -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';

View File

@ -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' }}>
<Row gutter={[16, 24]}>
<Col className="text-center" span={24}>
<SVGIcons
alt={t('label.open-metadata-logo')}
icon={Icons.LOGO}
width="152"
/>
<BrandImage className="m-auto" height="auto" width={152} />
</Col>
<Col className="flex-center text-center mt-8" span={24}>
<Typography.Text className="text-xl font-medium text-grey-muted">

View File

@ -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');
});
});

View File

@ -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,
})}>
<Image
alt="OpenMetadata Logo"
data-testid="brand-logo-image"
fallback={Logo}
preview={false}
src={brandLogoUrl}
width={152}
/>
<BrandImage height="auto" width={152} />
<Typography.Text className="mt-8 w-80 text-xl font-medium text-grey-muted">
{t('message.om-description')}{' '}
</Typography.Text>

View File

@ -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' }}>
<Row gutter={[16, 24]}>
<Col className="text-center" span={24}>
<SVGIcons alt="OpenMetadata Logo" icon={Icons.LOGO} width="152" />
<BrandImage className="m-auto" height="auto" width={152} />
</Col>
<Col className="mt-12 text-center" span={24}>

View File

@ -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 = () => {
<div className="d-flex bg-body-main flex-grow" data-testid="signin-page">
<div className="w-5/12">
<div className="mt-4 text-center flex-center flex-col">
<SVGIcons alt="OpenMetadata Logo" icon={Icons.LOGO} width="152" />
<BrandImage height="auto" width={152} />
<Typography.Text className="mt-8 w-80 text-xl font-medium text-grey-muted">
{t('message.om-description')}
</Typography.Text>

View File

@ -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<LogoConfiguration>(
`system/config/customLogoConfiguration`
);
return response.data;
};

View File

@ -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: <EmailSettingsIcon className="w-4 side-panel-icons" />,
},
{
label: i18next.t('label.custom-logo'),
isProtected: Boolean(isAdminUser),
key: 'openMetadata.customLogo',
icon: <CustomLogoIcon className="w-4 side-panel-icons" />,
},
],
},
{