@@ -24,8 +24,9 @@ import { getPendingPlugins } from '../../api/plugins'; | |||
import { getSystemStatus, waitSystemUPStatus } from '../../api/system'; | |||
import handleRequiredAuthorization from '../../app/utils/handleRequiredAuthorization'; | |||
import { translate } from '../../helpers/l10n'; | |||
import { AppState } from '../../types/appstate'; | |||
import { PendingPluginResult } from '../../types/plugins'; | |||
import { AppState, Extension, SysStatus } from '../../types/types'; | |||
import { Extension, SysStatus } from '../../types/types'; | |||
import AdminContext, { defaultPendingPlugins, defaultSystemStatus } from './AdminContext'; | |||
import withAppStateContext from './app-state/withAppStateContext'; | |||
import SettingsNav from './nav/settings/SettingsNav'; |
@@ -18,16 +18,16 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import { connect } from 'react-redux'; | |||
import { lazyLoadComponent } from '../../components/lazyLoadComponent'; | |||
import { getGlobalSettingValue, Store } from '../../store/rootReducer'; | |||
import { AppState } from '../../types/appstate'; | |||
import { GlobalSettingKeys } from '../../types/settings'; | |||
import withAppStateContext from './app-state/withAppStateContext'; | |||
import KeyboardShortcutsModal from './KeyboardShortcutsModal'; | |||
const PageTracker = lazyLoadComponent(() => import('./PageTracker')); | |||
interface Props { | |||
enableGravatar: boolean; | |||
gravatarServerUrl: string; | |||
appState: AppState; | |||
} | |||
export class App extends React.PureComponent<Props> { | |||
@@ -68,8 +68,19 @@ export class App extends React.PureComponent<Props> { | |||
}; | |||
renderPreconnectLink = () => { | |||
const { | |||
appState: { settings } | |||
} = this.props; | |||
const enableGravatar = settings[GlobalSettingKeys.EnableGravatar] === 'true'; | |||
const gravatarServerUrl = settings[GlobalSettingKeys.GravatarServerUrl]; | |||
if (!enableGravatar || !gravatarServerUrl) { | |||
return null; | |||
} | |||
const parser = document.createElement('a'); | |||
parser.href = this.props.gravatarServerUrl; | |||
parser.href = gravatarServerUrl; | |||
if (parser.hostname !== window.location.hostname) { | |||
return <link href={parser.origin} rel="preconnect" />; | |||
} | |||
@@ -79,7 +90,7 @@ export class App extends React.PureComponent<Props> { | |||
render() { | |||
return ( | |||
<> | |||
<PageTracker>{this.props.enableGravatar && this.renderPreconnectLink()}</PageTracker> | |||
<PageTracker>{this.renderPreconnectLink()}</PageTracker> | |||
{this.props.children} | |||
<KeyboardShortcutsModal /> | |||
</> | |||
@@ -87,13 +98,4 @@ export class App extends React.PureComponent<Props> { | |||
} | |||
} | |||
const mapStateToProps = (state: Store) => { | |||
const enableGravatar = getGlobalSettingValue(state, 'sonar.lf.enableGravatar'); | |||
const gravatarServerUrl = getGlobalSettingValue(state, 'sonar.lf.gravatarServerUrl'); | |||
return { | |||
enableGravatar: Boolean(enableGravatar && enableGravatar.value === 'true'), | |||
gravatarServerUrl: (gravatarServerUrl && gravatarServerUrl.value) || '' | |||
}; | |||
}; | |||
export default connect(mapStateToProps)(App); | |||
export default withAppStateContext(App); |
@@ -39,10 +39,11 @@ import { | |||
ProjectAlmBindingConfigurationErrors, | |||
ProjectAlmBindingResponse | |||
} from '../../types/alm-settings'; | |||
import { AppState } from '../../types/appstate'; | |||
import { BranchLike } from '../../types/branch-like'; | |||
import { ComponentQualifier, isPortfolioLike } from '../../types/component'; | |||
import { Task, TaskStatuses, TaskTypes, TaskWarning } from '../../types/tasks'; | |||
import { AppState, Component, Status } from '../../types/types'; | |||
import { Component, Status } from '../../types/types'; | |||
import handleRequiredAuthorization from '../utils/handleRequiredAuthorization'; | |||
import withAppStateContext from './app-state/withAppStateContext'; | |||
import ComponentContainerNotFound from './ComponentContainerNotFound'; |
@@ -23,8 +23,8 @@ import InstanceMessage from '../../components/common/InstanceMessage'; | |||
import { Alert } from '../../components/ui/Alert'; | |||
import { getEdition } from '../../helpers/editions'; | |||
import { translate, translateWithParameters } from '../../helpers/l10n'; | |||
import { AppState } from '../../types/appstate'; | |||
import { EditionKey } from '../../types/editions'; | |||
import { AppState } from '../../types/types'; | |||
import withAppStateContext from './app-state/withAppStateContext'; | |||
import GlobalFooterBranding from './GlobalFooterBranding'; | |||
@@ -19,19 +19,15 @@ | |||
*/ | |||
import * as React from 'react'; | |||
import { Helmet } from 'react-helmet-async'; | |||
import { connect } from 'react-redux'; | |||
import { Location, withRouter } from '../../components/hoc/withRouter'; | |||
import { gtm } from '../../helpers/analytics'; | |||
import { installScript } from '../../helpers/extensions'; | |||
import { getWebAnalyticsPageHandlerFromCache } from '../../helpers/extensionsHandler'; | |||
import { getInstance } from '../../helpers/system'; | |||
import { getGlobalSettingValue, Store } from '../../store/rootReducer'; | |||
import { AppState } from '../../types/types'; | |||
import { AppState } from '../../types/appstate'; | |||
import withAppStateContext from './app-state/withAppStateContext'; | |||
interface Props { | |||
location: Location; | |||
trackingIdGTM?: string; | |||
appState: AppState; | |||
} | |||
@@ -43,54 +39,37 @@ export class PageTracker extends React.Component<Props, State> { | |||
state: State = {}; | |||
componentDidMount() { | |||
const { trackingIdGTM, appState } = this.props; | |||
const { appState } = this.props; | |||
if (appState.webAnalyticsJsPath && !getWebAnalyticsPageHandlerFromCache()) { | |||
installScript(appState.webAnalyticsJsPath, 'head'); | |||
} | |||
if (trackingIdGTM) { | |||
gtm(trackingIdGTM); | |||
} | |||
} | |||
trackPage = () => { | |||
const { location, trackingIdGTM } = this.props; | |||
const { location } = this.props; | |||
const { lastLocation } = this.state; | |||
const { dataLayer } = window as any; | |||
const locationChanged = location.pathname !== lastLocation; | |||
const webAnalyticsPageChange = getWebAnalyticsPageHandlerFromCache(); | |||
if (webAnalyticsPageChange && locationChanged) { | |||
this.setState({ lastLocation: location.pathname }); | |||
setTimeout(() => webAnalyticsPageChange(location.pathname), 500); | |||
} else if (dataLayer && dataLayer.push && trackingIdGTM && location.pathname !== '/') { | |||
this.setState({ lastLocation: location.pathname }); | |||
setTimeout(() => dataLayer.push({ event: 'render-end' }), 500); | |||
} | |||
}; | |||
render() { | |||
const { trackingIdGTM, appState } = this.props; | |||
const { appState } = this.props; | |||
return ( | |||
<Helmet | |||
defaultTitle={getInstance()} | |||
defer={false} | |||
onChangeClientState={ | |||
trackingIdGTM || appState.webAnalyticsJsPath ? this.trackPage : undefined | |||
}> | |||
onChangeClientState={appState.webAnalyticsJsPath ? this.trackPage : undefined}> | |||
{this.props.children} | |||
</Helmet> | |||
); | |||
} | |||
} | |||
const mapStateToProps = (state: Store) => { | |||
const trackingIdGTM = getGlobalSettingValue(state, 'sonar.analytics.gtm.trackingId'); | |||
return { | |||
trackingIdGTM: trackingIdGTM && trackingIdGTM.value | |||
}; | |||
}; | |||
export default withRouter(connect(mapStateToProps)(withAppStateContext(PageTracker))); | |||
export default withRouter(withAppStateContext(PageTracker)); |
@@ -28,8 +28,9 @@ import { hasMessage } from '../../helpers/l10n'; | |||
import { get, save } from '../../helpers/storage'; | |||
import { isLoggedIn } from '../../helpers/users'; | |||
import { getCurrentUser, Store } from '../../store/rootReducer'; | |||
import { AppState } from '../../types/appstate'; | |||
import { EditionKey } from '../../types/editions'; | |||
import { AppState, CurrentUser } from '../../types/types'; | |||
import { CurrentUser } from '../../types/types'; | |||
import withAppStateContext from './app-state/withAppStateContext'; | |||
const LicensePromptModal = lazyLoadComponent( |
@@ -19,23 +19,20 @@ | |||
*/ | |||
import { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { connect } from 'react-redux'; | |||
import { mockAppState } from '../../../helpers/testMocks'; | |||
import { App } from '../App'; | |||
jest.mock('react-redux', () => ({ | |||
connect: jest.fn(() => (a: any) => a) | |||
})); | |||
jest.mock('../../../store/rootReducer', () => ({ | |||
getGlobalSettingValue: jest.fn((_, key: string) => ({ | |||
value: key === 'sonar.lf.enableGravatar' ? 'true' : 'http://gravatar.com' | |||
})) | |||
})); | |||
it('should render correctly', () => { | |||
expect(shallowRender()).toMatchSnapshot('default'); | |||
expect( | |||
shallowRender({ enableGravatar: true, gravatarServerUrl: 'http://example.com' }) | |||
shallowRender({ | |||
appState: mockAppState({ | |||
settings: { | |||
'sonar.lf.enableGravatar': 'true', | |||
'sonar.lf.gravatarServerUrl': 'http://example.com' | |||
} | |||
}) | |||
}) | |||
).toMatchSnapshot('with gravatar'); | |||
}); | |||
@@ -44,17 +41,16 @@ it('should correctly set the scrollbar width as a custom property', () => { | |||
expect(document.body.style.getPropertyValue('--sbw')).toBe('0px'); | |||
}); | |||
describe('redux', () => { | |||
it('should correctly map state props', () => { | |||
const [mapStateToProps] = (connect as jest.Mock).mock.calls[0]; | |||
expect(mapStateToProps({})).toEqual({ | |||
enableGravatar: true, | |||
gravatarServerUrl: 'http://gravatar.com' | |||
}); | |||
}); | |||
}); | |||
function shallowRender(props: Partial<App['props']> = {}) { | |||
return shallow<App>(<App enableGravatar={false} gravatarServerUrl="" {...props} />); | |||
return shallow<App>( | |||
<App | |||
appState={mockAppState({ | |||
settings: { | |||
'sonar.lf.enableGravatar': 'false', | |||
'sonar.lf.gravatarServerUrl': '' | |||
} | |||
})} | |||
{...props} | |||
/> | |||
); | |||
} |
@@ -19,7 +19,6 @@ | |||
*/ | |||
import { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { gtm } from '../../../helpers/analytics'; | |||
import { installScript } from '../../../helpers/extensions'; | |||
import { getWebAnalyticsPageHandlerFromCache } from '../../../helpers/extensionsHandler'; | |||
import { mockAppState, mockLocation } from '../../../helpers/testMocks'; | |||
@@ -33,7 +32,6 @@ jest.mock('../../../helpers/extensionsHandler', () => ({ | |||
getWebAnalyticsPageHandlerFromCache: jest.fn().mockReturnValue(undefined) | |||
})); | |||
jest.mock('../../../helpers/analytics', () => ({ gtm: jest.fn() })); | |||
beforeAll(() => { | |||
jest.useFakeTimers(); | |||
}); | |||
@@ -52,7 +50,6 @@ it('should not trigger if no analytics system is given', () => { | |||
const wrapper = shallowRender(); | |||
expect(wrapper).toMatchSnapshot(); | |||
expect(installScript).not.toHaveBeenCalled(); | |||
expect(gtm).not.toHaveBeenCalled(); | |||
}); | |||
it('should work for WebAnalytics plugin', () => { | |||
@@ -70,22 +67,6 @@ it('should work for WebAnalytics plugin', () => { | |||
expect(pageChange).toHaveBeenCalledWith('/path'); | |||
}); | |||
it('should work for Google Tag Manager', () => { | |||
(window as any).dataLayer = []; | |||
const { dataLayer } = window as any; | |||
const push = jest.spyOn(dataLayer, 'push'); | |||
const wrapper = shallowRender({ trackingIdGTM: '123' }); | |||
expect(wrapper.find('Helmet').prop('onChangeClientState')).toBe(wrapper.instance().trackPage); | |||
expect(gtm).toBeCalled(); | |||
expect(dataLayer).toHaveLength(0); | |||
wrapper.instance().trackPage(); | |||
jest.runAllTimers(); | |||
expect(push).toBeCalledWith({ event: 'render-end' }); | |||
expect(dataLayer).toHaveLength(1); | |||
}); | |||
function shallowRender(props: Partial<PageTracker['props']> = {}) { | |||
return shallow<PageTracker>( | |||
<PageTracker appState={mockAppState()} location={mockLocation()} {...props} /> |
@@ -19,7 +19,7 @@ | |||
*/ | |||
import * as React from 'react'; | |||
import { AppState } from '../../../types/types'; | |||
import { AppState } from '../../../types/appstate'; | |||
const defaultAppState = { | |||
authenticationError: false, |
@@ -20,29 +20,16 @@ | |||
*/ | |||
import * as React from 'react'; | |||
import { connect } from 'react-redux'; | |||
import { setValues } from '../../../apps/settings/store/actions'; | |||
import { AppState } from '../../../types/types'; | |||
import { AppState } from '../../../types/appstate'; | |||
import { AppStateContext } from './AppStateContext'; | |||
export interface AppStateContextProviderProps { | |||
setValues: typeof setValues; | |||
appState: AppState; | |||
} | |||
export function AppStateContextProvider({ | |||
setValues, | |||
export default function AppStateContextProvider({ | |||
appState, | |||
children | |||
}: React.PropsWithChildren<AppStateContextProviderProps>) { | |||
React.useEffect(() => { | |||
setValues( | |||
Object.keys(appState.settings), | |||
Object.entries(appState.settings).map(([key, value]) => ({ key, value })) | |||
); | |||
}); | |||
return <AppStateContext.Provider value={appState}>{children}</AppStateContext.Provider>; | |||
} | |||
const mapDispatchToProps = { setValues }; | |||
export default connect(null, mapDispatchToProps)(AppStateContextProvider); |
@@ -21,20 +21,18 @@ import { mount } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { mockAppState } from '../../../../helpers/testMocks'; | |||
import { waitAndUpdate } from '../../../../helpers/testUtils'; | |||
import { AppStateContextProvider, AppStateContextProviderProps } from '../AppStateContextProvider'; | |||
import AppStateContextProvider, { AppStateContextProviderProps } from '../AppStateContextProvider'; | |||
it('should set value correctly', async () => { | |||
const setValues = jest.fn(); | |||
const appState = mockAppState({ settings: { 'sonar.lf.logoUrl': 'whatevs/' } }); | |||
const wrapper = render({ | |||
appState: mockAppState({ settings: { foo: 'bar' } }), | |||
setValues | |||
appState | |||
}); | |||
await waitAndUpdate(wrapper); | |||
expect(setValues).toHaveBeenCalledWith(['foo'], [{ key: 'foo', value: 'bar' }]); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); | |||
function render(override?: Partial<AppStateContextProviderProps>) { | |||
return mount( | |||
<AppStateContextProvider appState={mockAppState()} setValues={jest.fn()} {...override} /> | |||
); | |||
return mount(<AppStateContextProvider appState={mockAppState()} {...override} />); | |||
} |
@@ -0,0 +1,19 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should set value correctly 1`] = ` | |||
<AppStateContextProvider | |||
appState={ | |||
Object { | |||
"edition": "community", | |||
"productionDatabase": true, | |||
"qualifiers": Array [ | |||
"TRK", | |||
], | |||
"settings": Object { | |||
"sonar.lf.logoUrl": "whatevs/", | |||
}, | |||
"version": "1.0", | |||
} | |||
} | |||
/> | |||
`; |
@@ -20,7 +20,7 @@ | |||
import { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { mockAppState } from '../../../../helpers/testMocks'; | |||
import { AppState } from '../../../../types/types'; | |||
import { AppState } from '../../../../types/appstate'; | |||
import withAppStateContext from '../withAppStateContext'; | |||
const appState = mockAppState(); |
@@ -20,7 +20,7 @@ | |||
import * as React from 'react'; | |||
import { getWrappedDisplayName } from '../../../components/hoc/utils'; | |||
import { AppState } from '../../../types/types'; | |||
import { AppState } from '../../../types/appstate'; | |||
import { AppStateContext } from './AppStateContext'; | |||
export interface WithAppStateContextProps { |
@@ -27,8 +27,9 @@ import { getCurrentL10nBundle, translate } from '../../../helpers/l10n'; | |||
import { getBaseUrl } from '../../../helpers/system'; | |||
import { addGlobalErrorMessage } from '../../../store/globalMessages'; | |||
import { getCurrentUser, Store } from '../../../store/rootReducer'; | |||
import { AppState } from '../../../types/appstate'; | |||
import { ExtensionStartMethod } from '../../../types/extension'; | |||
import { AppState, CurrentUser, Dict, Extension as TypeExtension } from '../../../types/types'; | |||
import { CurrentUser, Dict, Extension as TypeExtension } from '../../../types/types'; | |||
import * as theme from '../../theme'; | |||
import getStore from '../../utils/getStore'; | |||
import withAppStateContext from '../app-state/withAppStateContext'; |
@@ -18,7 +18,7 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import { AppState } from '../../../types/types'; | |||
import { AppState } from '../../../types/appstate'; | |||
import withAppStateContext from '../app-state/withAppStateContext'; | |||
import NotFound from '../NotFound'; | |||
import Extension from './Extension'; |
@@ -70,6 +70,7 @@ import DateFormatter from '../../../components/intl/DateFormatter'; | |||
import DateFromNow from '../../../components/intl/DateFromNow'; | |||
import DateTimeFormatter from '../../../components/intl/DateTimeFormatter'; | |||
import Measure from '../../../components/measure/Measure'; | |||
import RatingTooltipContent from '../../../components/measure/RatingTooltipContent'; | |||
import { Alert } from '../../../components/ui/Alert'; | |||
import CoverageRating from '../../../components/ui/CoverageRating'; | |||
import DeferredSpinner from '../../../components/ui/DeferredSpinner'; | |||
@@ -237,6 +238,7 @@ const exposeLibraries = () => { | |||
Radio, | |||
RadioToggle, | |||
Rating, | |||
RatingTooltipContent, | |||
ReloadButton, | |||
ResetButtonLink, | |||
SearchBox, |
@@ -19,8 +19,8 @@ | |||
*/ | |||
/* eslint-disable react/no-unused-state */ | |||
import * as React from 'react'; | |||
import { AppState } from '../../../types/appstate'; | |||
import { IndexationContextInterface, IndexationStatus } from '../../../types/indexation'; | |||
import { AppState } from '../../../types/types'; | |||
import withAppStateContext from '../app-state/withAppStateContext'; | |||
import { IndexationContext } from './IndexationContext'; | |||
import IndexationNotificationHelper from './IndexationNotificationHelper'; |
@@ -22,9 +22,9 @@ import { Link } from 'react-router'; | |||
import { isValidLicense } from '../../../../api/marketplace'; | |||
import { Alert } from '../../../../components/ui/Alert'; | |||
import { translate, translateWithParameters } from '../../../../helpers/l10n'; | |||
import { AppState } from '../../../../types/appstate'; | |||
import { ComponentQualifier } from '../../../../types/component'; | |||
import { Task } from '../../../../types/tasks'; | |||
import { AppState } from '../../../../types/types'; | |||
import withAppStateContext from '../../app-state/withAppStateContext'; | |||
interface Props { |
@@ -30,9 +30,10 @@ import NavBarTabs from '../../../../components/ui/NavBarTabs'; | |||
import { getBranchLikeQuery, isPullRequest } from '../../../../helpers/branch-like'; | |||
import { hasMessage, translate, translateWithParameters } from '../../../../helpers/l10n'; | |||
import { getPortfolioUrl, getProjectQueryUrl } from '../../../../helpers/urls'; | |||
import { AppState } from '../../../../types/appstate'; | |||
import { BranchLike, BranchParameters } from '../../../../types/branch-like'; | |||
import { ComponentQualifier, isPortfolioLike } from '../../../../types/component'; | |||
import { AppState, Component, Extension } from '../../../../types/types'; | |||
import { Component, Extension } from '../../../../types/types'; | |||
import withAppStateContext from '../../app-state/withAppStateContext'; | |||
import './Menu.css'; | |||
@@ -21,8 +21,9 @@ import classNames from 'classnames'; | |||
import * as React from 'react'; | |||
import Toggler from '../../../../../components/controls/Toggler'; | |||
import { ProjectAlmBindingResponse } from '../../../../../types/alm-settings'; | |||
import { AppState } from '../../../../../types/appstate'; | |||
import { BranchLike } from '../../../../../types/branch-like'; | |||
import { AppState, Component } from '../../../../../types/types'; | |||
import { Component } from '../../../../../types/types'; | |||
import withAppStateContext from '../../../app-state/withAppStateContext'; | |||
import './BranchLikeNavigation.css'; | |||
import CurrentBranchLike from './CurrentBranchLike'; |
@@ -18,18 +18,21 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import { connect } from 'react-redux'; | |||
import { Link } from 'react-router'; | |||
import { translate } from '../../../../helpers/l10n'; | |||
import { getBaseUrl } from '../../../../helpers/system'; | |||
import { getGlobalSettingValue, Store } from '../../../../store/rootReducer'; | |||
import { AppState } from '../../../../types/appstate'; | |||
import { GlobalSettingKeys } from '../../../../types/settings'; | |||
import withAppStateContext from '../../app-state/withAppStateContext'; | |||
interface StateProps { | |||
customLogoUrl?: string; | |||
customLogoWidth?: string | number; | |||
export interface GlobalNavBrandingProps { | |||
appState: AppState; | |||
} | |||
export function GlobalNavBranding({ customLogoUrl, customLogoWidth }: StateProps) { | |||
export function GlobalNavBranding({ appState: { settings } }: GlobalNavBrandingProps) { | |||
const customLogoUrl = settings[GlobalSettingKeys.LogoUrl]; | |||
const customLogoWidth = settings[GlobalSettingKeys.LogoWidth]; | |||
const title = translate('layout.sonar.slogan'); | |||
const url = customLogoUrl || `${getBaseUrl()}/images/logo.svg?v=6.6`; | |||
const width = customLogoUrl ? customLogoWidth || 100 : 83; | |||
@@ -41,13 +44,4 @@ export function GlobalNavBranding({ customLogoUrl, customLogoWidth }: StateProps | |||
); | |||
} | |||
const mapStateToProps = (state: Store): StateProps => { | |||
const customLogoUrl = getGlobalSettingValue(state, 'sonar.lf.logoUrl'); | |||
const customLogoWidth = getGlobalSettingValue(state, 'sonar.lf.logoWidthPx'); | |||
return { | |||
customLogoUrl: customLogoUrl && customLogoUrl.value, | |||
customLogoWidth: customLogoWidth && customLogoWidth.value | |||
}; | |||
}; | |||
export default connect(mapStateToProps)(GlobalNavBranding); | |||
export default withAppStateContext(GlobalNavBranding); |
@@ -25,8 +25,9 @@ import Dropdown from '../../../../components/controls/Dropdown'; | |||
import DropdownIcon from '../../../../components/icons/DropdownIcon'; | |||
import { translate } from '../../../../helpers/l10n'; | |||
import { getQualityGatesUrl } from '../../../../helpers/urls'; | |||
import { AppState } from '../../../../types/appstate'; | |||
import { ComponentQualifier } from '../../../../types/component'; | |||
import { AppState, CurrentUser, Extension } from '../../../../types/types'; | |||
import { CurrentUser, Extension } from '../../../../types/types'; | |||
import withAppStateContext from '../../app-state/withAppStateContext'; | |||
interface Props { |
@@ -0,0 +1,50 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2022 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { mockAppState } from '../../../../../helpers/testMocks'; | |||
import { GlobalNavBranding, GlobalNavBrandingProps } from '../GlobalNavBranding'; | |||
it('should render correctly', () => { | |||
expect(shallowRender()).toMatchSnapshot('default'); | |||
expect( | |||
shallowRender({ | |||
appState: mockAppState({ | |||
settings: { | |||
'sonar.lf.logoUrl': 'http://sonarsource.com/custom-logo.svg' | |||
} | |||
}) | |||
}) | |||
).toMatchSnapshot('with logo'); | |||
expect( | |||
shallowRender({ | |||
appState: mockAppState({ | |||
settings: { | |||
'sonar.lf.logoUrl': 'http://sonarsource.com/custom-logo.svg', | |||
'sonar.lf.logoWidthPx': '200' | |||
} | |||
}) | |||
}) | |||
).toMatchSnapshot('with logo and width'); | |||
}); | |||
function shallowRender(overrides: Partial<GlobalNavBrandingProps> = {}) { | |||
return shallow(<GlobalNavBranding appState={mockAppState()} {...overrides} />); | |||
} |
@@ -6,7 +6,7 @@ exports[`should render correctly: anonymous users 1`] = ` | |||
height={48} | |||
id="global-navigation" | |||
> | |||
<Connect(GlobalNavBranding) /> | |||
<withAppStateContext(GlobalNavBranding) /> | |||
<withAppStateContext(GlobalNavMenu) | |||
currentUser={ | |||
Object { | |||
@@ -47,7 +47,7 @@ exports[`should render correctly: logged in users 1`] = ` | |||
height={48} | |||
id="global-navigation" | |||
> | |||
<Connect(GlobalNavBranding) /> | |||
<withAppStateContext(GlobalNavBranding) /> | |||
<withAppStateContext(GlobalNavMenu) | |||
currentUser={ | |||
Object { |
@@ -0,0 +1,52 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly: default 1`] = ` | |||
<Link | |||
className="navbar-brand" | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to="/" | |||
> | |||
<img | |||
alt="layout.sonar.slogan" | |||
height={30} | |||
src="/images/logo.svg?v=6.6" | |||
title="layout.sonar.slogan" | |||
width={83} | |||
/> | |||
</Link> | |||
`; | |||
exports[`should render correctly: with logo 1`] = ` | |||
<Link | |||
className="navbar-brand" | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to="/" | |||
> | |||
<img | |||
alt="layout.sonar.slogan" | |||
height={30} | |||
src="http://sonarsource.com/custom-logo.svg" | |||
title="layout.sonar.slogan" | |||
width={100} | |||
/> | |||
</Link> | |||
`; | |||
exports[`should render correctly: with logo and width 1`] = ` | |||
<Link | |||
className="navbar-brand" | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to="/" | |||
> | |||
<img | |||
alt="layout.sonar.slogan" | |||
height={30} | |||
src="http://sonarsource.com/custom-logo.svg" | |||
title="layout.sonar.slogan" | |||
width="200" | |||
/> | |||
</Link> | |||
`; |
@@ -60,7 +60,7 @@ exports[`should render the right interface for logged in user 1`] = ` | |||
href="#" | |||
title="Skywalker" | |||
> | |||
<Connect(Avatar) | |||
<withAppStateContext(Avatar) | |||
name="Skywalker" | |||
size={32} | |||
/> |
@@ -27,9 +27,10 @@ import SystemUpgradeButton from '../../../components/upgrade/SystemUpgradeButton | |||
import { sortUpgrades, UpdateUseCase } from '../../../components/upgrade/utils'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { hasGlobalPermission, isLoggedIn } from '../../../helpers/users'; | |||
import { AppState } from '../../../types/appstate'; | |||
import { Permissions } from '../../../types/permissions'; | |||
import { SystemUpgrade } from '../../../types/system'; | |||
import { AppState, CurrentUser, Dict } from '../../../types/types'; | |||
import { CurrentUser, Dict } from '../../../types/types'; | |||
import withAppStateContext from '../app-state/withAppStateContext'; | |||
import './UpdateNotification.css'; | |||
@@ -21,7 +21,7 @@ import { installExtensionsHandler, installWebAnalyticsHandler } from '../helpers | |||
import { loadL10nBundle } from '../helpers/l10n'; | |||
import { parseJSON, request } from '../helpers/request'; | |||
import { getBaseUrl, getSystemStatus } from '../helpers/system'; | |||
import { AppState } from '../types/types'; | |||
import { AppState } from '../types/appstate'; | |||
import './styles/sonar.ts'; | |||
installWebAnalyticsHandler(); |
@@ -60,7 +60,8 @@ import webhooksRoutes from '../../apps/webhooks/routes'; | |||
import withIndexationGuard from '../../components/hoc/withIndexationGuard'; | |||
import { lazyLoadComponent } from '../../components/lazyLoadComponent'; | |||
import getHistory from '../../helpers/getHistory'; | |||
import { AppState, CurrentUser } from '../../types/types'; | |||
import { AppState } from '../../types/appstate'; | |||
import { CurrentUser } from '../../types/types'; | |||
import App from '../components/App'; | |||
import AppStateContextProvider from '../components/app-state/AppStateContextProvider'; | |||
import GlobalContainer from '../components/GlobalContainer'; |
@@ -18,60 +18,63 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import { connect } from 'react-redux'; | |||
import { getGlobalSettingValue, Store } from '../../../store/rootReducer'; | |||
import { getValues } from '../../../api/settings'; | |||
import { AdminPageExtension } from '../../../types/extension'; | |||
import { SettingsKey } from '../../../types/settings'; | |||
import { Extension } from '../../../types/types'; | |||
import { fetchValues } from '../../settings/store/actions'; | |||
import '../style.css'; | |||
import { HousekeepingPolicy, RangeOption } from '../utils'; | |||
import AuditAppRenderer from './AuditAppRenderer'; | |||
interface Props { | |||
auditHousekeepingPolicy: HousekeepingPolicy; | |||
fetchValues: typeof fetchValues; | |||
adminPages: Extension[]; | |||
} | |||
interface State { | |||
dateRange?: { from?: Date; to?: Date }; | |||
hasGovernanceExtension?: boolean; | |||
downloadStarted: boolean; | |||
housekeepingPolicy: HousekeepingPolicy; | |||
selection: RangeOption; | |||
} | |||
export class AuditApp extends React.PureComponent<Props, State> { | |||
export default class AuditApp extends React.PureComponent<Props, State> { | |||
constructor(props: Props) { | |||
super(props); | |||
const hasGovernanceExtension = Boolean( | |||
props.adminPages?.find(e => e.key === AdminPageExtension.GovernanceConsole) | |||
); | |||
this.state = { | |||
downloadStarted: false, | |||
selection: RangeOption.Today, | |||
hasGovernanceExtension | |||
housekeepingPolicy: HousekeepingPolicy.Monthly, | |||
selection: RangeOption.Today | |||
}; | |||
} | |||
componentDidMount() { | |||
const { hasGovernanceExtension } = this.state; | |||
if (hasGovernanceExtension) { | |||
this.props.fetchValues(['sonar.dbcleaner.auditHousekeeping']); | |||
if (this.hasGovernanceExtension()) { | |||
this.fetchHouseKeepingPolicy(); | |||
} | |||
} | |||
componentDidUpdate(prevProps: Props) { | |||
if (prevProps.adminPages !== this.props.adminPages) { | |||
const hasGovernanceExtension = Boolean( | |||
this.props.adminPages?.find(e => e.key === AdminPageExtension.GovernanceConsole) | |||
); | |||
this.setState({ | |||
hasGovernanceExtension | |||
}); | |||
if (prevProps.adminPages !== this.props.adminPages && this.hasGovernanceExtension()) { | |||
this.fetchHouseKeepingPolicy(); | |||
} | |||
} | |||
fetchHouseKeepingPolicy = async () => { | |||
const results = await getValues({ keys: SettingsKey.AuditHouseKeeping }); | |||
this.setState({ | |||
housekeepingPolicy: | |||
(results[0]?.value as HousekeepingPolicy | undefined) ?? HousekeepingPolicy.Monthly | |||
}); | |||
}; | |||
hasGovernanceExtension = () => { | |||
return Boolean( | |||
this.props.adminPages?.find(e => e.key === AdminPageExtension.GovernanceConsole) | |||
); | |||
}; | |||
handleDateSelection = (dateRange: { from?: Date; to?: Date }) => | |||
this.setState({ dateRange, downloadStarted: false, selection: RangeOption.Custom }); | |||
@@ -85,10 +88,7 @@ export class AuditApp extends React.PureComponent<Props, State> { | |||
}; | |||
render() { | |||
const { hasGovernanceExtension, ...auditAppRendererProps } = this.state; | |||
const { auditHousekeepingPolicy } = this.props; | |||
if (!hasGovernanceExtension) { | |||
if (!this.hasGovernanceExtension()) { | |||
return null; | |||
} | |||
@@ -97,20 +97,8 @@ export class AuditApp extends React.PureComponent<Props, State> { | |||
handleDateSelection={this.handleDateSelection} | |||
handleOptionSelection={this.handleOptionSelection} | |||
handleStartDownload={this.handleStartDownload} | |||
housekeepingPolicy={auditHousekeepingPolicy || HousekeepingPolicy.Monthly} | |||
{...auditAppRendererProps} | |||
{...this.state} | |||
/> | |||
); | |||
} | |||
} | |||
const mapDispatchToProps = { fetchValues }; | |||
const mapStateToProps = (state: Store) => { | |||
const settingValue = getGlobalSettingValue(state, 'sonar.dbcleaner.auditHousekeeping'); | |||
return { | |||
auditHousekeepingPolicy: settingValue?.value as HousekeepingPolicy | |||
}; | |||
}; | |||
export default connect(mapStateToProps, mapDispatchToProps)(AuditApp); |
@@ -20,30 +20,41 @@ | |||
import { subDays } from 'date-fns'; | |||
import { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { getValues } from '../../../../api/settings'; | |||
import { waitAndUpdate } from '../../../../helpers/testUtils'; | |||
import { AdminPageExtension } from '../../../../types/extension'; | |||
import { HousekeepingPolicy, RangeOption } from '../../utils'; | |||
import { AuditApp } from '../AuditApp'; | |||
import AuditApp from '../AuditApp'; | |||
import AuditAppRenderer from '../AuditAppRenderer'; | |||
jest.mock('../../../../api/settings', () => ({ | |||
getValues: jest.fn().mockResolvedValue([]) | |||
})); | |||
beforeEach(() => { | |||
jest.clearAllMocks(); | |||
}); | |||
it('should render correctly', () => { | |||
expect(shallowRender()).toMatchSnapshot(); | |||
}); | |||
it('should do nothing if governance is not available', async () => { | |||
const fetchValues = jest.fn(); | |||
const wrapper = shallowRender({ fetchValues, adminPages: [] }); | |||
const wrapper = shallowRender({ adminPages: [] }); | |||
await waitAndUpdate(wrapper); | |||
expect(wrapper.type()).toBeNull(); | |||
expect(fetchValues).not.toBeCalled(); | |||
expect(getValues).not.toBeCalled(); | |||
}); | |||
it('should fetch houskeeping policy on mount', async () => { | |||
const fetchValues = jest.fn(); | |||
const wrapper = shallowRender({ fetchValues }); | |||
it('should handle housekeeping policy', async () => { | |||
(getValues as jest.Mock).mockResolvedValueOnce([{ value: HousekeepingPolicy.Weekly }]); | |||
const wrapper = shallowRender(); | |||
await waitAndUpdate(wrapper); | |||
expect(fetchValues).toBeCalled(); | |||
expect(wrapper.find(AuditAppRenderer).props().housekeepingPolicy).toBe(HousekeepingPolicy.Weekly); | |||
}); | |||
it('should handle date selection', () => { | |||
@@ -76,11 +87,22 @@ it('should handle predefined selection', () => { | |||
expect(wrapper.state().dateRange).toBeUndefined(); | |||
}); | |||
it('should handle update to admin pages', async () => { | |||
const wrapper = shallowRender({ adminPages: [] }); | |||
await waitAndUpdate(wrapper); | |||
expect(wrapper.type()).toBeNull(); | |||
expect(getValues).not.toBeCalled(); | |||
wrapper.setProps({ adminPages: [{ key: AdminPageExtension.GovernanceConsole, name: 'name' }] }); | |||
await waitAndUpdate(wrapper); | |||
expect(getValues).toBeCalled(); | |||
}); | |||
function shallowRender(props: Partial<AuditApp['props']> = {}) { | |||
return shallow<AuditApp>( | |||
<AuditApp | |||
auditHousekeepingPolicy={HousekeepingPolicy.Monthly} | |||
fetchValues={jest.fn()} | |||
adminPages={[{ key: AdminPageExtension.GovernanceConsole, name: 'name' }]} | |||
{...props} | |||
/> |
@@ -24,7 +24,7 @@ import { ClearButton } from '../../../components/controls/buttons'; | |||
import ConfirmButton from '../../../components/controls/ConfirmButton'; | |||
import Tooltip from '../../../components/controls/Tooltip'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { AppState } from '../../../types/types'; | |||
import { AppState } from '../../../types/appstate'; | |||
export interface Props { | |||
appState: AppState; |
@@ -21,7 +21,7 @@ import * as React from 'react'; | |||
import { changePassword } from '../../api/users'; | |||
import withAppStateContext from '../../app/components/app-state/withAppStateContext'; | |||
import { Location, withRouter } from '../../components/hoc/withRouter'; | |||
import { AppState } from '../../types/types'; | |||
import { AppState } from '../../types/appstate'; | |||
import ChangeAdminPasswordAppRenderer from './ChangeAdminPasswordAppRenderer'; | |||
import { DEFAULT_ADMIN_LOGIN, DEFAULT_ADMIN_PASSWORD } from './constants'; | |||
@@ -26,7 +26,8 @@ import DeferredSpinner from '../../../components/ui/DeferredSpinner'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { formatMeasure } from '../../../helpers/measures'; | |||
import { getIssuesUrl } from '../../../helpers/urls'; | |||
import { AppState, RuleDetails } from '../../../types/types'; | |||
import { AppState } from '../../../types/appstate'; | |||
import { RuleDetails } from '../../../types/types'; | |||
interface Props { | |||
appState: AppState; |
@@ -24,7 +24,7 @@ import ChevronsIcon from '../../../components/icons/ChevronsIcon'; | |||
import { translate, translateWithParameters } from '../../../helpers/l10n'; | |||
import { getBaseUrl } from '../../../helpers/system'; | |||
import { AlmKeys } from '../../../types/alm-settings'; | |||
import { AppState } from '../../../types/types'; | |||
import { AppState } from '../../../types/appstate'; | |||
import { CreateProjectModes } from './types'; | |||
export interface CreateProjectModeSelectionProps { |
@@ -27,7 +27,8 @@ import { whenLoggedIn } from '../../../components/hoc/whenLoggedIn'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { getProjectUrl } from '../../../helpers/urls'; | |||
import { AlmKeys, AlmSettingsInstance } from '../../../types/alm-settings'; | |||
import { AppState, LoggedInUser } from '../../../types/types'; | |||
import { AppState } from '../../../types/appstate'; | |||
import { LoggedInUser } from '../../../types/types'; | |||
import AlmBindingDefinitionForm from '../../settings/components/almIntegration/AlmBindingDefinitionForm'; | |||
import AzureProjectCreate from './AzureProjectCreate'; | |||
import BitbucketCloudProjectCreate from './BitbucketCloudProjectCreate'; |
@@ -65,7 +65,7 @@ exports[`should show warnning when not all projects are accessible 1`] = ` | |||
displayReset={true} | |||
onReset={[Function]} | |||
/> | |||
<Connect(Sidebar) | |||
<withAppStateContext(Sidebar) | |||
component={ | |||
Object { | |||
"breadcrumbs": Array [], |
@@ -18,9 +18,9 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import { connect } from 'react-redux'; | |||
import withAppStateContext from '../../../app/components/app-state/withAppStateContext'; | |||
import { isBranch, isPullRequest } from '../../../helpers/branch-like'; | |||
import { getGlobalSettingValue, Store } from '../../../store/rootReducer'; | |||
import { AppState } from '../../../types/appstate'; | |||
import { BranchLike } from '../../../types/branch-like'; | |||
import { ComponentQualifier, isApplication, isPortfolioLike } from '../../../types/component'; | |||
import { | |||
@@ -29,6 +29,7 @@ import { | |||
ReferencedLanguage, | |||
ReferencedRule | |||
} from '../../../types/issues'; | |||
import { GlobalSettingKeys } from '../../../types/settings'; | |||
import { Component, Dict, UserBase } from '../../../types/types'; | |||
import { Query } from '../utils'; | |||
import AssigneeFacet from './AssigneeFacet'; | |||
@@ -48,6 +49,7 @@ import TagFacet from './TagFacet'; | |||
import TypeFacet from './TypeFacet'; | |||
export interface Props { | |||
appState: AppState; | |||
branchLike?: BranchLike; | |||
component: Component | undefined; | |||
createdAfterIncludesTime: boolean; | |||
@@ -64,7 +66,6 @@ export interface Props { | |||
referencedLanguages: Dict<ReferencedLanguage>; | |||
referencedRules: Dict<ReferencedRule>; | |||
referencedUsers: Dict<UserBase>; | |||
disableDeveloperAggregatedInfo: boolean; | |||
} | |||
export class Sidebar extends React.PureComponent<Props> { | |||
@@ -108,6 +109,7 @@ export class Sidebar extends React.PureComponent<Props> { | |||
render() { | |||
const { | |||
appState: { settings }, | |||
component, | |||
createdAfterIncludesTime, | |||
facets, | |||
@@ -116,6 +118,9 @@ export class Sidebar extends React.PureComponent<Props> { | |||
branchLike | |||
} = this.props; | |||
const disableDeveloperAggregatedInfo = | |||
settings[GlobalSettingKeys.DeveloperAggregatedInfoDisabled] === 'true'; | |||
const branch = | |||
(isBranch(branchLike) && branchLike.name) || | |||
(isPullRequest(branchLike) && branchLike.branch) || | |||
@@ -255,7 +260,7 @@ export class Sidebar extends React.PureComponent<Props> { | |||
/> | |||
)} | |||
{this.renderComponentFacets()} | |||
{!this.props.myIssues && !this.props.disableDeveloperAggregatedInfo && ( | |||
{!this.props.myIssues && !disableDeveloperAggregatedInfo && ( | |||
<AssigneeFacet | |||
assigned={query.assigned} | |||
assignees={query.assignees} | |||
@@ -269,7 +274,7 @@ export class Sidebar extends React.PureComponent<Props> { | |||
stats={facets.assignees} | |||
/> | |||
)} | |||
{displayAuthorFacet && !this.props.disableDeveloperAggregatedInfo && ( | |||
{displayAuthorFacet && !disableDeveloperAggregatedInfo && ( | |||
<AuthorFacet | |||
author={query.author} | |||
component={component} | |||
@@ -287,14 +292,4 @@ export class Sidebar extends React.PureComponent<Props> { | |||
} | |||
} | |||
export const mapStateToProps = (state: Store) => { | |||
const disableDeveloperAggregatedInfo = getGlobalSettingValue( | |||
state, | |||
'sonar.developerAggregatedInfo.disabled' | |||
); | |||
return { | |||
disableDeveloperAggregatedInfo: disableDeveloperAggregatedInfo?.value === true.toString() | |||
}; | |||
}; | |||
export default connect(mapStateToProps)(Sidebar); | |||
export default withAppStateContext(Sidebar); |
@@ -21,12 +21,11 @@ import { shallow, ShallowWrapper } from 'enzyme'; | |||
import { flatten } from 'lodash'; | |||
import * as React from 'react'; | |||
import { mockComponent } from '../../../../helpers/mocks/component'; | |||
import { getGlobalSettingValue } from '../../../../store/rootReducer'; | |||
import { mockAppState } from '../../../../helpers/testMocks'; | |||
import { GlobalSettingKeys } from '../../../../types/settings'; | |||
import { ComponentQualifier } from '../../../../types/component'; | |||
import { Query } from '../../utils'; | |||
import { mapStateToProps, Sidebar } from '../Sidebar'; | |||
jest.mock('../../../../store/rootReducer', () => ({ getGlobalSettingValue: jest.fn() })); | |||
import { Sidebar } from '../Sidebar'; | |||
it('should render facets for global page', () => { | |||
expect(renderSidebar()).toMatchSnapshot(); | |||
@@ -52,16 +51,13 @@ it('should render facets when my issues are selected', () => { | |||
}); | |||
it('should not render developer nominative facets when asked not to', () => { | |||
expect(renderSidebar({ disableDeveloperAggregatedInfo: true })).toMatchSnapshot(); | |||
}); | |||
it('should init the component with the proper store value', () => { | |||
mapStateToProps({} as any); | |||
expect(getGlobalSettingValue).toHaveBeenCalledWith( | |||
expect.any(Object), | |||
'sonar.developerAggregatedInfo.disabled' | |||
); | |||
expect( | |||
renderSidebar({ | |||
appState: mockAppState({ | |||
settings: { [GlobalSettingKeys.DeveloperAggregatedInfoDisabled]: 'true' } | |||
}) | |||
}) | |||
).toMatchSnapshot(); | |||
}); | |||
const renderSidebar = (props?: Partial<Sidebar['props']>) => { | |||
@@ -69,6 +65,9 @@ const renderSidebar = (props?: Partial<Sidebar['props']>) => { | |||
mapChildren( | |||
shallow<Sidebar>( | |||
<Sidebar | |||
appState={mockAppState({ | |||
settings: { [GlobalSettingKeys.DeveloperAggregatedInfoDisabled]: 'false' } | |||
})} | |||
component={undefined} | |||
createdAfterIncludesTime={false} | |||
facets={{}} | |||
@@ -84,7 +83,6 @@ const renderSidebar = (props?: Partial<Sidebar['props']>) => { | |||
referencedLanguages={{}} | |||
referencedRules={{}} | |||
referencedUsers={{}} | |||
disableDeveloperAggregatedInfo={false} | |||
{...props} | |||
/> | |||
) |
@@ -41,7 +41,7 @@ exports[`should render 1`] = ` | |||
exports[`test behavior should correctly render facet item 1`] = ` | |||
<React.Fragment> | |||
<Connect(Avatar) | |||
<withAppStateContext(Avatar) | |||
className="little-spacer-right" | |||
name="Name Baz" | |||
size={16} | |||
@@ -52,7 +52,7 @@ exports[`test behavior should correctly render facet item 1`] = ` | |||
exports[`test behavior should correctly render facet item 2`] = ` | |||
<React.Fragment> | |||
<Connect(Avatar) | |||
<withAppStateContext(Avatar) | |||
className="little-spacer-right" | |||
name="foo" | |||
size={16} | |||
@@ -63,7 +63,7 @@ exports[`test behavior should correctly render facet item 2`] = ` | |||
exports[`test behavior should correctly render search result correctly 1`] = ` | |||
<React.Fragment> | |||
<Connect(Avatar) | |||
<withAppStateContext(Avatar) | |||
className="little-spacer-right" | |||
name="Name Bar" | |||
size={16} | |||
@@ -80,7 +80,7 @@ exports[`test behavior should correctly render search result correctly 1`] = ` | |||
exports[`test behavior should correctly render search result correctly 2`] = ` | |||
<React.Fragment> | |||
<Connect(Avatar) | |||
<withAppStateContext(Avatar) | |||
className="little-spacer-right" | |||
name="foo" | |||
size={16} |
@@ -18,35 +18,27 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import { connect } from 'react-redux'; | |||
import AdminContext from '../../app/components/AdminContext'; | |||
import withAppStateContext from '../../app/components/app-state/withAppStateContext'; | |||
import { getGlobalSettingValue, Store } from '../../store/rootReducer'; | |||
import { AppState } from '../../types/appstate'; | |||
import { EditionKey } from '../../types/editions'; | |||
import { AppState, RawQuery } from '../../types/types'; | |||
import { fetchValues } from '../settings/store/actions'; | |||
import { GlobalSettingKeys } from '../../types/settings'; | |||
import { RawQuery } from '../../types/types'; | |||
import App from './App'; | |||
interface OwnProps { | |||
export interface MarketplaceAppContainerProps { | |||
location: { pathname: string; query: RawQuery }; | |||
appState: AppState; | |||
} | |||
interface StateToProps { | |||
fetchValues: typeof fetchValues; | |||
updateCenterActive: boolean; | |||
} | |||
function WithAdminContext(props: StateToProps & OwnProps) { | |||
React.useEffect(() => { | |||
props.fetchValues(['sonar.updatecenter.activate']); | |||
}); | |||
export function MarketplaceAppContainer(props: MarketplaceAppContainerProps) { | |||
const { appState, location } = props; | |||
const propsToPass = { | |||
location: props.location, | |||
updateCenterActive: props.updateCenterActive, | |||
currentEdition: props.appState.edition as EditionKey, | |||
standaloneMode: props.appState.standalone | |||
location, | |||
updateCenterActive: appState.settings[GlobalSettingKeys.UpdatecenterActivated] === 'true', | |||
currentEdition: appState.edition as EditionKey, | |||
standaloneMode: appState.standalone | |||
}; | |||
return ( | |||
@@ -62,13 +54,4 @@ function WithAdminContext(props: StateToProps & OwnProps) { | |||
); | |||
} | |||
const mapDispatchToProps = { fetchValues }; | |||
const mapStateToProps = (state: Store) => { | |||
const updateCenterActive = getGlobalSettingValue(state, 'sonar.updatecenter.activate'); | |||
return { | |||
updateCenterActive: Boolean(updateCenterActive && updateCenterActive.value === 'true') | |||
}; | |||
}; | |||
export default connect(mapStateToProps, mapDispatchToProps)(withAppStateContext(WithAdminContext)); | |||
export default withAppStateContext(MarketplaceAppContainer); |
@@ -1,52 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2022 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { connect } from 'react-redux'; | |||
import { mockStore } from '../../../helpers/testMocks'; | |||
import { getGlobalSettingValue } from '../../../store/rootReducer'; | |||
import '../AppContainer'; | |||
jest.mock('react-redux', () => ({ | |||
connect: jest.fn(() => (a: any) => a) | |||
})); | |||
jest.mock('../../../store/rootReducer', () => { | |||
return { | |||
getGlobalSettingValue: jest.fn() | |||
}; | |||
}); | |||
describe('redux', () => { | |||
it('should correctly map state and dispatch props', () => { | |||
const store = mockStore(); | |||
const updateCenterActive = true; | |||
(getGlobalSettingValue as jest.Mock).mockReturnValueOnce({ | |||
value: `${updateCenterActive}` | |||
}); | |||
const [mapStateToProps] = (connect as jest.Mock).mock.calls[0]; | |||
const props = mapStateToProps(store); | |||
expect(props).toEqual({ | |||
updateCenterActive | |||
}); | |||
expect(getGlobalSettingValue).toHaveBeenCalledWith(store, 'sonar.updatecenter.activate'); | |||
}); | |||
}); |
@@ -0,0 +1,48 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2022 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { shallow } from 'enzyme'; | |||
import React from 'react'; | |||
import { mockAppState, mockLocation } from '../../../helpers/testMocks'; | |||
import { GlobalSettingKeys } from '../../../types/settings'; | |||
import { EditionKey } from '../../../types/editions'; | |||
import { MarketplaceAppContainer, MarketplaceAppContainerProps } from '../MarketplaceAppContainer'; | |||
it('should render correctly', () => { | |||
expect(shallowRender().dive()).toMatchSnapshot('default'); | |||
expect( | |||
shallowRender({ | |||
appState: mockAppState({ | |||
settings: { | |||
[GlobalSettingKeys.UpdatecenterActivated]: 'true' | |||
} | |||
}) | |||
}).dive() | |||
).toMatchSnapshot('update center active'); | |||
}); | |||
function shallowRender(overrides: Partial<MarketplaceAppContainerProps> = {}) { | |||
return shallow<MarketplaceAppContainerProps>( | |||
<MarketplaceAppContainer | |||
appState={mockAppState({ edition: EditionKey.community, standalone: true })} | |||
location={mockLocation()} | |||
{...overrides} | |||
/> | |||
); | |||
} |
@@ -0,0 +1,54 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly: default 1`] = ` | |||
<withRouter(App) | |||
currentEdition="community" | |||
fetchPendingPlugins={[Function]} | |||
location={ | |||
Object { | |||
"action": "PUSH", | |||
"hash": "", | |||
"key": "key", | |||
"pathname": "/path", | |||
"query": Object {}, | |||
"search": "", | |||
"state": Object {}, | |||
} | |||
} | |||
pendingPlugins={ | |||
Object { | |||
"installing": Array [], | |||
"removing": Array [], | |||
"updating": Array [], | |||
} | |||
} | |||
standaloneMode={true} | |||
updateCenterActive={false} | |||
/> | |||
`; | |||
exports[`should render correctly: update center active 1`] = ` | |||
<withRouter(App) | |||
currentEdition="community" | |||
fetchPendingPlugins={[Function]} | |||
location={ | |||
Object { | |||
"action": "PUSH", | |||
"hash": "", | |||
"key": "key", | |||
"pathname": "/path", | |||
"query": Object {}, | |||
"search": "", | |||
"state": Object {}, | |||
} | |||
} | |||
pendingPlugins={ | |||
Object { | |||
"installing": Array [], | |||
"removing": Array [], | |||
"updating": Array [], | |||
} | |||
} | |||
updateCenterActive={true} | |||
/> | |||
`; |
@@ -21,7 +21,7 @@ import { lazyLoadComponent } from '../../components/lazyLoadComponent'; | |||
const routes = [ | |||
{ | |||
indexRoute: { component: lazyLoadComponent(() => import('./AppContainer')) } | |||
indexRoute: { component: lazyLoadComponent(() => import('./MarketplaceAppContainer')) } | |||
} | |||
]; | |||
@@ -24,9 +24,10 @@ import { Router, withRouter } from '../../../components/hoc/withRouter'; | |||
import { lazyLoadComponent } from '../../../components/lazyLoadComponent'; | |||
import { isPullRequest } from '../../../helpers/branch-like'; | |||
import { ProjectAlmBindingResponse } from '../../../types/alm-settings'; | |||
import { AppState } from '../../../types/appstate'; | |||
import { BranchLike } from '../../../types/branch-like'; | |||
import { isPortfolioLike } from '../../../types/component'; | |||
import { AppState, Component } from '../../../types/types'; | |||
import { Component } from '../../../types/types'; | |||
import BranchOverview from '../branches/BranchOverview'; | |||
const EmptyOverview = lazyLoadComponent(() => import('./EmptyOverview')); |
@@ -19,7 +19,8 @@ | |||
*/ | |||
import * as React from 'react'; | |||
import Tooltip from '../../../components/controls/Tooltip'; | |||
import { getLeakValue, getRatingTooltip } from '../../../components/measure/utils'; | |||
import RatingTooltipContent from '../../../components/measure/RatingTooltipContent'; | |||
import { getLeakValue } from '../../../components/measure/utils'; | |||
import DrilldownLink from '../../../components/shared/DrilldownLink'; | |||
import Rating from '../../../components/ui/Rating'; | |||
import { findMeasure } from '../../../helpers/measures'; | |||
@@ -50,10 +51,9 @@ function renderRatingLink(props: IssueRatingProps) { | |||
} | |||
const value = measure && (useDiffMetric ? getLeakValue(measure) : measure.value); | |||
const tooltip = value && getRatingTooltip(rating, Number(value)); | |||
return ( | |||
<Tooltip overlay={tooltip}> | |||
<Tooltip overlay={value && <RatingTooltipContent metricKey={rating} value={value} />}> | |||
<span> | |||
<DrilldownLink | |||
branchLike={branchLike} |
@@ -8,7 +8,12 @@ exports[`should render correctly for bugs 1`] = ` | |||
metric_domain.Reliability | |||
</span> | |||
<Tooltip | |||
overlay="metric.reliability_rating.tooltip.A" | |||
overlay={ | |||
<withAppStateContext(RatingTooltipContent) | |||
metricKey="reliability_rating" | |||
value="1.0" | |||
/> | |||
} | |||
> | |||
<span> | |||
<DrilldownLink | |||
@@ -43,7 +48,12 @@ exports[`should render correctly for bugs 2`] = ` | |||
metric_domain.Reliability | |||
</span> | |||
<Tooltip | |||
overlay="metric.reliability_rating.tooltip.A" | |||
overlay={ | |||
<withAppStateContext(RatingTooltipContent) | |||
metricKey="new_reliability_rating" | |||
value="1.0" | |||
/> | |||
} | |||
> | |||
<span> | |||
<DrilldownLink | |||
@@ -78,7 +88,12 @@ exports[`should render correctly for code smells 1`] = ` | |||
metric_domain.Maintainability | |||
</span> | |||
<Tooltip | |||
overlay="metric.sqale_rating.tooltip.A.0.0%" | |||
overlay={ | |||
<withAppStateContext(RatingTooltipContent) | |||
metricKey="sqale_rating" | |||
value="1.0" | |||
/> | |||
} | |||
> | |||
<span> | |||
<DrilldownLink | |||
@@ -113,7 +128,12 @@ exports[`should render correctly for code smells 2`] = ` | |||
metric_domain.Maintainability | |||
</span> | |||
<Tooltip | |||
overlay="metric.sqale_rating.tooltip.A.0.0%" | |||
overlay={ | |||
<withAppStateContext(RatingTooltipContent) | |||
metricKey="new_maintainability_rating" | |||
value="1.0" | |||
/> | |||
} | |||
> | |||
<span> | |||
<DrilldownLink | |||
@@ -148,7 +168,12 @@ exports[`should render correctly for vulnerabilities 1`] = ` | |||
metric_domain.Security | |||
</span> | |||
<Tooltip | |||
overlay="metric.security_rating.tooltip.A" | |||
overlay={ | |||
<withAppStateContext(RatingTooltipContent) | |||
metricKey="security_rating" | |||
value="1.0" | |||
/> | |||
} | |||
> | |||
<span> | |||
<DrilldownLink | |||
@@ -183,7 +208,12 @@ exports[`should render correctly for vulnerabilities 2`] = ` | |||
metric_domain.Security | |||
</span> | |||
<Tooltip | |||
overlay="metric.security_rating.tooltip.A" | |||
overlay={ | |||
<withAppStateContext(RatingTooltipContent) | |||
metricKey="new_security_rating" | |||
value="1.0" | |||
/> | |||
} | |||
> | |||
<span> | |||
<DrilldownLink |
@@ -24,7 +24,8 @@ import { getPermissionTemplates } from '../../../api/permissions'; | |||
import withAppStateContext from '../../../app/components/app-state/withAppStateContext'; | |||
import Suggestions from '../../../app/components/embed-docs-modal/Suggestions'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { AppState, Permission, PermissionTemplate } from '../../../types/types'; | |||
import { AppState } from '../../../types/appstate'; | |||
import { Permission, PermissionTemplate } from '../../../types/types'; | |||
import '../../permissions/styles.css'; | |||
import { mergeDefaultsToTemplates, mergePermissionsToTemplates, sortPermissions } from '../utils'; | |||
import Home from './Home'; |
@@ -20,8 +20,9 @@ | |||
import * as React from 'react'; | |||
import withAppStateContext from '../../../../app/components/app-state/withAppStateContext'; | |||
import ListFooter from '../../../../components/controls/ListFooter'; | |||
import { AppState } from '../../../../types/appstate'; | |||
import { ComponentQualifier } from '../../../../types/component'; | |||
import { AppState, Paging, PermissionGroup, PermissionUser } from '../../../../types/types'; | |||
import { Paging, PermissionGroup, PermissionUser } from '../../../../types/types'; | |||
import HoldersList from '../../shared/components/HoldersList'; | |||
import SearchForm from '../../shared/components/SearchForm'; | |||
import { |
@@ -90,7 +90,7 @@ exports[`should render correctly: default 1`] = ` | |||
<div | |||
className="display-flex-center" | |||
> | |||
<Connect(Avatar) | |||
<withAppStateContext(Avatar) | |||
className="text-middle big-spacer-right flex-0" | |||
name="John Doe" | |||
size={36} |
@@ -27,9 +27,9 @@ import AlertSuccessIcon from '../../../components/icons/AlertSuccessIcon'; | |||
import DeferredSpinner from '../../../components/ui/DeferredSpinner'; | |||
import { isBranch, sortBranches } from '../../../helpers/branch-like'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { AppState } from '../../../types/appstate'; | |||
import { Branch, BranchLike } from '../../../types/branch-like'; | |||
import { | |||
AppState, | |||
Component, | |||
NewCodePeriod, | |||
NewCodePeriodSettingType, |
@@ -20,8 +20,8 @@ | |||
import * as React from 'react'; | |||
import { getValues } from '../../../api/settings'; | |||
import withAppStateContext from '../../../app/components/app-state/withAppStateContext'; | |||
import { AppState } from '../../../types/appstate'; | |||
import { SettingsKey } from '../../../types/settings'; | |||
import { AppState } from '../../../types/types'; | |||
import LifetimeInformationRenderer from './LifetimeInformationRenderer'; | |||
interface Props { |
@@ -23,9 +23,10 @@ import { getStatus } from '../../api/project-dump'; | |||
import withAppStateContext from '../../app/components/app-state/withAppStateContext'; | |||
import throwGlobalError from '../../app/utils/throwGlobalError'; | |||
import { translate } from '../../helpers/l10n'; | |||
import { AppState } from '../../types/appstate'; | |||
import { DumpStatus, DumpTask } from '../../types/project-dump'; | |||
import { TaskStatuses, TaskTypes } from '../../types/tasks'; | |||
import { AppState, Component } from '../../types/types'; | |||
import { Component } from '../../types/types'; | |||
import Export from './components/Export'; | |||
import Import from './components/Import'; | |||
import './styles.css'; |
@@ -33,8 +33,9 @@ import { translate } from '../../../helpers/l10n'; | |||
import { addSideBarClass, removeSideBarClass } from '../../../helpers/pages'; | |||
import { get, save } from '../../../helpers/storage'; | |||
import { isLoggedIn } from '../../../helpers/users'; | |||
import { AppState } from '../../../types/appstate'; | |||
import { ComponentQualifier } from '../../../types/component'; | |||
import { AppState, CurrentUser, RawQuery } from '../../../types/types'; | |||
import { CurrentUser, RawQuery } from '../../../types/types'; | |||
import { hasFilterParams, hasViewParams, parseUrlQuery, Query } from '../query'; | |||
import '../styles.css'; | |||
import { Facets, Project } from '../types'; |
@@ -27,9 +27,10 @@ import { Router, withRouter } from '../../../components/hoc/withRouter'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { getComponentAdminUrl, getComponentOverviewUrl } from '../../../helpers/urls'; | |||
import { hasGlobalPermission } from '../../../helpers/users'; | |||
import { AppState } from '../../../types/appstate'; | |||
import { ComponentQualifier } from '../../../types/component'; | |||
import { Permissions } from '../../../types/permissions'; | |||
import { AppState, LoggedInUser } from '../../../types/types'; | |||
import { LoggedInUser } from '../../../types/types'; | |||
export interface ApplicationCreationProps { | |||
appState: AppState; |
@@ -29,7 +29,8 @@ import SearchBox from '../../components/controls/SearchBox'; | |||
import SelectLegacy from '../../components/controls/SelectLegacy'; | |||
import QualifierIcon from '../../components/icons/QualifierIcon'; | |||
import { translate } from '../../helpers/l10n'; | |||
import { AppState, Visibility } from '../../types/types'; | |||
import { AppState } from '../../types/appstate'; | |||
import { Visibility } from '../../types/types'; | |||
import BulkApplyTemplateModal from './BulkApplyTemplateModal'; | |||
import DeleteModal from './DeleteModal'; | |||
@@ -27,14 +27,9 @@ import ModalButton from '../../../components/controls/ModalButton'; | |||
import { Alert } from '../../../components/ui/Alert'; | |||
import { getLocalizedMetricName, translate } from '../../../helpers/l10n'; | |||
import { isDiffMetric } from '../../../helpers/measures'; | |||
import { AppState } from '../../../types/appstate'; | |||
import { MetricKey } from '../../../types/metrics'; | |||
import { | |||
AppState, | |||
Condition as ConditionType, | |||
Dict, | |||
Metric, | |||
QualityGate | |||
} from '../../../types/types'; | |||
import { Condition as ConditionType, Dict, Metric, QualityGate } from '../../../types/types'; | |||
import Condition from './Condition'; | |||
import ConditionModal from './ConditionModal'; | |||
@@ -25,7 +25,7 @@ exports[`should render correctly: user 1`] = ` | |||
<div | |||
className="display-flex-center permission-list-item padded" | |||
> | |||
<Connect(Avatar) | |||
<withAppStateContext(Avatar) | |||
className="spacer-right" | |||
name="John Doe" | |||
size={32} |
@@ -334,7 +334,7 @@ exports[`should render options correctly: group 1`] = ` | |||
exports[`should render options correctly: user 1`] = ` | |||
<React.Fragment> | |||
<Connect(Avatar) | |||
<withAppStateContext(Avatar) | |||
hash="A" | |||
name="name" | |||
size={16} |
@@ -8,7 +8,7 @@ exports[`renders 1`] = ` | |||
className="pull-right spacer-top spacer-left spacer-right button-small" | |||
onClick={[Function]} | |||
/> | |||
<Connect(Avatar) | |||
<withAppStateContext(Avatar) | |||
className="pull-left spacer-right" | |||
name="Luke Skywalker" | |||
size={32} |
@@ -10,7 +10,7 @@ exports[`should render correctly: default 1`] = ` | |||
<div | |||
className="display-flex-center" | |||
> | |||
<Connect(Avatar) | |||
<withAppStateContext(Avatar) | |||
className="little-spacer-right" | |||
name="Luke Skywalker" | |||
size={20} | |||
@@ -49,7 +49,7 @@ exports[`should render correctly: default 1`] = ` | |||
<div | |||
className="display-flex-center" | |||
> | |||
<Connect(Avatar) | |||
<withAppStateContext(Avatar) | |||
className="little-spacer-right" | |||
name="Luke Skywalker" | |||
size={20} | |||
@@ -88,7 +88,7 @@ exports[`should render correctly: default 1`] = ` | |||
<div | |||
className="display-flex-center" | |||
> | |||
<Connect(Avatar) | |||
<withAppStateContext(Avatar) | |||
className="little-spacer-right" | |||
name="John Doe" | |||
size={20} | |||
@@ -130,7 +130,7 @@ exports[`should render correctly: default 1`] = ` | |||
<div | |||
className="display-flex-center" | |||
> | |||
<Connect(Avatar) | |||
<withAppStateContext(Avatar) | |||
className="little-spacer-right" | |||
name="John Doe" | |||
size={20} | |||
@@ -172,7 +172,7 @@ exports[`should render correctly: default 1`] = ` | |||
<div | |||
className="display-flex-center" | |||
> | |||
<Connect(Avatar) | |||
<withAppStateContext(Avatar) | |||
className="little-spacer-right" | |||
name="John Doe" | |||
size={20} | |||
@@ -255,7 +255,7 @@ exports[`should render correctly: show full list 1`] = ` | |||
<div | |||
className="display-flex-center" | |||
> | |||
<Connect(Avatar) | |||
<withAppStateContext(Avatar) | |||
className="little-spacer-right" | |||
name="Luke Skywalker" | |||
size={20} | |||
@@ -294,7 +294,7 @@ exports[`should render correctly: show full list 1`] = ` | |||
<div | |||
className="display-flex-center" | |||
> | |||
<Connect(Avatar) | |||
<withAppStateContext(Avatar) | |||
className="little-spacer-right" | |||
name="Luke Skywalker" | |||
size={20} | |||
@@ -333,7 +333,7 @@ exports[`should render correctly: show full list 1`] = ` | |||
<div | |||
className="display-flex-center" | |||
> | |||
<Connect(Avatar) | |||
<withAppStateContext(Avatar) | |||
className="little-spacer-right" | |||
name="John Doe" | |||
size={20} | |||
@@ -375,7 +375,7 @@ exports[`should render correctly: show full list 1`] = ` | |||
<div | |||
className="display-flex-center" | |||
> | |||
<Connect(Avatar) | |||
<withAppStateContext(Avatar) | |||
className="little-spacer-right" | |||
name="John Doe" | |||
size={20} | |||
@@ -417,7 +417,7 @@ exports[`should render correctly: show full list 1`] = ` | |||
<div | |||
className="display-flex-center" | |||
> | |||
<Connect(Avatar) | |||
<withAppStateContext(Avatar) | |||
className="little-spacer-right" | |||
name="John Doe" | |||
size={20} | |||
@@ -459,7 +459,7 @@ exports[`should render correctly: show full list 1`] = ` | |||
<div | |||
className="display-flex-center" | |||
> | |||
<Connect(Avatar) | |||
<withAppStateContext(Avatar) | |||
className="little-spacer-right" | |||
name="john.doe" | |||
size={20} | |||
@@ -501,7 +501,7 @@ exports[`should render correctly: show full list 1`] = ` | |||
<div | |||
className="display-flex-center" | |||
> | |||
<Connect(Avatar) | |||
<withAppStateContext(Avatar) | |||
className="little-spacer-right" | |||
name="John Doe" | |||
size={20} | |||
@@ -592,7 +592,7 @@ exports[`should render correctly: show full list 1`] = ` | |||
<div | |||
className="display-flex-center" | |||
> | |||
<Connect(Avatar) | |||
<withAppStateContext(Avatar) | |||
className="little-spacer-right" | |||
name="John Doe" | |||
size={20} |
@@ -71,7 +71,7 @@ exports[`should render correctly: open with results 1`] = ` | |||
key="john.doe" | |||
onClick={[Function]} | |||
> | |||
<Connect(Avatar) | |||
<withAppStateContext(Avatar) | |||
className="spacer-right" | |||
name="John Doe" | |||
size={16} | |||
@@ -83,7 +83,7 @@ exports[`should render correctly: open with results 1`] = ` | |||
key="highlighted" | |||
onClick={[Function]} | |||
> | |||
<Connect(Avatar) | |||
<withAppStateContext(Avatar) | |||
className="spacer-right" | |||
name="John Doe" | |||
size={16} |
@@ -23,7 +23,8 @@ import * as React from 'react'; | |||
import { IndexLink } from 'react-router'; | |||
import withAppStateContext from '../../../app/components/app-state/withAppStateContext'; | |||
import { getGlobalSettingsUrl, getProjectSettingsUrl } from '../../../helpers/urls'; | |||
import { AppState, Component } from '../../../types/types'; | |||
import { AppState } from '../../../types/appstate'; | |||
import { Component } from '../../../types/types'; | |||
import { getCategoryName } from '../utils'; | |||
import { ADDITIONAL_CATEGORIES } from './AdditionalCategories'; | |||
import CATEGORY_OVERRIDES from './CategoryOverrides'; |
@@ -34,8 +34,9 @@ import { | |||
AlmSettingsBindingStatus, | |||
AlmSettingsBindingStatusType | |||
} from '../../../../types/alm-settings'; | |||
import { AppState } from '../../../../types/appstate'; | |||
import { ExtendedSettingDefinition } from '../../../../types/settings'; | |||
import { AppState, Dict } from '../../../../types/types'; | |||
import { Dict } from '../../../../types/types'; | |||
import AlmIntegrationRenderer from './AlmIntegrationRenderer'; | |||
interface Props extends Pick<WithRouterProps, 'location' | 'router'> { |
@@ -24,8 +24,8 @@ import Tooltip from '../../../../components/controls/Tooltip'; | |||
import { getEdition, getEditionUrl } from '../../../../helpers/editions'; | |||
import { translate } from '../../../../helpers/l10n'; | |||
import { AlmKeys } from '../../../../types/alm-settings'; | |||
import { AppState } from '../../../../types/appstate'; | |||
import { EditionKey } from '../../../../types/editions'; | |||
import { AppState } from '../../../../types/types'; | |||
export interface CreationTooltipProps { | |||
alm: AlmKeys; |
@@ -32,8 +32,9 @@ import { | |||
AlmSettingsInstance, | |||
ProjectAlmBindingResponse | |||
} from '../../../../types/alm-settings'; | |||
import { AppState } from '../../../../types/appstate'; | |||
import { EditionKey } from '../../../../types/editions'; | |||
import { AppState, Dict } from '../../../../types/types'; | |||
import { Dict } from '../../../../types/types'; | |||
export interface AlmSpecificFormProps { | |||
alm: AlmKeys; |
@@ -1,55 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2022 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { fetchValues, setValues } from '../actions'; | |||
jest.mock('../../../../api/settings', () => { | |||
const { mockSettingValue } = jest.requireActual('../../../../helpers/mocks/settings'); | |||
return { | |||
getValues: jest.fn().mockResolvedValue([mockSettingValue()]) | |||
}; | |||
}); | |||
it('should setValues correctly', () => { | |||
const dispatch = jest.fn(); | |||
setValues(['test'], [{ key: 'test', value: 'foo' }])(dispatch); | |||
expect(dispatch).toHaveBeenCalledWith({ | |||
component: undefined, | |||
settings: [ | |||
{ | |||
key: 'test', | |||
value: 'foo' | |||
} | |||
], | |||
type: 'RECEIVE_VALUES', | |||
updateKeys: ['test'] | |||
}); | |||
}); | |||
it('should fetchValue correclty', async () => { | |||
const dispatch = jest.fn(); | |||
await fetchValues(['test'], 'foo')(dispatch); | |||
expect(dispatch).toHaveBeenCalledWith({ | |||
component: 'foo', | |||
settings: [{ key: 'test' }], | |||
type: 'RECEIVE_VALUES', | |||
updateKeys: ['test'] | |||
}); | |||
expect(dispatch).toHaveBeenCalledWith({ type: 'CLOSE_ALL_GLOBAL_MESSAGES' }); | |||
}); |
@@ -1,39 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2022 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { Dispatch } from 'redux'; | |||
import { getValues } from '../../../api/settings'; | |||
import { closeAllGlobalMessages } from '../../../store/globalMessages'; | |||
import { receiveValues } from './values'; | |||
export function fetchValues(keys: string[], component?: string) { | |||
return (dispatch: Dispatch) => | |||
getValues({ keys: keys.join(), component }).then(settings => { | |||
dispatch(receiveValues(keys, settings, component)); | |||
dispatch(closeAllGlobalMessages()); | |||
}); | |||
} | |||
export function setValues( | |||
keys: string[], | |||
settings: Array<{ key: string; value?: string }>, | |||
component?: string | |||
) { | |||
return (dispatch: Dispatch) => dispatch(receiveValues(keys, settings, component)); | |||
} |
@@ -1,78 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2022 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { keyBy, omit } from 'lodash'; | |||
import { combineReducers } from 'redux'; | |||
import { ActionType } from '../../../store/utils/actions'; | |||
import { SettingValue } from '../../../types/settings'; | |||
import { Dict } from '../../../types/types'; | |||
enum Actions { | |||
receiveValues = 'RECEIVE_VALUES' | |||
} | |||
type Action = ActionType<typeof receiveValues, Actions.receiveValues>; | |||
type SettingsState = Dict<SettingValue>; | |||
export interface State { | |||
components: Dict<SettingsState>; | |||
global: SettingsState; | |||
} | |||
export function receiveValues( | |||
updateKeys: string[], | |||
settings: Array<{ key: string; value?: string }>, | |||
component?: string | |||
) { | |||
return { type: Actions.receiveValues, updateKeys, settings, component }; | |||
} | |||
function components(state: State['components'] = {}, action: Action) { | |||
const { component: key } = action; | |||
if (!key) { | |||
return state; | |||
} | |||
if (action.type === Actions.receiveValues) { | |||
const settingsByKey = keyBy(action.settings, 'key'); | |||
return { ...state, [key]: { ...omit(state[key] || {}, action.updateKeys), ...settingsByKey } }; | |||
} | |||
return state; | |||
} | |||
function global(state: State['components'] = {}, action: Action) { | |||
if (action.type === Actions.receiveValues) { | |||
if (action.component) { | |||
return state; | |||
} | |||
const settingsByKey = keyBy(action.settings, 'key'); | |||
return { ...omit(state, action.updateKeys), ...settingsByKey }; | |||
} | |||
return state; | |||
} | |||
export default combineReducers({ components, global }); | |||
export function getValue(state: State, key: string, component?: string): SettingValue | undefined { | |||
if (component) { | |||
return state.components[component] && state.components[component][key]; | |||
} | |||
return state.global[key]; | |||
} |
@@ -23,7 +23,7 @@ import { ClipboardButton } from '../../../components/controls/clipboard'; | |||
import { Alert } from '../../../components/ui/Alert'; | |||
import { toShortNotSoISOString } from '../../../helpers/dates'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { AppState } from '../../../types/types'; | |||
import { AppState } from '../../../types/appstate'; | |||
import PageActions from './PageActions'; | |||
export interface Props { |
@@ -5,7 +5,7 @@ exports[`should render correctly 1`] = ` | |||
<td | |||
className="thin nowrap text-middle" | |||
> | |||
<Connect(Avatar) | |||
<withAppStateContext(Avatar) | |||
name="One" | |||
size={36} | |||
/> | |||
@@ -92,7 +92,7 @@ exports[`should render correctly without last connection date 1`] = ` | |||
<td | |||
className="thin nowrap text-middle" | |||
> | |||
<Connect(Avatar) | |||
<withAppStateContext(Avatar) | |||
name="One" | |||
size={36} | |||
/> |
@@ -88,7 +88,7 @@ exports[`UsersSelectSearchOption should render correctly with email instead of h | |||
role="listitem" | |||
title="Administrator" | |||
> | |||
<Connect(Avatar) | |||
<withAppStateContext(Avatar) | |||
name="Administrator" | |||
size={16} | |||
/> | |||
@@ -113,7 +113,7 @@ exports[`UsersSelectSearchOption should render correctly without all parameters | |||
role="listitem" | |||
title="Administrator" | |||
> | |||
<Connect(Avatar) | |||
<withAppStateContext(Avatar) | |||
hash="7daf6c79d4802916d83f6266e24850af" | |||
name="Administrator" | |||
size={16} | |||
@@ -139,7 +139,7 @@ exports[`UsersSelectSearchValue should render correctly with a user 1`] = ` | |||
<div | |||
className="Select-value-label" | |||
> | |||
<Connect(Avatar) | |||
<withAppStateContext(Avatar) | |||
hash="7daf6c79d4802916d83f6266e24850af" | |||
name="Administrator" | |||
size={16} | |||
@@ -166,7 +166,7 @@ exports[`UsersSelectSearchValue should render correctly with email instead of ha | |||
<div | |||
className="Select-value-label" | |||
> | |||
<Connect(Avatar) | |||
<withAppStateContext(Avatar) | |||
name="Administrator" | |||
size={16} | |||
/> |
@@ -27,10 +27,11 @@ import withAppStateContext from '../../app/components/app-state/withAppStateCont | |||
import addGlobalSuccessMessage from '../../app/utils/addGlobalSuccessMessage'; | |||
import { translate, translateWithParameters } from '../../helpers/l10n'; | |||
import { isLoggedIn } from '../../helpers/users'; | |||
import { AppState } from '../../types/appstate'; | |||
import { Branch } from '../../types/branch-like'; | |||
import { ComponentQualifier } from '../../types/component'; | |||
import { ComponentReportStatus } from '../../types/component-report'; | |||
import { AppState, Component, CurrentUser } from '../../types/types'; | |||
import { Component, CurrentUser } from '../../types/types'; | |||
import { withCurrentUser } from '../hoc/withCurrentUser'; | |||
import ComponentReportActionsRenderer from './ComponentReportActionsRenderer'; | |||
@@ -22,7 +22,7 @@ import { Link } from 'react-router'; | |||
import withAppStateContext from '../../app/components/app-state/withAppStateContext'; | |||
import DetachIcon from '../../components/icons/DetachIcon'; | |||
import { isSonarCloud } from '../../helpers/system'; | |||
import { AppState } from '../../types/types'; | |||
import { AppState } from '../../types/appstate'; | |||
interface OwnProps { | |||
appState: AppState; |
@@ -32,7 +32,7 @@ exports[`should open the popup when the button is clicked 2`] = ` | |||
<span | |||
className="text-top" | |||
> | |||
<Connect(Avatar) | |||
<withAppStateContext(Avatar) | |||
className="little-spacer-right" | |||
hash="gravatarhash" | |||
name="" | |||
@@ -108,7 +108,7 @@ exports[`should render with the action 1`] = ` | |||
<span | |||
className="text-top" | |||
> | |||
<Connect(Avatar) | |||
<withAppStateContext(Avatar) | |||
className="little-spacer-right" | |||
hash="gravatarhash" | |||
name="" | |||
@@ -133,7 +133,7 @@ exports[`should render without the action when the correct rights are missing 1` | |||
<span | |||
className="text-top" | |||
> | |||
<Connect(Avatar) | |||
<withAppStateContext(Avatar) | |||
className="little-spacer-right" | |||
hash="gravatarhash" | |||
name="" |
@@ -20,7 +20,7 @@ exports[`should open the right popups when the buttons are clicked 3`] = ` | |||
className="issue-comment-author" | |||
title="John Doe" | |||
> | |||
<Connect(Avatar) | |||
<withAppStateContext(Avatar) | |||
className="little-spacer-right" | |||
hash="gravatarhash" | |||
name="John Doe" | |||
@@ -118,7 +118,7 @@ exports[`should render correctly a comment that is not updatable 1`] = ` | |||
className="issue-comment-author" | |||
title="John Doe" | |||
> | |||
<Connect(Avatar) | |||
<withAppStateContext(Avatar) | |||
className="little-spacer-right" | |||
hash="gravatarhash" | |||
name="John Doe" | |||
@@ -160,7 +160,7 @@ exports[`should render correctly a comment that is updatable 1`] = ` | |||
className="issue-comment-author" | |||
title="John Doe" | |||
> | |||
<Connect(Avatar) | |||
<withAppStateContext(Avatar) | |||
className="little-spacer-right" | |||
hash="gravatarhash" | |||
name="John Doe" | |||
@@ -255,7 +255,7 @@ exports[`should render correctly a comment with a deleted author 1`] = ` | |||
className="issue-comment-author" | |||
title="user.x_deleted.john.doe" | |||
> | |||
<Connect(Avatar) | |||
<withAppStateContext(Avatar) | |||
className="little-spacer-right" | |||
hash="gravatarhash" | |||
name="john.doe" |
@@ -39,7 +39,7 @@ exports[`should render the changelog popup correctly 1`] = ` | |||
className="text-left text-top" | |||
> | |||
<p> | |||
<Connect(Avatar) | |||
<withAppStateContext(Avatar) | |||
className="little-spacer-right" | |||
hash="gravatarhash" | |||
name="John Doe" | |||
@@ -104,7 +104,7 @@ exports[`should render the changelog popup when we have a deleted user 1`] = ` | |||
className="text-left text-top" | |||
> | |||
<p> | |||
<Connect(Avatar) | |||
<withAppStateContext(Avatar) | |||
className="little-spacer-right" | |||
name="john.doe" | |||
size={16} |
@@ -33,7 +33,7 @@ exports[`should render correctly 1`] = ` | |||
item="luke" | |||
key="luke" | |||
> | |||
<Connect(Avatar) | |||
<withAppStateContext(Avatar) | |||
className="spacer-right" | |||
name="Skywalker" | |||
size={16} |
@@ -127,7 +127,7 @@ exports[`should render correctly when assigned 1`] = ` | |||
> | |||
<span> | |||
assigned_to | |||
<Connect(Avatar) | |||
<withAppStateContext(Avatar) | |||
className="little-spacer-left little-spacer-right" | |||
name="Luke Skywalker" | |||
size={16} | |||
@@ -143,7 +143,7 @@ exports[`should render correctly when assigned 2`] = ` | |||
> | |||
<span> | |||
assigned_to | |||
<Connect(Avatar) | |||
<withAppStateContext(Avatar) | |||
className="little-spacer-left little-spacer-right" | |||
name="luke" | |||
size={16} |
@@ -22,7 +22,7 @@ import Tooltip from '../../components/controls/Tooltip'; | |||
import Level from '../../components/ui/Level'; | |||
import Rating from '../../components/ui/Rating'; | |||
import { formatMeasure } from '../../helpers/measures'; | |||
import { getRatingTooltip } from './utils'; | |||
import RatingTooltipContent from './RatingTooltipContent'; | |||
interface Props { | |||
className?: string; | |||
@@ -57,8 +57,9 @@ export default function Measure({ | |||
return <span className={className}>{formattedValue != null ? formattedValue : '–'}</span>; | |||
} | |||
const tooltip = getRatingTooltip(metricKey, Number(value)); | |||
const tooltip = <RatingTooltipContent metricKey={metricKey} value={value} />; | |||
const rating = <Rating small={small} value={value} />; | |||
if (tooltip) { | |||
return ( | |||
<Tooltip overlay={tooltip}> |
@@ -0,0 +1,94 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2022 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import withAppStateContext from '../../app/components/app-state/withAppStateContext'; | |||
import { translate, translateWithParameters } from '../../helpers/l10n'; | |||
import { formatMeasure, isDiffMetric } from '../../helpers/measures'; | |||
import { AppState } from '../../types/appstate'; | |||
import { MetricKey } from '../../types/metrics'; | |||
import { GlobalSettingKeys } from '../../types/settings'; | |||
import { KNOWN_RATINGS } from './utils'; | |||
const RATING_GRID_SIZE = 4; | |||
const DIFF_METRIC_PREFIX_LENGTH = 4; | |||
const PERCENT_MULTIPLIER = 100; | |||
const GRID_INDEX_OFFSET = 2; // Rating of 2 should get index 0 (threshold between 1 and 2) | |||
export interface RatingTooltipContentProps { | |||
appState: AppState; | |||
metricKey: MetricKey | string; | |||
value: number | string; | |||
} | |||
function getMaintainabilityGrid(ratingGridSetting: string) { | |||
const numbers = ratingGridSetting | |||
.split(',') | |||
.map(s => parseFloat(s)) | |||
.filter(n => !isNaN(n)); | |||
return numbers.length === RATING_GRID_SIZE ? numbers : [0, 0, 0, 0]; | |||
} | |||
export function RatingTooltipContent(props: RatingTooltipContentProps) { | |||
const { | |||
appState: { settings }, | |||
metricKey, | |||
value | |||
} = props; | |||
const finalMetricKey = isDiffMetric(metricKey) | |||
? metricKey.slice(DIFF_METRIC_PREFIX_LENGTH) | |||
: metricKey; | |||
if (!KNOWN_RATINGS.includes(finalMetricKey)) { | |||
return null; | |||
} | |||
const rating = Number(value); | |||
const ratingLetter = formatMeasure(value, 'RATING'); | |||
if (finalMetricKey !== 'sqale_rating' && finalMetricKey !== 'maintainability_rating') { | |||
return <>{translate('metric', finalMetricKey, 'tooltip', ratingLetter)}</>; | |||
} | |||
const maintainabilityGrid = getMaintainabilityGrid(settings[GlobalSettingKeys.RatingGrid] ?? ''); | |||
const maintainabilityRatingThreshold = | |||
maintainabilityGrid[Math.floor(rating) - GRID_INDEX_OFFSET]; | |||
return ( | |||
// Required to correctly satisfy the context typing | |||
// eslint-disable-next-line react/jsx-no-useless-fragment | |||
<> | |||
{rating === 1 | |||
? translateWithParameters( | |||
'metric.sqale_rating.tooltip.A', | |||
formatMeasure(maintainabilityGrid[0] * PERCENT_MULTIPLIER, 'PERCENT') | |||
) | |||
: translateWithParameters( | |||
'metric.sqale_rating.tooltip', | |||
ratingLetter, | |||
formatMeasure(maintainabilityRatingThreshold * PERCENT_MULTIPLIER, 'PERCENT') | |||
)} | |||
</> | |||
); | |||
} | |||
export default withAppStateContext(RatingTooltipContent); |
@@ -21,12 +21,6 @@ import { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import Measure from '../Measure'; | |||
jest.mock('../../../helpers/measures', () => { | |||
const measures = jest.requireActual('../../../helpers/measures'); | |||
measures.getRatingTooltip = jest.fn(() => 'tooltip'); | |||
return measures; | |||
}); | |||
it('renders trivial measure', () => { | |||
expect( | |||
shallow(<Measure metricKey="coverage" metricType="PERCENT" value="73.0" />) | |||
@@ -45,18 +39,12 @@ it('renders LEVEL', () => { | |||
).toMatchSnapshot(); | |||
}); | |||
it('renders known RATING', () => { | |||
it('renders RATING', () => { | |||
expect( | |||
shallow(<Measure metricKey="sqale_rating" metricType="RATING" value="3" />) | |||
).toMatchSnapshot(); | |||
}); | |||
it('renders unknown RATING', () => { | |||
expect( | |||
shallow(<Measure metricKey="foo_rating" metricType="RATING" value="4" />) | |||
).toMatchSnapshot(); | |||
}); | |||
it('renders undefined measure', () => { | |||
expect( | |||
shallow(<Measure metricKey="foo" metricType="PERCENT" value={undefined} />) |
@@ -0,0 +1,62 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2022 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { mockAppState } from '../../../helpers/testMocks'; | |||
import { GlobalSettingKeys } from '../../../types/settings'; | |||
import { MetricKey } from '../../../types/metrics'; | |||
import { RatingTooltipContent, RatingTooltipContentProps } from '../RatingTooltipContent'; | |||
it('should render maintainability correctly', () => { | |||
expect(shallowRender()).toMatchSnapshot('sqale rating'); | |||
expect(shallowRender({ value: 1 })).toMatchSnapshot('sqale rating A'); | |||
expect(shallowRender({ appState: mockAppState({ settings: {} }) })).toMatchSnapshot( | |||
'sqale rating default grid' | |||
); | |||
expect( | |||
shallowRender({ | |||
appState: mockAppState({ settings: { [GlobalSettingKeys.RatingGrid]: '0,0.1' } }) | |||
}) | |||
).toMatchSnapshot('sqale rating wrong grid'); | |||
}); | |||
it('should render other ratings correctly', () => { | |||
expect(shallowRender({ metricKey: MetricKey.security_rating })).toMatchSnapshot( | |||
'security rating' | |||
); | |||
expect(shallowRender({ metricKey: MetricKey.new_security_rating })).toMatchSnapshot( | |||
'new security rating' | |||
); | |||
}); | |||
it('should ignore non-rating metrics', () => { | |||
expect(shallowRender({ metricKey: MetricKey.code_smells }).type()).toBeNull(); | |||
}); | |||
function shallowRender(overrides: Partial<RatingTooltipContentProps> = {}) { | |||
return shallow( | |||
<RatingTooltipContent | |||
appState={mockAppState({ settings: { [GlobalSettingKeys.RatingGrid]: '0.05,0.1,0.2,0.4' } })} | |||
metricKey={MetricKey.sqale_rating} | |||
value={2} | |||
{...overrides} | |||
/> | |||
); | |||
} |
@@ -6,9 +6,14 @@ exports[`renders LEVEL 1`] = ` | |||
/> | |||
`; | |||
exports[`renders known RATING 1`] = ` | |||
exports[`renders RATING 1`] = ` | |||
<Tooltip | |||
overlay="tooltip" | |||
overlay={ | |||
<withAppStateContext(RatingTooltipContent) | |||
metricKey="sqale_rating" | |||
value="3" | |||
/> | |||
} | |||
> | |||
<span> | |||
<Rating | |||
@@ -35,9 +40,3 @@ exports[`renders undefined measure 1`] = ` | |||
– | |||
</span> | |||
`; | |||
exports[`renders unknown RATING 1`] = ` | |||
<Rating | |||
value="4" | |||
/> | |||
`; |
@@ -0,0 +1,37 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render maintainability correctly: sqale rating 1`] = ` | |||
<Fragment> | |||
metric.sqale_rating.tooltip.B.5.0% | |||
</Fragment> | |||
`; | |||
exports[`should render maintainability correctly: sqale rating A 1`] = ` | |||
<Fragment> | |||
metric.sqale_rating.tooltip.A.5.0% | |||
</Fragment> | |||
`; | |||
exports[`should render maintainability correctly: sqale rating default grid 1`] = ` | |||
<Fragment> | |||
metric.sqale_rating.tooltip.B.0.0% | |||
</Fragment> | |||
`; | |||
exports[`should render maintainability correctly: sqale rating wrong grid 1`] = ` | |||
<Fragment> | |||
metric.sqale_rating.tooltip.B.0.0% | |||
</Fragment> | |||
`; | |||
exports[`should render other ratings correctly: new security rating 1`] = ` | |||
<Fragment> | |||
metric.security_rating.tooltip.B | |||
</Fragment> | |||
`; | |||
exports[`should render other ratings correctly: security rating 1`] = ` | |||
<Fragment> | |||
metric.security_rating.tooltip.B | |||
</Fragment> | |||
`; |
@@ -17,10 +17,9 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { getRatingTooltip as nextGetRatingTooltip, isDiffMetric } from '../../helpers/measures'; | |||
import { Dict, Measure, MeasureEnhanced, MeasureIntern, Metric } from '../../types/types'; | |||
const KNOWN_RATINGS = [ | |||
export const KNOWN_RATINGS = [ | |||
'sqale_rating', | |||
'maintainability_rating', // Needed to provide the label for "new_maintainability_rating" | |||
'reliability_rating', | |||
@@ -39,11 +38,3 @@ export function enhanceMeasure(measure: Measure, metrics: Dict<Metric>): Measure | |||
export function getLeakValue(measure: MeasureIntern | undefined): string | undefined { | |||
return measure?.period?.value; | |||
} | |||
export function getRatingTooltip(metricKey: string, value: number): string | undefined { | |||
const finalMetricKey = isDiffMetric(metricKey) ? metricKey.substr(4) : metricKey; | |||
if (KNOWN_RATINGS.includes(finalMetricKey)) { | |||
return nextGetRatingTooltip(finalMetricKey, value); | |||
} | |||
return undefined; | |||
} |
@@ -25,7 +25,7 @@ import { Alert } from '../../../../components/ui/Alert'; | |||
import { ALM_DOCUMENTATION_PATHS } from '../../../../helpers/constants'; | |||
import { translate } from '../../../../helpers/l10n'; | |||
import { AlmKeys } from '../../../../types/alm-settings'; | |||
import { AppState } from '../../../../types/types'; | |||
import { AppState } from '../../../../types/appstate'; | |||
import SentenceWithHighlights from '../../components/SentenceWithHighlights'; | |||
export interface PublishStepsProps { |
@@ -20,7 +20,8 @@ | |||
import { Dictionary } from 'lodash'; | |||
import * as React from 'react'; | |||
import withAppStateContext from '../../../app/components/app-state/withAppStateContext'; | |||
import { AppState, Component } from '../../../types/types'; | |||
import { AppState } from '../../../types/appstate'; | |||
import { Component } from '../../../types/types'; | |||
import { CompilationInfo } from '../components/CompilationInfo'; | |||
import CreateYmlFile from '../components/CreateYmlFile'; | |||
import { BuildTools } from '../types'; |
@@ -22,7 +22,7 @@ import withAppStateContext from '../../../app/components/app-state/withAppStateC | |||
import { translate } from '../../../helpers/l10n'; | |||
import { getBaseUrl } from '../../../helpers/system'; | |||
import { AlmKeys } from '../../../types/alm-settings'; | |||
import { AppState } from '../../../types/types'; | |||
import { AppState } from '../../../types/appstate'; | |||
import SentenceWithHighlights from './SentenceWithHighlights'; | |||
export interface AllSetProps { |
@@ -19,7 +19,8 @@ | |||
*/ | |||
import * as React from 'react'; | |||
import withAppStateContext from '../../../app/components/app-state/withAppStateContext'; | |||
import { AppState, Component } from '../../../types/types'; | |||
import { AppState } from '../../../types/appstate'; | |||
import { Component } from '../../../types/types'; | |||
import { BuildTools } from '../types'; | |||
import CFamily from './commands/CFamily'; | |||
import DotNet from './commands/DotNet'; |
@@ -22,7 +22,7 @@ import { FormattedMessage } from 'react-intl'; | |||
import withAppStateContext from '../../../app/components/app-state/withAppStateContext'; | |||
import { ClipboardIconButton } from '../../../components/controls/clipboard'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { AppState } from '../../../types/types'; | |||
import { AppState } from '../../../types/appstate'; | |||
import FinishButton from '../components/FinishButton'; | |||
import GithubCFamilyExampleRepositories from '../components/GithubCFamilyExampleRepositories'; | |||
import Step from '../components/Step'; |
@@ -28,7 +28,8 @@ import { | |||
AlmSettingsInstance, | |||
ProjectAlmBindingResponse | |||
} from '../../../types/alm-settings'; | |||
import { AppState, Component, CurrentUserSetting } from '../../../types/types'; | |||
import { AppState } from '../../../types/appstate'; | |||
import { Component, CurrentUserSetting } from '../../../types/types'; | |||
import AllSetStep from '../components/AllSetStep'; | |||
import JenkinsfileStep from './JenkinsfileStep'; | |||
import MultiBranchPipelineStep from './MultiBranchPipelineStep'; |
@@ -19,51 +19,53 @@ | |||
*/ | |||
import classNames from 'classnames'; | |||
import * as React from 'react'; | |||
import { connect } from 'react-redux'; | |||
import withAppStateContext from '../../app/components/app-state/withAppStateContext'; | |||
import GenericAvatar from '../../components/ui/GenericAvatar'; | |||
import { getGlobalSettingValue, Store } from '../../store/rootReducer'; | |||
import { AppState } from '../../types/appstate'; | |||
import { GlobalSettingKeys } from '../../types/settings'; | |||
const GRAVATAR_SIZE_MULTIPLIER = 2; | |||
interface Props { | |||
appState: AppState; | |||
className?: string; | |||
enableGravatar: boolean; | |||
gravatarServerUrl: string; | |||
hash?: string; | |||
name?: string; | |||
size: number; | |||
} | |||
function Avatar(props: Props) { | |||
if (!props.enableGravatar || !props.hash) { | |||
if (!props.name) { | |||
export function Avatar(props: Props) { | |||
const { | |||
appState: { settings }, | |||
className, | |||
hash, | |||
name, | |||
size | |||
} = props; | |||
const enableGravatar = settings[GlobalSettingKeys.EnableGravatar] === 'true'; | |||
if (!enableGravatar || !hash) { | |||
if (!name) { | |||
return null; | |||
} | |||
return <GenericAvatar className={props.className} name={props.name} size={props.size} />; | |||
return <GenericAvatar className={className} name={name} size={size} />; | |||
} | |||
const url = props.gravatarServerUrl | |||
.replace('{EMAIL_MD5}', props.hash) | |||
.replace('{SIZE}', String(props.size * 2)); | |||
const gravatarServerUrl = settings[GlobalSettingKeys.GravatarServerUrl] ?? ''; | |||
const url = gravatarServerUrl | |||
.replace('{EMAIL_MD5}', hash) | |||
.replace('{SIZE}', String(size * GRAVATAR_SIZE_MULTIPLIER)); | |||
return ( | |||
<img | |||
alt={props.name} | |||
className={classNames(props.className, 'rounded')} | |||
height={props.size} | |||
alt={name} | |||
className={classNames(className, 'rounded')} | |||
height={size} | |||
src={url} | |||
width={props.size} | |||
width={size} | |||
/> | |||
); | |||
} | |||
const mapStateToProps = (state: Store) => { | |||
const enableGravatar = getGlobalSettingValue(state, 'sonar.lf.enableGravatar'); | |||
const gravatarServerUrl = getGlobalSettingValue(state, 'sonar.lf.gravatarServerUrl'); | |||
return { | |||
enableGravatar: Boolean(enableGravatar && enableGravatar.value === 'true'), | |||
gravatarServerUrl: (gravatarServerUrl && gravatarServerUrl.value) || '' | |||
}; | |||
}; | |||
export default connect(mapStateToProps)(Avatar); | |||
export const unconnectedAvatar = Avatar; | |||
export default withAppStateContext(Avatar); |
@@ -19,15 +19,21 @@ | |||
*/ | |||
import { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { unconnectedAvatar as Avatar } from '../Avatar'; | |||
import { mockAppState } from '../../../helpers/testMocks'; | |||
import { GlobalSettingKeys } from '../../../types/settings'; | |||
import { Avatar } from '../Avatar'; | |||
const gravatarServerUrl = 'http://example.com/{EMAIL_MD5}.jpg?s={SIZE}'; | |||
it('should be able to render with hash only', () => { | |||
const avatar = shallow( | |||
<Avatar | |||
enableGravatar={true} | |||
gravatarServerUrl={gravatarServerUrl} | |||
appState={mockAppState({ | |||
settings: { | |||
[GlobalSettingKeys.EnableGravatar]: 'true', | |||
[GlobalSettingKeys.GravatarServerUrl]: gravatarServerUrl | |||
} | |||
})} | |||
hash="7daf6c79d4802916d83f6266e24850af" | |||
name="Foo" | |||
size={30} | |||
@@ -38,14 +44,14 @@ it('should be able to render with hash only', () => { | |||
it('falls back to dummy avatar', () => { | |||
const avatar = shallow( | |||
<Avatar enableGravatar={false} gravatarServerUrl="" name="Foo Bar" size={30} /> | |||
<Avatar appState={mockAppState({ settings: {} })} name="Foo Bar" size={30} /> | |||
); | |||
expect(avatar).toMatchSnapshot(); | |||
}); | |||
it('do not fail when name is missing', () => { | |||
const avatar = shallow( | |||
<Avatar enableGravatar={false} gravatarServerUrl="" name={undefined} size={30} /> | |||
<Avatar appState={mockAppState({ settings: {} })} name={undefined} size={30} /> | |||
); | |||
expect(avatar.getElement()).toBeNull(); | |||
}); |
@@ -21,9 +21,9 @@ import { filter, flatMap, isEmpty, negate } from 'lodash'; | |||
import * as React from 'react'; | |||
import withAppStateContext from '../../app/components/app-state/withAppStateContext'; | |||
import { translate } from '../../helpers/l10n'; | |||
import { AppState } from '../../types/appstate'; | |||
import { EditionKey } from '../../types/editions'; | |||
import { SystemUpgrade } from '../../types/system'; | |||
import { AppState } from '../../types/types'; | |||
import { ResetButtonLink } from '../controls/buttons'; | |||
import Modal from '../controls/Modal'; | |||
import { Alert, AlertVariant } from '../ui/Alert'; |
@@ -1,26 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2022 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
// The body of the `gtm` function comes from Google Tag Manager docs; let's keep it like it was written. | |||
// @ts-ignore | |||
// prettier-ignore | |||
// eslint-disable-next-line | |||
const gtm = id => (function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start': new Date().getTime(),event:'gtm.js'});const f=d.getElementsByTagName(s)[0], j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src= 'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);}(window,document,'script','dataLayer',id)); | |||
module.exports = { gtm }; |
@@ -63,68 +63,6 @@ export function isDiffMetric(metricKey: MetricKey | string): boolean { | |||
return metricKey.indexOf('new_') === 0; | |||
} | |||
function getRatingGrid(): string { | |||
// workaround cyclic dependencies | |||
const getStore = require('../app/utils/getStore').default; | |||
const { getGlobalSettingValue } = require('../store/rootReducer'); | |||
const store = getStore(); | |||
const settingValue = getGlobalSettingValue(store.getState(), 'sonar.technicalDebt.ratingGrid'); | |||
return settingValue ? settingValue.value : ''; | |||
} | |||
let maintainabilityRatingGrid: number[]; | |||
function getMaintainabilityRatingGrid(): number[] { | |||
if (maintainabilityRatingGrid) { | |||
return maintainabilityRatingGrid; | |||
} | |||
const str = getRatingGrid(); | |||
const numbers = str | |||
.split(',') | |||
.map(s => parseFloat(s)) | |||
.filter(n => !isNaN(n)); | |||
if (numbers.length === 4) { | |||
maintainabilityRatingGrid = numbers; | |||
} else { | |||
maintainabilityRatingGrid = [0, 0, 0, 0]; | |||
} | |||
return maintainabilityRatingGrid; | |||
} | |||
function getMaintainabilityRatingTooltip(rating: number): string { | |||
const maintainabilityGrid = getMaintainabilityRatingGrid(); | |||
const maintainabilityRatingThreshold = maintainabilityGrid[Math.floor(rating) - 2]; | |||
if (rating < 2) { | |||
return translateWithParameters( | |||
'metric.sqale_rating.tooltip.A', | |||
formatMeasure(maintainabilityGrid[0] * 100, 'PERCENT') | |||
); | |||
} | |||
const ratingLetter = formatMeasure(rating, 'RATING'); | |||
return translateWithParameters( | |||
'metric.sqale_rating.tooltip', | |||
ratingLetter, | |||
formatMeasure(maintainabilityRatingThreshold * 100, 'PERCENT') | |||
); | |||
} | |||
export function getRatingTooltip(metricKey: MetricKey | string, value: number | string): string { | |||
const ratingLetter = formatMeasure(value, 'RATING'); | |||
const finalMetricKey = isDiffMetric(metricKey) ? metricKey.substr(4) : metricKey; | |||
return finalMetricKey === 'sqale_rating' || finalMetricKey === 'maintainability_rating' | |||
? getMaintainabilityRatingTooltip(Number(value)) | |||
: translate('metric', finalMetricKey, 'tooltip', ratingLetter); | |||
} | |||
export function getDisplayMetrics(metrics: Metric[]) { | |||
return metrics.filter(metric => !metric.hidden && !['DATA', 'DISTRIB'].includes(metric.type)); | |||
} |