import { getSystemStatus, waitSystemUPStatus } from '../../api/system'; | import { getSystemStatus, waitSystemUPStatus } from '../../api/system'; | ||||
import handleRequiredAuthorization from '../../app/utils/handleRequiredAuthorization'; | import handleRequiredAuthorization from '../../app/utils/handleRequiredAuthorization'; | ||||
import { translate } from '../../helpers/l10n'; | import { translate } from '../../helpers/l10n'; | ||||
import { AppState } from '../../types/appstate'; | |||||
import { PendingPluginResult } from '../../types/plugins'; | 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 AdminContext, { defaultPendingPlugins, defaultSystemStatus } from './AdminContext'; | ||||
import withAppStateContext from './app-state/withAppStateContext'; | import withAppStateContext from './app-state/withAppStateContext'; | ||||
import SettingsNav from './nav/settings/SettingsNav'; | import SettingsNav from './nav/settings/SettingsNav'; |
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | ||||
*/ | */ | ||||
import * as React from 'react'; | import * as React from 'react'; | ||||
import { connect } from 'react-redux'; | |||||
import { lazyLoadComponent } from '../../components/lazyLoadComponent'; | 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'; | import KeyboardShortcutsModal from './KeyboardShortcutsModal'; | ||||
const PageTracker = lazyLoadComponent(() => import('./PageTracker')); | const PageTracker = lazyLoadComponent(() => import('./PageTracker')); | ||||
interface Props { | interface Props { | ||||
enableGravatar: boolean; | |||||
gravatarServerUrl: string; | |||||
appState: AppState; | |||||
} | } | ||||
export class App extends React.PureComponent<Props> { | export class App extends React.PureComponent<Props> { | ||||
}; | }; | ||||
renderPreconnectLink = () => { | 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'); | const parser = document.createElement('a'); | ||||
parser.href = this.props.gravatarServerUrl; | |||||
parser.href = gravatarServerUrl; | |||||
if (parser.hostname !== window.location.hostname) { | if (parser.hostname !== window.location.hostname) { | ||||
return <link href={parser.origin} rel="preconnect" />; | return <link href={parser.origin} rel="preconnect" />; | ||||
} | } | ||||
render() { | render() { | ||||
return ( | return ( | ||||
<> | <> | ||||
<PageTracker>{this.props.enableGravatar && this.renderPreconnectLink()}</PageTracker> | |||||
<PageTracker>{this.renderPreconnectLink()}</PageTracker> | |||||
{this.props.children} | {this.props.children} | ||||
<KeyboardShortcutsModal /> | <KeyboardShortcutsModal /> | ||||
</> | </> | ||||
} | } | ||||
} | } | ||||
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); |
ProjectAlmBindingConfigurationErrors, | ProjectAlmBindingConfigurationErrors, | ||||
ProjectAlmBindingResponse | ProjectAlmBindingResponse | ||||
} from '../../types/alm-settings'; | } from '../../types/alm-settings'; | ||||
import { AppState } from '../../types/appstate'; | |||||
import { BranchLike } from '../../types/branch-like'; | import { BranchLike } from '../../types/branch-like'; | ||||
import { ComponentQualifier, isPortfolioLike } from '../../types/component'; | import { ComponentQualifier, isPortfolioLike } from '../../types/component'; | ||||
import { Task, TaskStatuses, TaskTypes, TaskWarning } from '../../types/tasks'; | 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 handleRequiredAuthorization from '../utils/handleRequiredAuthorization'; | ||||
import withAppStateContext from './app-state/withAppStateContext'; | import withAppStateContext from './app-state/withAppStateContext'; | ||||
import ComponentContainerNotFound from './ComponentContainerNotFound'; | import ComponentContainerNotFound from './ComponentContainerNotFound'; |
import { Alert } from '../../components/ui/Alert'; | import { Alert } from '../../components/ui/Alert'; | ||||
import { getEdition } from '../../helpers/editions'; | import { getEdition } from '../../helpers/editions'; | ||||
import { translate, translateWithParameters } from '../../helpers/l10n'; | import { translate, translateWithParameters } from '../../helpers/l10n'; | ||||
import { AppState } from '../../types/appstate'; | |||||
import { EditionKey } from '../../types/editions'; | import { EditionKey } from '../../types/editions'; | ||||
import { AppState } from '../../types/types'; | |||||
import withAppStateContext from './app-state/withAppStateContext'; | import withAppStateContext from './app-state/withAppStateContext'; | ||||
import GlobalFooterBranding from './GlobalFooterBranding'; | import GlobalFooterBranding from './GlobalFooterBranding'; | ||||
*/ | */ | ||||
import * as React from 'react'; | import * as React from 'react'; | ||||
import { Helmet } from 'react-helmet-async'; | import { Helmet } from 'react-helmet-async'; | ||||
import { connect } from 'react-redux'; | |||||
import { Location, withRouter } from '../../components/hoc/withRouter'; | import { Location, withRouter } from '../../components/hoc/withRouter'; | ||||
import { gtm } from '../../helpers/analytics'; | |||||
import { installScript } from '../../helpers/extensions'; | import { installScript } from '../../helpers/extensions'; | ||||
import { getWebAnalyticsPageHandlerFromCache } from '../../helpers/extensionsHandler'; | import { getWebAnalyticsPageHandlerFromCache } from '../../helpers/extensionsHandler'; | ||||
import { getInstance } from '../../helpers/system'; | 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'; | import withAppStateContext from './app-state/withAppStateContext'; | ||||
interface Props { | interface Props { | ||||
location: Location; | location: Location; | ||||
trackingIdGTM?: string; | |||||
appState: AppState; | appState: AppState; | ||||
} | } | ||||
state: State = {}; | state: State = {}; | ||||
componentDidMount() { | componentDidMount() { | ||||
const { trackingIdGTM, appState } = this.props; | |||||
const { appState } = this.props; | |||||
if (appState.webAnalyticsJsPath && !getWebAnalyticsPageHandlerFromCache()) { | if (appState.webAnalyticsJsPath && !getWebAnalyticsPageHandlerFromCache()) { | ||||
installScript(appState.webAnalyticsJsPath, 'head'); | installScript(appState.webAnalyticsJsPath, 'head'); | ||||
} | } | ||||
if (trackingIdGTM) { | |||||
gtm(trackingIdGTM); | |||||
} | |||||
} | } | ||||
trackPage = () => { | trackPage = () => { | ||||
const { location, trackingIdGTM } = this.props; | |||||
const { location } = this.props; | |||||
const { lastLocation } = this.state; | const { lastLocation } = this.state; | ||||
const { dataLayer } = window as any; | |||||
const locationChanged = location.pathname !== lastLocation; | const locationChanged = location.pathname !== lastLocation; | ||||
const webAnalyticsPageChange = getWebAnalyticsPageHandlerFromCache(); | const webAnalyticsPageChange = getWebAnalyticsPageHandlerFromCache(); | ||||
if (webAnalyticsPageChange && locationChanged) { | if (webAnalyticsPageChange && locationChanged) { | ||||
this.setState({ lastLocation: location.pathname }); | this.setState({ lastLocation: location.pathname }); | ||||
setTimeout(() => webAnalyticsPageChange(location.pathname), 500); | 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() { | render() { | ||||
const { trackingIdGTM, appState } = this.props; | |||||
const { appState } = this.props; | |||||
return ( | return ( | ||||
<Helmet | <Helmet | ||||
defaultTitle={getInstance()} | defaultTitle={getInstance()} | ||||
defer={false} | defer={false} | ||||
onChangeClientState={ | |||||
trackingIdGTM || appState.webAnalyticsJsPath ? this.trackPage : undefined | |||||
}> | |||||
onChangeClientState={appState.webAnalyticsJsPath ? this.trackPage : undefined}> | |||||
{this.props.children} | {this.props.children} | ||||
</Helmet> | </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)); |
import { get, save } from '../../helpers/storage'; | import { get, save } from '../../helpers/storage'; | ||||
import { isLoggedIn } from '../../helpers/users'; | import { isLoggedIn } from '../../helpers/users'; | ||||
import { getCurrentUser, Store } from '../../store/rootReducer'; | import { getCurrentUser, Store } from '../../store/rootReducer'; | ||||
import { AppState } from '../../types/appstate'; | |||||
import { EditionKey } from '../../types/editions'; | import { EditionKey } from '../../types/editions'; | ||||
import { AppState, CurrentUser } from '../../types/types'; | |||||
import { CurrentUser } from '../../types/types'; | |||||
import withAppStateContext from './app-state/withAppStateContext'; | import withAppStateContext from './app-state/withAppStateContext'; | ||||
const LicensePromptModal = lazyLoadComponent( | const LicensePromptModal = lazyLoadComponent( |
*/ | */ | ||||
import { shallow } from 'enzyme'; | import { shallow } from 'enzyme'; | ||||
import * as React from 'react'; | import * as React from 'react'; | ||||
import { connect } from 'react-redux'; | |||||
import { mockAppState } from '../../../helpers/testMocks'; | |||||
import { App } from '../App'; | 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', () => { | it('should render correctly', () => { | ||||
expect(shallowRender()).toMatchSnapshot('default'); | expect(shallowRender()).toMatchSnapshot('default'); | ||||
expect( | 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'); | ).toMatchSnapshot('with gravatar'); | ||||
}); | }); | ||||
expect(document.body.style.getPropertyValue('--sbw')).toBe('0px'); | 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']> = {}) { | 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} | |||||
/> | |||||
); | |||||
} | } |
*/ | */ | ||||
import { shallow } from 'enzyme'; | import { shallow } from 'enzyme'; | ||||
import * as React from 'react'; | import * as React from 'react'; | ||||
import { gtm } from '../../../helpers/analytics'; | |||||
import { installScript } from '../../../helpers/extensions'; | import { installScript } from '../../../helpers/extensions'; | ||||
import { getWebAnalyticsPageHandlerFromCache } from '../../../helpers/extensionsHandler'; | import { getWebAnalyticsPageHandlerFromCache } from '../../../helpers/extensionsHandler'; | ||||
import { mockAppState, mockLocation } from '../../../helpers/testMocks'; | import { mockAppState, mockLocation } from '../../../helpers/testMocks'; | ||||
getWebAnalyticsPageHandlerFromCache: jest.fn().mockReturnValue(undefined) | getWebAnalyticsPageHandlerFromCache: jest.fn().mockReturnValue(undefined) | ||||
})); | })); | ||||
jest.mock('../../../helpers/analytics', () => ({ gtm: jest.fn() })); | |||||
beforeAll(() => { | beforeAll(() => { | ||||
jest.useFakeTimers(); | jest.useFakeTimers(); | ||||
}); | }); | ||||
const wrapper = shallowRender(); | const wrapper = shallowRender(); | ||||
expect(wrapper).toMatchSnapshot(); | expect(wrapper).toMatchSnapshot(); | ||||
expect(installScript).not.toHaveBeenCalled(); | expect(installScript).not.toHaveBeenCalled(); | ||||
expect(gtm).not.toHaveBeenCalled(); | |||||
}); | }); | ||||
it('should work for WebAnalytics plugin', () => { | it('should work for WebAnalytics plugin', () => { | ||||
expect(pageChange).toHaveBeenCalledWith('/path'); | 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']> = {}) { | function shallowRender(props: Partial<PageTracker['props']> = {}) { | ||||
return shallow<PageTracker>( | return shallow<PageTracker>( | ||||
<PageTracker appState={mockAppState()} location={mockLocation()} {...props} /> | <PageTracker appState={mockAppState()} location={mockLocation()} {...props} /> |
*/ | */ | ||||
import * as React from 'react'; | import * as React from 'react'; | ||||
import { AppState } from '../../../types/types'; | |||||
import { AppState } from '../../../types/appstate'; | |||||
const defaultAppState = { | const defaultAppState = { | ||||
authenticationError: false, | authenticationError: false, |
*/ | */ | ||||
import * as React from 'react'; | 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'; | import { AppStateContext } from './AppStateContext'; | ||||
export interface AppStateContextProviderProps { | export interface AppStateContextProviderProps { | ||||
setValues: typeof setValues; | |||||
appState: AppState; | appState: AppState; | ||||
} | } | ||||
export function AppStateContextProvider({ | |||||
setValues, | |||||
export default function AppStateContextProvider({ | |||||
appState, | appState, | ||||
children | children | ||||
}: React.PropsWithChildren<AppStateContextProviderProps>) { | }: 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>; | return <AppStateContext.Provider value={appState}>{children}</AppStateContext.Provider>; | ||||
} | } | ||||
const mapDispatchToProps = { setValues }; | |||||
export default connect(null, mapDispatchToProps)(AppStateContextProvider); |
import * as React from 'react'; | import * as React from 'react'; | ||||
import { mockAppState } from '../../../../helpers/testMocks'; | import { mockAppState } from '../../../../helpers/testMocks'; | ||||
import { waitAndUpdate } from '../../../../helpers/testUtils'; | import { waitAndUpdate } from '../../../../helpers/testUtils'; | ||||
import { AppStateContextProvider, AppStateContextProviderProps } from '../AppStateContextProvider'; | |||||
import AppStateContextProvider, { AppStateContextProviderProps } from '../AppStateContextProvider'; | |||||
it('should set value correctly', async () => { | it('should set value correctly', async () => { | ||||
const setValues = jest.fn(); | |||||
const appState = mockAppState({ settings: { 'sonar.lf.logoUrl': 'whatevs/' } }); | |||||
const wrapper = render({ | const wrapper = render({ | ||||
appState: mockAppState({ settings: { foo: 'bar' } }), | |||||
setValues | |||||
appState | |||||
}); | }); | ||||
await waitAndUpdate(wrapper); | await waitAndUpdate(wrapper); | ||||
expect(setValues).toHaveBeenCalledWith(['foo'], [{ key: 'foo', value: 'bar' }]); | |||||
expect(wrapper).toMatchSnapshot(); | |||||
}); | }); | ||||
function render(override?: Partial<AppStateContextProviderProps>) { | function render(override?: Partial<AppStateContextProviderProps>) { | ||||
return mount( | |||||
<AppStateContextProvider appState={mockAppState()} setValues={jest.fn()} {...override} /> | |||||
); | |||||
return mount(<AppStateContextProvider appState={mockAppState()} {...override} />); | |||||
} | } |
// 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", | |||||
} | |||||
} | |||||
/> | |||||
`; |
import { shallow } from 'enzyme'; | import { shallow } from 'enzyme'; | ||||
import * as React from 'react'; | import * as React from 'react'; | ||||
import { mockAppState } from '../../../../helpers/testMocks'; | import { mockAppState } from '../../../../helpers/testMocks'; | ||||
import { AppState } from '../../../../types/types'; | |||||
import { AppState } from '../../../../types/appstate'; | |||||
import withAppStateContext from '../withAppStateContext'; | import withAppStateContext from '../withAppStateContext'; | ||||
const appState = mockAppState(); | const appState = mockAppState(); |
import * as React from 'react'; | import * as React from 'react'; | ||||
import { getWrappedDisplayName } from '../../../components/hoc/utils'; | import { getWrappedDisplayName } from '../../../components/hoc/utils'; | ||||
import { AppState } from '../../../types/types'; | |||||
import { AppState } from '../../../types/appstate'; | |||||
import { AppStateContext } from './AppStateContext'; | import { AppStateContext } from './AppStateContext'; | ||||
export interface WithAppStateContextProps { | export interface WithAppStateContextProps { |
import { getBaseUrl } from '../../../helpers/system'; | import { getBaseUrl } from '../../../helpers/system'; | ||||
import { addGlobalErrorMessage } from '../../../store/globalMessages'; | import { addGlobalErrorMessage } from '../../../store/globalMessages'; | ||||
import { getCurrentUser, Store } from '../../../store/rootReducer'; | import { getCurrentUser, Store } from '../../../store/rootReducer'; | ||||
import { AppState } from '../../../types/appstate'; | |||||
import { ExtensionStartMethod } from '../../../types/extension'; | 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 * as theme from '../../theme'; | ||||
import getStore from '../../utils/getStore'; | import getStore from '../../utils/getStore'; | ||||
import withAppStateContext from '../app-state/withAppStateContext'; | import withAppStateContext from '../app-state/withAppStateContext'; |
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | ||||
*/ | */ | ||||
import * as React from 'react'; | import * as React from 'react'; | ||||
import { AppState } from '../../../types/types'; | |||||
import { AppState } from '../../../types/appstate'; | |||||
import withAppStateContext from '../app-state/withAppStateContext'; | import withAppStateContext from '../app-state/withAppStateContext'; | ||||
import NotFound from '../NotFound'; | import NotFound from '../NotFound'; | ||||
import Extension from './Extension'; | import Extension from './Extension'; |
import DateFromNow from '../../../components/intl/DateFromNow'; | import DateFromNow from '../../../components/intl/DateFromNow'; | ||||
import DateTimeFormatter from '../../../components/intl/DateTimeFormatter'; | import DateTimeFormatter from '../../../components/intl/DateTimeFormatter'; | ||||
import Measure from '../../../components/measure/Measure'; | import Measure from '../../../components/measure/Measure'; | ||||
import RatingTooltipContent from '../../../components/measure/RatingTooltipContent'; | |||||
import { Alert } from '../../../components/ui/Alert'; | import { Alert } from '../../../components/ui/Alert'; | ||||
import CoverageRating from '../../../components/ui/CoverageRating'; | import CoverageRating from '../../../components/ui/CoverageRating'; | ||||
import DeferredSpinner from '../../../components/ui/DeferredSpinner'; | import DeferredSpinner from '../../../components/ui/DeferredSpinner'; | ||||
Radio, | Radio, | ||||
RadioToggle, | RadioToggle, | ||||
Rating, | Rating, | ||||
RatingTooltipContent, | |||||
ReloadButton, | ReloadButton, | ||||
ResetButtonLink, | ResetButtonLink, | ||||
SearchBox, | SearchBox, |
*/ | */ | ||||
/* eslint-disable react/no-unused-state */ | /* eslint-disable react/no-unused-state */ | ||||
import * as React from 'react'; | import * as React from 'react'; | ||||
import { AppState } from '../../../types/appstate'; | |||||
import { IndexationContextInterface, IndexationStatus } from '../../../types/indexation'; | import { IndexationContextInterface, IndexationStatus } from '../../../types/indexation'; | ||||
import { AppState } from '../../../types/types'; | |||||
import withAppStateContext from '../app-state/withAppStateContext'; | import withAppStateContext from '../app-state/withAppStateContext'; | ||||
import { IndexationContext } from './IndexationContext'; | import { IndexationContext } from './IndexationContext'; | ||||
import IndexationNotificationHelper from './IndexationNotificationHelper'; | import IndexationNotificationHelper from './IndexationNotificationHelper'; |
import { isValidLicense } from '../../../../api/marketplace'; | import { isValidLicense } from '../../../../api/marketplace'; | ||||
import { Alert } from '../../../../components/ui/Alert'; | import { Alert } from '../../../../components/ui/Alert'; | ||||
import { translate, translateWithParameters } from '../../../../helpers/l10n'; | import { translate, translateWithParameters } from '../../../../helpers/l10n'; | ||||
import { AppState } from '../../../../types/appstate'; | |||||
import { ComponentQualifier } from '../../../../types/component'; | import { ComponentQualifier } from '../../../../types/component'; | ||||
import { Task } from '../../../../types/tasks'; | import { Task } from '../../../../types/tasks'; | ||||
import { AppState } from '../../../../types/types'; | |||||
import withAppStateContext from '../../app-state/withAppStateContext'; | import withAppStateContext from '../../app-state/withAppStateContext'; | ||||
interface Props { | interface Props { |
import { getBranchLikeQuery, isPullRequest } from '../../../../helpers/branch-like'; | import { getBranchLikeQuery, isPullRequest } from '../../../../helpers/branch-like'; | ||||
import { hasMessage, translate, translateWithParameters } from '../../../../helpers/l10n'; | import { hasMessage, translate, translateWithParameters } from '../../../../helpers/l10n'; | ||||
import { getPortfolioUrl, getProjectQueryUrl } from '../../../../helpers/urls'; | import { getPortfolioUrl, getProjectQueryUrl } from '../../../../helpers/urls'; | ||||
import { AppState } from '../../../../types/appstate'; | |||||
import { BranchLike, BranchParameters } from '../../../../types/branch-like'; | import { BranchLike, BranchParameters } from '../../../../types/branch-like'; | ||||
import { ComponentQualifier, isPortfolioLike } from '../../../../types/component'; | 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 withAppStateContext from '../../app-state/withAppStateContext'; | ||||
import './Menu.css'; | import './Menu.css'; | ||||
import * as React from 'react'; | import * as React from 'react'; | ||||
import Toggler from '../../../../../components/controls/Toggler'; | import Toggler from '../../../../../components/controls/Toggler'; | ||||
import { ProjectAlmBindingResponse } from '../../../../../types/alm-settings'; | import { ProjectAlmBindingResponse } from '../../../../../types/alm-settings'; | ||||
import { AppState } from '../../../../../types/appstate'; | |||||
import { BranchLike } from '../../../../../types/branch-like'; | import { BranchLike } from '../../../../../types/branch-like'; | ||||
import { AppState, Component } from '../../../../../types/types'; | |||||
import { Component } from '../../../../../types/types'; | |||||
import withAppStateContext from '../../../app-state/withAppStateContext'; | import withAppStateContext from '../../../app-state/withAppStateContext'; | ||||
import './BranchLikeNavigation.css'; | import './BranchLikeNavigation.css'; | ||||
import CurrentBranchLike from './CurrentBranchLike'; | import CurrentBranchLike from './CurrentBranchLike'; |
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | ||||
*/ | */ | ||||
import * as React from 'react'; | import * as React from 'react'; | ||||
import { connect } from 'react-redux'; | |||||
import { Link } from 'react-router'; | import { Link } from 'react-router'; | ||||
import { translate } from '../../../../helpers/l10n'; | import { translate } from '../../../../helpers/l10n'; | ||||
import { getBaseUrl } from '../../../../helpers/system'; | 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 title = translate('layout.sonar.slogan'); | ||||
const url = customLogoUrl || `${getBaseUrl()}/images/logo.svg?v=6.6`; | const url = customLogoUrl || `${getBaseUrl()}/images/logo.svg?v=6.6`; | ||||
const width = customLogoUrl ? customLogoWidth || 100 : 83; | const width = customLogoUrl ? customLogoWidth || 100 : 83; | ||||
); | ); | ||||
} | } | ||||
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); |
import DropdownIcon from '../../../../components/icons/DropdownIcon'; | import DropdownIcon from '../../../../components/icons/DropdownIcon'; | ||||
import { translate } from '../../../../helpers/l10n'; | import { translate } from '../../../../helpers/l10n'; | ||||
import { getQualityGatesUrl } from '../../../../helpers/urls'; | import { getQualityGatesUrl } from '../../../../helpers/urls'; | ||||
import { AppState } from '../../../../types/appstate'; | |||||
import { ComponentQualifier } from '../../../../types/component'; | import { ComponentQualifier } from '../../../../types/component'; | ||||
import { AppState, CurrentUser, Extension } from '../../../../types/types'; | |||||
import { CurrentUser, Extension } from '../../../../types/types'; | |||||
import withAppStateContext from '../../app-state/withAppStateContext'; | import withAppStateContext from '../../app-state/withAppStateContext'; | ||||
interface Props { | interface Props { |
/* | |||||
* 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} />); | |||||
} |
height={48} | height={48} | ||||
id="global-navigation" | id="global-navigation" | ||||
> | > | ||||
<Connect(GlobalNavBranding) /> | |||||
<withAppStateContext(GlobalNavBranding) /> | |||||
<withAppStateContext(GlobalNavMenu) | <withAppStateContext(GlobalNavMenu) | ||||
currentUser={ | currentUser={ | ||||
Object { | Object { | ||||
height={48} | height={48} | ||||
id="global-navigation" | id="global-navigation" | ||||
> | > | ||||
<Connect(GlobalNavBranding) /> | |||||
<withAppStateContext(GlobalNavBranding) /> | |||||
<withAppStateContext(GlobalNavMenu) | <withAppStateContext(GlobalNavMenu) | ||||
currentUser={ | currentUser={ | ||||
Object { | Object { |
// 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> | |||||
`; |
href="#" | href="#" | ||||
title="Skywalker" | title="Skywalker" | ||||
> | > | ||||
<Connect(Avatar) | |||||
<withAppStateContext(Avatar) | |||||
name="Skywalker" | name="Skywalker" | ||||
size={32} | size={32} | ||||
/> | /> |
import { sortUpgrades, UpdateUseCase } from '../../../components/upgrade/utils'; | import { sortUpgrades, UpdateUseCase } from '../../../components/upgrade/utils'; | ||||
import { translate } from '../../../helpers/l10n'; | import { translate } from '../../../helpers/l10n'; | ||||
import { hasGlobalPermission, isLoggedIn } from '../../../helpers/users'; | import { hasGlobalPermission, isLoggedIn } from '../../../helpers/users'; | ||||
import { AppState } from '../../../types/appstate'; | |||||
import { Permissions } from '../../../types/permissions'; | import { Permissions } from '../../../types/permissions'; | ||||
import { SystemUpgrade } from '../../../types/system'; | 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 withAppStateContext from '../app-state/withAppStateContext'; | ||||
import './UpdateNotification.css'; | import './UpdateNotification.css'; | ||||
import { loadL10nBundle } from '../helpers/l10n'; | import { loadL10nBundle } from '../helpers/l10n'; | ||||
import { parseJSON, request } from '../helpers/request'; | import { parseJSON, request } from '../helpers/request'; | ||||
import { getBaseUrl, getSystemStatus } from '../helpers/system'; | import { getBaseUrl, getSystemStatus } from '../helpers/system'; | ||||
import { AppState } from '../types/types'; | |||||
import { AppState } from '../types/appstate'; | |||||
import './styles/sonar.ts'; | import './styles/sonar.ts'; | ||||
installWebAnalyticsHandler(); | installWebAnalyticsHandler(); |
import withIndexationGuard from '../../components/hoc/withIndexationGuard'; | import withIndexationGuard from '../../components/hoc/withIndexationGuard'; | ||||
import { lazyLoadComponent } from '../../components/lazyLoadComponent'; | import { lazyLoadComponent } from '../../components/lazyLoadComponent'; | ||||
import getHistory from '../../helpers/getHistory'; | 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 App from '../components/App'; | ||||
import AppStateContextProvider from '../components/app-state/AppStateContextProvider'; | import AppStateContextProvider from '../components/app-state/AppStateContextProvider'; | ||||
import GlobalContainer from '../components/GlobalContainer'; | import GlobalContainer from '../components/GlobalContainer'; |
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | ||||
*/ | */ | ||||
import * as React from 'react'; | 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 { AdminPageExtension } from '../../../types/extension'; | ||||
import { SettingsKey } from '../../../types/settings'; | |||||
import { Extension } from '../../../types/types'; | import { Extension } from '../../../types/types'; | ||||
import { fetchValues } from '../../settings/store/actions'; | |||||
import '../style.css'; | import '../style.css'; | ||||
import { HousekeepingPolicy, RangeOption } from '../utils'; | import { HousekeepingPolicy, RangeOption } from '../utils'; | ||||
import AuditAppRenderer from './AuditAppRenderer'; | import AuditAppRenderer from './AuditAppRenderer'; | ||||
interface Props { | interface Props { | ||||
auditHousekeepingPolicy: HousekeepingPolicy; | |||||
fetchValues: typeof fetchValues; | |||||
adminPages: Extension[]; | adminPages: Extension[]; | ||||
} | } | ||||
interface State { | interface State { | ||||
dateRange?: { from?: Date; to?: Date }; | dateRange?: { from?: Date; to?: Date }; | ||||
hasGovernanceExtension?: boolean; | |||||
downloadStarted: boolean; | downloadStarted: boolean; | ||||
housekeepingPolicy: HousekeepingPolicy; | |||||
selection: RangeOption; | selection: RangeOption; | ||||
} | } | ||||
export class AuditApp extends React.PureComponent<Props, State> { | |||||
export default class AuditApp extends React.PureComponent<Props, State> { | |||||
constructor(props: Props) { | constructor(props: Props) { | ||||
super(props); | super(props); | ||||
const hasGovernanceExtension = Boolean( | |||||
props.adminPages?.find(e => e.key === AdminPageExtension.GovernanceConsole) | |||||
); | |||||
this.state = { | this.state = { | ||||
downloadStarted: false, | downloadStarted: false, | ||||
selection: RangeOption.Today, | |||||
hasGovernanceExtension | |||||
housekeepingPolicy: HousekeepingPolicy.Monthly, | |||||
selection: RangeOption.Today | |||||
}; | }; | ||||
} | } | ||||
componentDidMount() { | componentDidMount() { | ||||
const { hasGovernanceExtension } = this.state; | |||||
if (hasGovernanceExtension) { | |||||
this.props.fetchValues(['sonar.dbcleaner.auditHousekeeping']); | |||||
if (this.hasGovernanceExtension()) { | |||||
this.fetchHouseKeepingPolicy(); | |||||
} | } | ||||
} | } | ||||
componentDidUpdate(prevProps: Props) { | 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 }) => | handleDateSelection = (dateRange: { from?: Date; to?: Date }) => | ||||
this.setState({ dateRange, downloadStarted: false, selection: RangeOption.Custom }); | this.setState({ dateRange, downloadStarted: false, selection: RangeOption.Custom }); | ||||
}; | }; | ||||
render() { | render() { | ||||
const { hasGovernanceExtension, ...auditAppRendererProps } = this.state; | |||||
const { auditHousekeepingPolicy } = this.props; | |||||
if (!hasGovernanceExtension) { | |||||
if (!this.hasGovernanceExtension()) { | |||||
return null; | return null; | ||||
} | } | ||||
handleDateSelection={this.handleDateSelection} | handleDateSelection={this.handleDateSelection} | ||||
handleOptionSelection={this.handleOptionSelection} | handleOptionSelection={this.handleOptionSelection} | ||||
handleStartDownload={this.handleStartDownload} | 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); |
import { subDays } from 'date-fns'; | import { subDays } from 'date-fns'; | ||||
import { shallow } from 'enzyme'; | import { shallow } from 'enzyme'; | ||||
import * as React from 'react'; | import * as React from 'react'; | ||||
import { getValues } from '../../../../api/settings'; | |||||
import { waitAndUpdate } from '../../../../helpers/testUtils'; | import { waitAndUpdate } from '../../../../helpers/testUtils'; | ||||
import { AdminPageExtension } from '../../../../types/extension'; | import { AdminPageExtension } from '../../../../types/extension'; | ||||
import { HousekeepingPolicy, RangeOption } from '../../utils'; | import { HousekeepingPolicy, RangeOption } from '../../utils'; | ||||
import { AuditApp } from '../AuditApp'; | |||||
import AuditApp from '../AuditApp'; | |||||
import AuditAppRenderer from '../AuditAppRenderer'; | import AuditAppRenderer from '../AuditAppRenderer'; | ||||
jest.mock('../../../../api/settings', () => ({ | |||||
getValues: jest.fn().mockResolvedValue([]) | |||||
})); | |||||
beforeEach(() => { | |||||
jest.clearAllMocks(); | |||||
}); | |||||
it('should render correctly', () => { | it('should render correctly', () => { | ||||
expect(shallowRender()).toMatchSnapshot(); | expect(shallowRender()).toMatchSnapshot(); | ||||
}); | }); | ||||
it('should do nothing if governance is not available', async () => { | 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); | await waitAndUpdate(wrapper); | ||||
expect(wrapper.type()).toBeNull(); | 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); | await waitAndUpdate(wrapper); | ||||
expect(fetchValues).toBeCalled(); | |||||
expect(wrapper.find(AuditAppRenderer).props().housekeepingPolicy).toBe(HousekeepingPolicy.Weekly); | |||||
}); | }); | ||||
it('should handle date selection', () => { | it('should handle date selection', () => { | ||||
expect(wrapper.state().dateRange).toBeUndefined(); | 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']> = {}) { | function shallowRender(props: Partial<AuditApp['props']> = {}) { | ||||
return shallow<AuditApp>( | return shallow<AuditApp>( | ||||
<AuditApp | <AuditApp | ||||
auditHousekeepingPolicy={HousekeepingPolicy.Monthly} | |||||
fetchValues={jest.fn()} | |||||
adminPages={[{ key: AdminPageExtension.GovernanceConsole, name: 'name' }]} | adminPages={[{ key: AdminPageExtension.GovernanceConsole, name: 'name' }]} | ||||
{...props} | {...props} | ||||
/> | /> |
import ConfirmButton from '../../../components/controls/ConfirmButton'; | import ConfirmButton from '../../../components/controls/ConfirmButton'; | ||||
import Tooltip from '../../../components/controls/Tooltip'; | import Tooltip from '../../../components/controls/Tooltip'; | ||||
import { translate } from '../../../helpers/l10n'; | import { translate } from '../../../helpers/l10n'; | ||||
import { AppState } from '../../../types/types'; | |||||
import { AppState } from '../../../types/appstate'; | |||||
export interface Props { | export interface Props { | ||||
appState: AppState; | appState: AppState; |
import { changePassword } from '../../api/users'; | import { changePassword } from '../../api/users'; | ||||
import withAppStateContext from '../../app/components/app-state/withAppStateContext'; | import withAppStateContext from '../../app/components/app-state/withAppStateContext'; | ||||
import { Location, withRouter } from '../../components/hoc/withRouter'; | import { Location, withRouter } from '../../components/hoc/withRouter'; | ||||
import { AppState } from '../../types/types'; | |||||
import { AppState } from '../../types/appstate'; | |||||
import ChangeAdminPasswordAppRenderer from './ChangeAdminPasswordAppRenderer'; | import ChangeAdminPasswordAppRenderer from './ChangeAdminPasswordAppRenderer'; | ||||
import { DEFAULT_ADMIN_LOGIN, DEFAULT_ADMIN_PASSWORD } from './constants'; | import { DEFAULT_ADMIN_LOGIN, DEFAULT_ADMIN_PASSWORD } from './constants'; | ||||
import { translate } from '../../../helpers/l10n'; | import { translate } from '../../../helpers/l10n'; | ||||
import { formatMeasure } from '../../../helpers/measures'; | import { formatMeasure } from '../../../helpers/measures'; | ||||
import { getIssuesUrl } from '../../../helpers/urls'; | import { getIssuesUrl } from '../../../helpers/urls'; | ||||
import { AppState, RuleDetails } from '../../../types/types'; | |||||
import { AppState } from '../../../types/appstate'; | |||||
import { RuleDetails } from '../../../types/types'; | |||||
interface Props { | interface Props { | ||||
appState: AppState; | appState: AppState; |
import { translate, translateWithParameters } from '../../../helpers/l10n'; | import { translate, translateWithParameters } from '../../../helpers/l10n'; | ||||
import { getBaseUrl } from '../../../helpers/system'; | import { getBaseUrl } from '../../../helpers/system'; | ||||
import { AlmKeys } from '../../../types/alm-settings'; | import { AlmKeys } from '../../../types/alm-settings'; | ||||
import { AppState } from '../../../types/types'; | |||||
import { AppState } from '../../../types/appstate'; | |||||
import { CreateProjectModes } from './types'; | import { CreateProjectModes } from './types'; | ||||
export interface CreateProjectModeSelectionProps { | export interface CreateProjectModeSelectionProps { |
import { translate } from '../../../helpers/l10n'; | import { translate } from '../../../helpers/l10n'; | ||||
import { getProjectUrl } from '../../../helpers/urls'; | import { getProjectUrl } from '../../../helpers/urls'; | ||||
import { AlmKeys, AlmSettingsInstance } from '../../../types/alm-settings'; | 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 AlmBindingDefinitionForm from '../../settings/components/almIntegration/AlmBindingDefinitionForm'; | ||||
import AzureProjectCreate from './AzureProjectCreate'; | import AzureProjectCreate from './AzureProjectCreate'; | ||||
import BitbucketCloudProjectCreate from './BitbucketCloudProjectCreate'; | import BitbucketCloudProjectCreate from './BitbucketCloudProjectCreate'; |
displayReset={true} | displayReset={true} | ||||
onReset={[Function]} | onReset={[Function]} | ||||
/> | /> | ||||
<Connect(Sidebar) | |||||
<withAppStateContext(Sidebar) | |||||
component={ | component={ | ||||
Object { | Object { | ||||
"breadcrumbs": Array [], | "breadcrumbs": Array [], |
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | ||||
*/ | */ | ||||
import * as React from 'react'; | 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 { isBranch, isPullRequest } from '../../../helpers/branch-like'; | ||||
import { getGlobalSettingValue, Store } from '../../../store/rootReducer'; | |||||
import { AppState } from '../../../types/appstate'; | |||||
import { BranchLike } from '../../../types/branch-like'; | import { BranchLike } from '../../../types/branch-like'; | ||||
import { ComponentQualifier, isApplication, isPortfolioLike } from '../../../types/component'; | import { ComponentQualifier, isApplication, isPortfolioLike } from '../../../types/component'; | ||||
import { | import { | ||||
ReferencedLanguage, | ReferencedLanguage, | ||||
ReferencedRule | ReferencedRule | ||||
} from '../../../types/issues'; | } from '../../../types/issues'; | ||||
import { GlobalSettingKeys } from '../../../types/settings'; | |||||
import { Component, Dict, UserBase } from '../../../types/types'; | import { Component, Dict, UserBase } from '../../../types/types'; | ||||
import { Query } from '../utils'; | import { Query } from '../utils'; | ||||
import AssigneeFacet from './AssigneeFacet'; | import AssigneeFacet from './AssigneeFacet'; | ||||
import TypeFacet from './TypeFacet'; | import TypeFacet from './TypeFacet'; | ||||
export interface Props { | export interface Props { | ||||
appState: AppState; | |||||
branchLike?: BranchLike; | branchLike?: BranchLike; | ||||
component: Component | undefined; | component: Component | undefined; | ||||
createdAfterIncludesTime: boolean; | createdAfterIncludesTime: boolean; | ||||
referencedLanguages: Dict<ReferencedLanguage>; | referencedLanguages: Dict<ReferencedLanguage>; | ||||
referencedRules: Dict<ReferencedRule>; | referencedRules: Dict<ReferencedRule>; | ||||
referencedUsers: Dict<UserBase>; | referencedUsers: Dict<UserBase>; | ||||
disableDeveloperAggregatedInfo: boolean; | |||||
} | } | ||||
export class Sidebar extends React.PureComponent<Props> { | export class Sidebar extends React.PureComponent<Props> { | ||||
render() { | render() { | ||||
const { | const { | ||||
appState: { settings }, | |||||
component, | component, | ||||
createdAfterIncludesTime, | createdAfterIncludesTime, | ||||
facets, | facets, | ||||
branchLike | branchLike | ||||
} = this.props; | } = this.props; | ||||
const disableDeveloperAggregatedInfo = | |||||
settings[GlobalSettingKeys.DeveloperAggregatedInfoDisabled] === 'true'; | |||||
const branch = | const branch = | ||||
(isBranch(branchLike) && branchLike.name) || | (isBranch(branchLike) && branchLike.name) || | ||||
(isPullRequest(branchLike) && branchLike.branch) || | (isPullRequest(branchLike) && branchLike.branch) || | ||||
/> | /> | ||||
)} | )} | ||||
{this.renderComponentFacets()} | {this.renderComponentFacets()} | ||||
{!this.props.myIssues && !this.props.disableDeveloperAggregatedInfo && ( | |||||
{!this.props.myIssues && !disableDeveloperAggregatedInfo && ( | |||||
<AssigneeFacet | <AssigneeFacet | ||||
assigned={query.assigned} | assigned={query.assigned} | ||||
assignees={query.assignees} | assignees={query.assignees} | ||||
stats={facets.assignees} | stats={facets.assignees} | ||||
/> | /> | ||||
)} | )} | ||||
{displayAuthorFacet && !this.props.disableDeveloperAggregatedInfo && ( | |||||
{displayAuthorFacet && !disableDeveloperAggregatedInfo && ( | |||||
<AuthorFacet | <AuthorFacet | ||||
author={query.author} | author={query.author} | ||||
component={component} | component={component} | ||||
} | } | ||||
} | } | ||||
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); |
import { flatten } from 'lodash'; | import { flatten } from 'lodash'; | ||||
import * as React from 'react'; | import * as React from 'react'; | ||||
import { mockComponent } from '../../../../helpers/mocks/component'; | 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 { ComponentQualifier } from '../../../../types/component'; | ||||
import { Query } from '../../utils'; | 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', () => { | it('should render facets for global page', () => { | ||||
expect(renderSidebar()).toMatchSnapshot(); | expect(renderSidebar()).toMatchSnapshot(); | ||||
}); | }); | ||||
it('should not render developer nominative facets when asked not to', () => { | 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']>) => { | const renderSidebar = (props?: Partial<Sidebar['props']>) => { | ||||
mapChildren( | mapChildren( | ||||
shallow<Sidebar>( | shallow<Sidebar>( | ||||
<Sidebar | <Sidebar | ||||
appState={mockAppState({ | |||||
settings: { [GlobalSettingKeys.DeveloperAggregatedInfoDisabled]: 'false' } | |||||
})} | |||||
component={undefined} | component={undefined} | ||||
createdAfterIncludesTime={false} | createdAfterIncludesTime={false} | ||||
facets={{}} | facets={{}} | ||||
referencedLanguages={{}} | referencedLanguages={{}} | ||||
referencedRules={{}} | referencedRules={{}} | ||||
referencedUsers={{}} | referencedUsers={{}} | ||||
disableDeveloperAggregatedInfo={false} | |||||
{...props} | {...props} | ||||
/> | /> | ||||
) | ) |
exports[`test behavior should correctly render facet item 1`] = ` | exports[`test behavior should correctly render facet item 1`] = ` | ||||
<React.Fragment> | <React.Fragment> | ||||
<Connect(Avatar) | |||||
<withAppStateContext(Avatar) | |||||
className="little-spacer-right" | className="little-spacer-right" | ||||
name="Name Baz" | name="Name Baz" | ||||
size={16} | size={16} | ||||
exports[`test behavior should correctly render facet item 2`] = ` | exports[`test behavior should correctly render facet item 2`] = ` | ||||
<React.Fragment> | <React.Fragment> | ||||
<Connect(Avatar) | |||||
<withAppStateContext(Avatar) | |||||
className="little-spacer-right" | className="little-spacer-right" | ||||
name="foo" | name="foo" | ||||
size={16} | size={16} | ||||
exports[`test behavior should correctly render search result correctly 1`] = ` | exports[`test behavior should correctly render search result correctly 1`] = ` | ||||
<React.Fragment> | <React.Fragment> | ||||
<Connect(Avatar) | |||||
<withAppStateContext(Avatar) | |||||
className="little-spacer-right" | className="little-spacer-right" | ||||
name="Name Bar" | name="Name Bar" | ||||
size={16} | size={16} | ||||
exports[`test behavior should correctly render search result correctly 2`] = ` | exports[`test behavior should correctly render search result correctly 2`] = ` | ||||
<React.Fragment> | <React.Fragment> | ||||
<Connect(Avatar) | |||||
<withAppStateContext(Avatar) | |||||
className="little-spacer-right" | className="little-spacer-right" | ||||
name="foo" | name="foo" | ||||
size={16} | size={16} |
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | ||||
*/ | */ | ||||
import * as React from 'react'; | import * as React from 'react'; | ||||
import { connect } from 'react-redux'; | |||||
import AdminContext from '../../app/components/AdminContext'; | import AdminContext from '../../app/components/AdminContext'; | ||||
import withAppStateContext from '../../app/components/app-state/withAppStateContext'; | 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 { 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'; | import App from './App'; | ||||
interface OwnProps { | |||||
export interface MarketplaceAppContainerProps { | |||||
location: { pathname: string; query: RawQuery }; | location: { pathname: string; query: RawQuery }; | ||||
appState: AppState; | 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 = { | 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 ( | return ( | ||||
); | ); | ||||
} | } | ||||
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); |
/* | |||||
* 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'); | |||||
}); | |||||
}); |
/* | |||||
* 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} | |||||
/> | |||||
); | |||||
} |
// 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} | |||||
/> | |||||
`; |
const routes = [ | const routes = [ | ||||
{ | { | ||||
indexRoute: { component: lazyLoadComponent(() => import('./AppContainer')) } | |||||
indexRoute: { component: lazyLoadComponent(() => import('./MarketplaceAppContainer')) } | |||||
} | } | ||||
]; | ]; | ||||
import { lazyLoadComponent } from '../../../components/lazyLoadComponent'; | import { lazyLoadComponent } from '../../../components/lazyLoadComponent'; | ||||
import { isPullRequest } from '../../../helpers/branch-like'; | import { isPullRequest } from '../../../helpers/branch-like'; | ||||
import { ProjectAlmBindingResponse } from '../../../types/alm-settings'; | import { ProjectAlmBindingResponse } from '../../../types/alm-settings'; | ||||
import { AppState } from '../../../types/appstate'; | |||||
import { BranchLike } from '../../../types/branch-like'; | import { BranchLike } from '../../../types/branch-like'; | ||||
import { isPortfolioLike } from '../../../types/component'; | import { isPortfolioLike } from '../../../types/component'; | ||||
import { AppState, Component } from '../../../types/types'; | |||||
import { Component } from '../../../types/types'; | |||||
import BranchOverview from '../branches/BranchOverview'; | import BranchOverview from '../branches/BranchOverview'; | ||||
const EmptyOverview = lazyLoadComponent(() => import('./EmptyOverview')); | const EmptyOverview = lazyLoadComponent(() => import('./EmptyOverview')); |
*/ | */ | ||||
import * as React from 'react'; | import * as React from 'react'; | ||||
import Tooltip from '../../../components/controls/Tooltip'; | 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 DrilldownLink from '../../../components/shared/DrilldownLink'; | ||||
import Rating from '../../../components/ui/Rating'; | import Rating from '../../../components/ui/Rating'; | ||||
import { findMeasure } from '../../../helpers/measures'; | import { findMeasure } from '../../../helpers/measures'; | ||||
} | } | ||||
const value = measure && (useDiffMetric ? getLeakValue(measure) : measure.value); | const value = measure && (useDiffMetric ? getLeakValue(measure) : measure.value); | ||||
const tooltip = value && getRatingTooltip(rating, Number(value)); | |||||
return ( | return ( | ||||
<Tooltip overlay={tooltip}> | |||||
<Tooltip overlay={value && <RatingTooltipContent metricKey={rating} value={value} />}> | |||||
<span> | <span> | ||||
<DrilldownLink | <DrilldownLink | ||||
branchLike={branchLike} | branchLike={branchLike} |
metric_domain.Reliability | metric_domain.Reliability | ||||
</span> | </span> | ||||
<Tooltip | <Tooltip | ||||
overlay="metric.reliability_rating.tooltip.A" | |||||
overlay={ | |||||
<withAppStateContext(RatingTooltipContent) | |||||
metricKey="reliability_rating" | |||||
value="1.0" | |||||
/> | |||||
} | |||||
> | > | ||||
<span> | <span> | ||||
<DrilldownLink | <DrilldownLink | ||||
metric_domain.Reliability | metric_domain.Reliability | ||||
</span> | </span> | ||||
<Tooltip | <Tooltip | ||||
overlay="metric.reliability_rating.tooltip.A" | |||||
overlay={ | |||||
<withAppStateContext(RatingTooltipContent) | |||||
metricKey="new_reliability_rating" | |||||
value="1.0" | |||||
/> | |||||
} | |||||
> | > | ||||
<span> | <span> | ||||
<DrilldownLink | <DrilldownLink | ||||
metric_domain.Maintainability | metric_domain.Maintainability | ||||
</span> | </span> | ||||
<Tooltip | <Tooltip | ||||
overlay="metric.sqale_rating.tooltip.A.0.0%" | |||||
overlay={ | |||||
<withAppStateContext(RatingTooltipContent) | |||||
metricKey="sqale_rating" | |||||
value="1.0" | |||||
/> | |||||
} | |||||
> | > | ||||
<span> | <span> | ||||
<DrilldownLink | <DrilldownLink | ||||
metric_domain.Maintainability | metric_domain.Maintainability | ||||
</span> | </span> | ||||
<Tooltip | <Tooltip | ||||
overlay="metric.sqale_rating.tooltip.A.0.0%" | |||||
overlay={ | |||||
<withAppStateContext(RatingTooltipContent) | |||||
metricKey="new_maintainability_rating" | |||||
value="1.0" | |||||
/> | |||||
} | |||||
> | > | ||||
<span> | <span> | ||||
<DrilldownLink | <DrilldownLink | ||||
metric_domain.Security | metric_domain.Security | ||||
</span> | </span> | ||||
<Tooltip | <Tooltip | ||||
overlay="metric.security_rating.tooltip.A" | |||||
overlay={ | |||||
<withAppStateContext(RatingTooltipContent) | |||||
metricKey="security_rating" | |||||
value="1.0" | |||||
/> | |||||
} | |||||
> | > | ||||
<span> | <span> | ||||
<DrilldownLink | <DrilldownLink | ||||
metric_domain.Security | metric_domain.Security | ||||
</span> | </span> | ||||
<Tooltip | <Tooltip | ||||
overlay="metric.security_rating.tooltip.A" | |||||
overlay={ | |||||
<withAppStateContext(RatingTooltipContent) | |||||
metricKey="new_security_rating" | |||||
value="1.0" | |||||
/> | |||||
} | |||||
> | > | ||||
<span> | <span> | ||||
<DrilldownLink | <DrilldownLink |
import withAppStateContext from '../../../app/components/app-state/withAppStateContext'; | import withAppStateContext from '../../../app/components/app-state/withAppStateContext'; | ||||
import Suggestions from '../../../app/components/embed-docs-modal/Suggestions'; | import Suggestions from '../../../app/components/embed-docs-modal/Suggestions'; | ||||
import { translate } from '../../../helpers/l10n'; | 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 '../../permissions/styles.css'; | ||||
import { mergeDefaultsToTemplates, mergePermissionsToTemplates, sortPermissions } from '../utils'; | import { mergeDefaultsToTemplates, mergePermissionsToTemplates, sortPermissions } from '../utils'; | ||||
import Home from './Home'; | import Home from './Home'; |
import * as React from 'react'; | import * as React from 'react'; | ||||
import withAppStateContext from '../../../../app/components/app-state/withAppStateContext'; | import withAppStateContext from '../../../../app/components/app-state/withAppStateContext'; | ||||
import ListFooter from '../../../../components/controls/ListFooter'; | import ListFooter from '../../../../components/controls/ListFooter'; | ||||
import { AppState } from '../../../../types/appstate'; | |||||
import { ComponentQualifier } from '../../../../types/component'; | 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 HoldersList from '../../shared/components/HoldersList'; | ||||
import SearchForm from '../../shared/components/SearchForm'; | import SearchForm from '../../shared/components/SearchForm'; | ||||
import { | import { |
<div | <div | ||||
className="display-flex-center" | className="display-flex-center" | ||||
> | > | ||||
<Connect(Avatar) | |||||
<withAppStateContext(Avatar) | |||||
className="text-middle big-spacer-right flex-0" | className="text-middle big-spacer-right flex-0" | ||||
name="John Doe" | name="John Doe" | ||||
size={36} | size={36} |
import DeferredSpinner from '../../../components/ui/DeferredSpinner'; | import DeferredSpinner from '../../../components/ui/DeferredSpinner'; | ||||
import { isBranch, sortBranches } from '../../../helpers/branch-like'; | import { isBranch, sortBranches } from '../../../helpers/branch-like'; | ||||
import { translate } from '../../../helpers/l10n'; | import { translate } from '../../../helpers/l10n'; | ||||
import { AppState } from '../../../types/appstate'; | |||||
import { Branch, BranchLike } from '../../../types/branch-like'; | import { Branch, BranchLike } from '../../../types/branch-like'; | ||||
import { | import { | ||||
AppState, | |||||
Component, | Component, | ||||
NewCodePeriod, | NewCodePeriod, | ||||
NewCodePeriodSettingType, | NewCodePeriodSettingType, |
import * as React from 'react'; | import * as React from 'react'; | ||||
import { getValues } from '../../../api/settings'; | import { getValues } from '../../../api/settings'; | ||||
import withAppStateContext from '../../../app/components/app-state/withAppStateContext'; | import withAppStateContext from '../../../app/components/app-state/withAppStateContext'; | ||||
import { AppState } from '../../../types/appstate'; | |||||
import { SettingsKey } from '../../../types/settings'; | import { SettingsKey } from '../../../types/settings'; | ||||
import { AppState } from '../../../types/types'; | |||||
import LifetimeInformationRenderer from './LifetimeInformationRenderer'; | import LifetimeInformationRenderer from './LifetimeInformationRenderer'; | ||||
interface Props { | interface Props { |
import withAppStateContext from '../../app/components/app-state/withAppStateContext'; | import withAppStateContext from '../../app/components/app-state/withAppStateContext'; | ||||
import throwGlobalError from '../../app/utils/throwGlobalError'; | import throwGlobalError from '../../app/utils/throwGlobalError'; | ||||
import { translate } from '../../helpers/l10n'; | import { translate } from '../../helpers/l10n'; | ||||
import { AppState } from '../../types/appstate'; | |||||
import { DumpStatus, DumpTask } from '../../types/project-dump'; | import { DumpStatus, DumpTask } from '../../types/project-dump'; | ||||
import { TaskStatuses, TaskTypes } from '../../types/tasks'; | import { TaskStatuses, TaskTypes } from '../../types/tasks'; | ||||
import { AppState, Component } from '../../types/types'; | |||||
import { Component } from '../../types/types'; | |||||
import Export from './components/Export'; | import Export from './components/Export'; | ||||
import Import from './components/Import'; | import Import from './components/Import'; | ||||
import './styles.css'; | import './styles.css'; |
import { addSideBarClass, removeSideBarClass } from '../../../helpers/pages'; | import { addSideBarClass, removeSideBarClass } from '../../../helpers/pages'; | ||||
import { get, save } from '../../../helpers/storage'; | import { get, save } from '../../../helpers/storage'; | ||||
import { isLoggedIn } from '../../../helpers/users'; | import { isLoggedIn } from '../../../helpers/users'; | ||||
import { AppState } from '../../../types/appstate'; | |||||
import { ComponentQualifier } from '../../../types/component'; | 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 { hasFilterParams, hasViewParams, parseUrlQuery, Query } from '../query'; | ||||
import '../styles.css'; | import '../styles.css'; | ||||
import { Facets, Project } from '../types'; | import { Facets, Project } from '../types'; |
import { translate } from '../../../helpers/l10n'; | import { translate } from '../../../helpers/l10n'; | ||||
import { getComponentAdminUrl, getComponentOverviewUrl } from '../../../helpers/urls'; | import { getComponentAdminUrl, getComponentOverviewUrl } from '../../../helpers/urls'; | ||||
import { hasGlobalPermission } from '../../../helpers/users'; | import { hasGlobalPermission } from '../../../helpers/users'; | ||||
import { AppState } from '../../../types/appstate'; | |||||
import { ComponentQualifier } from '../../../types/component'; | import { ComponentQualifier } from '../../../types/component'; | ||||
import { Permissions } from '../../../types/permissions'; | import { Permissions } from '../../../types/permissions'; | ||||
import { AppState, LoggedInUser } from '../../../types/types'; | |||||
import { LoggedInUser } from '../../../types/types'; | |||||
export interface ApplicationCreationProps { | export interface ApplicationCreationProps { | ||||
appState: AppState; | appState: AppState; |
import SelectLegacy from '../../components/controls/SelectLegacy'; | import SelectLegacy from '../../components/controls/SelectLegacy'; | ||||
import QualifierIcon from '../../components/icons/QualifierIcon'; | import QualifierIcon from '../../components/icons/QualifierIcon'; | ||||
import { translate } from '../../helpers/l10n'; | 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 BulkApplyTemplateModal from './BulkApplyTemplateModal'; | ||||
import DeleteModal from './DeleteModal'; | import DeleteModal from './DeleteModal'; | ||||
import { Alert } from '../../../components/ui/Alert'; | import { Alert } from '../../../components/ui/Alert'; | ||||
import { getLocalizedMetricName, translate } from '../../../helpers/l10n'; | import { getLocalizedMetricName, translate } from '../../../helpers/l10n'; | ||||
import { isDiffMetric } from '../../../helpers/measures'; | import { isDiffMetric } from '../../../helpers/measures'; | ||||
import { AppState } from '../../../types/appstate'; | |||||
import { MetricKey } from '../../../types/metrics'; | 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 Condition from './Condition'; | ||||
import ConditionModal from './ConditionModal'; | import ConditionModal from './ConditionModal'; | ||||
<div | <div | ||||
className="display-flex-center permission-list-item padded" | className="display-flex-center permission-list-item padded" | ||||
> | > | ||||
<Connect(Avatar) | |||||
<withAppStateContext(Avatar) | |||||
className="spacer-right" | className="spacer-right" | ||||
name="John Doe" | name="John Doe" | ||||
size={32} | size={32} |
exports[`should render options correctly: user 1`] = ` | exports[`should render options correctly: user 1`] = ` | ||||
<React.Fragment> | <React.Fragment> | ||||
<Connect(Avatar) | |||||
<withAppStateContext(Avatar) | |||||
hash="A" | hash="A" | ||||
name="name" | name="name" | ||||
size={16} | size={16} |
className="pull-right spacer-top spacer-left spacer-right button-small" | className="pull-right spacer-top spacer-left spacer-right button-small" | ||||
onClick={[Function]} | onClick={[Function]} | ||||
/> | /> | ||||
<Connect(Avatar) | |||||
<withAppStateContext(Avatar) | |||||
className="pull-left spacer-right" | className="pull-left spacer-right" | ||||
name="Luke Skywalker" | name="Luke Skywalker" | ||||
size={32} | size={32} |
<div | <div | ||||
className="display-flex-center" | className="display-flex-center" | ||||
> | > | ||||
<Connect(Avatar) | |||||
<withAppStateContext(Avatar) | |||||
className="little-spacer-right" | className="little-spacer-right" | ||||
name="Luke Skywalker" | name="Luke Skywalker" | ||||
size={20} | size={20} | ||||
<div | <div | ||||
className="display-flex-center" | className="display-flex-center" | ||||
> | > | ||||
<Connect(Avatar) | |||||
<withAppStateContext(Avatar) | |||||
className="little-spacer-right" | className="little-spacer-right" | ||||
name="Luke Skywalker" | name="Luke Skywalker" | ||||
size={20} | size={20} | ||||
<div | <div | ||||
className="display-flex-center" | className="display-flex-center" | ||||
> | > | ||||
<Connect(Avatar) | |||||
<withAppStateContext(Avatar) | |||||
className="little-spacer-right" | className="little-spacer-right" | ||||
name="John Doe" | name="John Doe" | ||||
size={20} | size={20} | ||||
<div | <div | ||||
className="display-flex-center" | className="display-flex-center" | ||||
> | > | ||||
<Connect(Avatar) | |||||
<withAppStateContext(Avatar) | |||||
className="little-spacer-right" | className="little-spacer-right" | ||||
name="John Doe" | name="John Doe" | ||||
size={20} | size={20} | ||||
<div | <div | ||||
className="display-flex-center" | className="display-flex-center" | ||||
> | > | ||||
<Connect(Avatar) | |||||
<withAppStateContext(Avatar) | |||||
className="little-spacer-right" | className="little-spacer-right" | ||||
name="John Doe" | name="John Doe" | ||||
size={20} | size={20} | ||||
<div | <div | ||||
className="display-flex-center" | className="display-flex-center" | ||||
> | > | ||||
<Connect(Avatar) | |||||
<withAppStateContext(Avatar) | |||||
className="little-spacer-right" | className="little-spacer-right" | ||||
name="Luke Skywalker" | name="Luke Skywalker" | ||||
size={20} | size={20} | ||||
<div | <div | ||||
className="display-flex-center" | className="display-flex-center" | ||||
> | > | ||||
<Connect(Avatar) | |||||
<withAppStateContext(Avatar) | |||||
className="little-spacer-right" | className="little-spacer-right" | ||||
name="Luke Skywalker" | name="Luke Skywalker" | ||||
size={20} | size={20} | ||||
<div | <div | ||||
className="display-flex-center" | className="display-flex-center" | ||||
> | > | ||||
<Connect(Avatar) | |||||
<withAppStateContext(Avatar) | |||||
className="little-spacer-right" | className="little-spacer-right" | ||||
name="John Doe" | name="John Doe" | ||||
size={20} | size={20} | ||||
<div | <div | ||||
className="display-flex-center" | className="display-flex-center" | ||||
> | > | ||||
<Connect(Avatar) | |||||
<withAppStateContext(Avatar) | |||||
className="little-spacer-right" | className="little-spacer-right" | ||||
name="John Doe" | name="John Doe" | ||||
size={20} | size={20} | ||||
<div | <div | ||||
className="display-flex-center" | className="display-flex-center" | ||||
> | > | ||||
<Connect(Avatar) | |||||
<withAppStateContext(Avatar) | |||||
className="little-spacer-right" | className="little-spacer-right" | ||||
name="John Doe" | name="John Doe" | ||||
size={20} | size={20} | ||||
<div | <div | ||||
className="display-flex-center" | className="display-flex-center" | ||||
> | > | ||||
<Connect(Avatar) | |||||
<withAppStateContext(Avatar) | |||||
className="little-spacer-right" | className="little-spacer-right" | ||||
name="john.doe" | name="john.doe" | ||||
size={20} | size={20} | ||||
<div | <div | ||||
className="display-flex-center" | className="display-flex-center" | ||||
> | > | ||||
<Connect(Avatar) | |||||
<withAppStateContext(Avatar) | |||||
className="little-spacer-right" | className="little-spacer-right" | ||||
name="John Doe" | name="John Doe" | ||||
size={20} | size={20} | ||||
<div | <div | ||||
className="display-flex-center" | className="display-flex-center" | ||||
> | > | ||||
<Connect(Avatar) | |||||
<withAppStateContext(Avatar) | |||||
className="little-spacer-right" | className="little-spacer-right" | ||||
name="John Doe" | name="John Doe" | ||||
size={20} | size={20} |
key="john.doe" | key="john.doe" | ||||
onClick={[Function]} | onClick={[Function]} | ||||
> | > | ||||
<Connect(Avatar) | |||||
<withAppStateContext(Avatar) | |||||
className="spacer-right" | className="spacer-right" | ||||
name="John Doe" | name="John Doe" | ||||
size={16} | size={16} | ||||
key="highlighted" | key="highlighted" | ||||
onClick={[Function]} | onClick={[Function]} | ||||
> | > | ||||
<Connect(Avatar) | |||||
<withAppStateContext(Avatar) | |||||
className="spacer-right" | className="spacer-right" | ||||
name="John Doe" | name="John Doe" | ||||
size={16} | size={16} |
import { IndexLink } from 'react-router'; | import { IndexLink } from 'react-router'; | ||||
import withAppStateContext from '../../../app/components/app-state/withAppStateContext'; | import withAppStateContext from '../../../app/components/app-state/withAppStateContext'; | ||||
import { getGlobalSettingsUrl, getProjectSettingsUrl } from '../../../helpers/urls'; | 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 { getCategoryName } from '../utils'; | ||||
import { ADDITIONAL_CATEGORIES } from './AdditionalCategories'; | import { ADDITIONAL_CATEGORIES } from './AdditionalCategories'; | ||||
import CATEGORY_OVERRIDES from './CategoryOverrides'; | import CATEGORY_OVERRIDES from './CategoryOverrides'; |
AlmSettingsBindingStatus, | AlmSettingsBindingStatus, | ||||
AlmSettingsBindingStatusType | AlmSettingsBindingStatusType | ||||
} from '../../../../types/alm-settings'; | } from '../../../../types/alm-settings'; | ||||
import { AppState } from '../../../../types/appstate'; | |||||
import { ExtendedSettingDefinition } from '../../../../types/settings'; | import { ExtendedSettingDefinition } from '../../../../types/settings'; | ||||
import { AppState, Dict } from '../../../../types/types'; | |||||
import { Dict } from '../../../../types/types'; | |||||
import AlmIntegrationRenderer from './AlmIntegrationRenderer'; | import AlmIntegrationRenderer from './AlmIntegrationRenderer'; | ||||
interface Props extends Pick<WithRouterProps, 'location' | 'router'> { | interface Props extends Pick<WithRouterProps, 'location' | 'router'> { |
import { getEdition, getEditionUrl } from '../../../../helpers/editions'; | import { getEdition, getEditionUrl } from '../../../../helpers/editions'; | ||||
import { translate } from '../../../../helpers/l10n'; | import { translate } from '../../../../helpers/l10n'; | ||||
import { AlmKeys } from '../../../../types/alm-settings'; | import { AlmKeys } from '../../../../types/alm-settings'; | ||||
import { AppState } from '../../../../types/appstate'; | |||||
import { EditionKey } from '../../../../types/editions'; | import { EditionKey } from '../../../../types/editions'; | ||||
import { AppState } from '../../../../types/types'; | |||||
export interface CreationTooltipProps { | export interface CreationTooltipProps { | ||||
alm: AlmKeys; | alm: AlmKeys; |
AlmSettingsInstance, | AlmSettingsInstance, | ||||
ProjectAlmBindingResponse | ProjectAlmBindingResponse | ||||
} from '../../../../types/alm-settings'; | } from '../../../../types/alm-settings'; | ||||
import { AppState } from '../../../../types/appstate'; | |||||
import { EditionKey } from '../../../../types/editions'; | import { EditionKey } from '../../../../types/editions'; | ||||
import { AppState, Dict } from '../../../../types/types'; | |||||
import { Dict } from '../../../../types/types'; | |||||
export interface AlmSpecificFormProps { | export interface AlmSpecificFormProps { | ||||
alm: AlmKeys; | alm: AlmKeys; |
/* | |||||
* 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' }); | |||||
}); |
/* | |||||
* 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)); | |||||
} |
/* | |||||
* 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]; | |||||
} |
import { Alert } from '../../../components/ui/Alert'; | import { Alert } from '../../../components/ui/Alert'; | ||||
import { toShortNotSoISOString } from '../../../helpers/dates'; | import { toShortNotSoISOString } from '../../../helpers/dates'; | ||||
import { translate } from '../../../helpers/l10n'; | import { translate } from '../../../helpers/l10n'; | ||||
import { AppState } from '../../../types/types'; | |||||
import { AppState } from '../../../types/appstate'; | |||||
import PageActions from './PageActions'; | import PageActions from './PageActions'; | ||||
export interface Props { | export interface Props { |
<td | <td | ||||
className="thin nowrap text-middle" | className="thin nowrap text-middle" | ||||
> | > | ||||
<Connect(Avatar) | |||||
<withAppStateContext(Avatar) | |||||
name="One" | name="One" | ||||
size={36} | size={36} | ||||
/> | /> | ||||
<td | <td | ||||
className="thin nowrap text-middle" | className="thin nowrap text-middle" | ||||
> | > | ||||
<Connect(Avatar) | |||||
<withAppStateContext(Avatar) | |||||
name="One" | name="One" | ||||
size={36} | size={36} | ||||
/> | /> |
role="listitem" | role="listitem" | ||||
title="Administrator" | title="Administrator" | ||||
> | > | ||||
<Connect(Avatar) | |||||
<withAppStateContext(Avatar) | |||||
name="Administrator" | name="Administrator" | ||||
size={16} | size={16} | ||||
/> | /> | ||||
role="listitem" | role="listitem" | ||||
title="Administrator" | title="Administrator" | ||||
> | > | ||||
<Connect(Avatar) | |||||
<withAppStateContext(Avatar) | |||||
hash="7daf6c79d4802916d83f6266e24850af" | hash="7daf6c79d4802916d83f6266e24850af" | ||||
name="Administrator" | name="Administrator" | ||||
size={16} | size={16} | ||||
<div | <div | ||||
className="Select-value-label" | className="Select-value-label" | ||||
> | > | ||||
<Connect(Avatar) | |||||
<withAppStateContext(Avatar) | |||||
hash="7daf6c79d4802916d83f6266e24850af" | hash="7daf6c79d4802916d83f6266e24850af" | ||||
name="Administrator" | name="Administrator" | ||||
size={16} | size={16} | ||||
<div | <div | ||||
className="Select-value-label" | className="Select-value-label" | ||||
> | > | ||||
<Connect(Avatar) | |||||
<withAppStateContext(Avatar) | |||||
name="Administrator" | name="Administrator" | ||||
size={16} | size={16} | ||||
/> | /> |
import addGlobalSuccessMessage from '../../app/utils/addGlobalSuccessMessage'; | import addGlobalSuccessMessage from '../../app/utils/addGlobalSuccessMessage'; | ||||
import { translate, translateWithParameters } from '../../helpers/l10n'; | import { translate, translateWithParameters } from '../../helpers/l10n'; | ||||
import { isLoggedIn } from '../../helpers/users'; | import { isLoggedIn } from '../../helpers/users'; | ||||
import { AppState } from '../../types/appstate'; | |||||
import { Branch } from '../../types/branch-like'; | import { Branch } from '../../types/branch-like'; | ||||
import { ComponentQualifier } from '../../types/component'; | import { ComponentQualifier } from '../../types/component'; | ||||
import { ComponentReportStatus } from '../../types/component-report'; | 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 { withCurrentUser } from '../hoc/withCurrentUser'; | ||||
import ComponentReportActionsRenderer from './ComponentReportActionsRenderer'; | import ComponentReportActionsRenderer from './ComponentReportActionsRenderer'; | ||||
import withAppStateContext from '../../app/components/app-state/withAppStateContext'; | import withAppStateContext from '../../app/components/app-state/withAppStateContext'; | ||||
import DetachIcon from '../../components/icons/DetachIcon'; | import DetachIcon from '../../components/icons/DetachIcon'; | ||||
import { isSonarCloud } from '../../helpers/system'; | import { isSonarCloud } from '../../helpers/system'; | ||||
import { AppState } from '../../types/types'; | |||||
import { AppState } from '../../types/appstate'; | |||||
interface OwnProps { | interface OwnProps { | ||||
appState: AppState; | appState: AppState; |
<span | <span | ||||
className="text-top" | className="text-top" | ||||
> | > | ||||
<Connect(Avatar) | |||||
<withAppStateContext(Avatar) | |||||
className="little-spacer-right" | className="little-spacer-right" | ||||
hash="gravatarhash" | hash="gravatarhash" | ||||
name="" | name="" | ||||
<span | <span | ||||
className="text-top" | className="text-top" | ||||
> | > | ||||
<Connect(Avatar) | |||||
<withAppStateContext(Avatar) | |||||
className="little-spacer-right" | className="little-spacer-right" | ||||
hash="gravatarhash" | hash="gravatarhash" | ||||
name="" | name="" | ||||
<span | <span | ||||
className="text-top" | className="text-top" | ||||
> | > | ||||
<Connect(Avatar) | |||||
<withAppStateContext(Avatar) | |||||
className="little-spacer-right" | className="little-spacer-right" | ||||
hash="gravatarhash" | hash="gravatarhash" | ||||
name="" | name="" |
className="issue-comment-author" | className="issue-comment-author" | ||||
title="John Doe" | title="John Doe" | ||||
> | > | ||||
<Connect(Avatar) | |||||
<withAppStateContext(Avatar) | |||||
className="little-spacer-right" | className="little-spacer-right" | ||||
hash="gravatarhash" | hash="gravatarhash" | ||||
name="John Doe" | name="John Doe" | ||||
className="issue-comment-author" | className="issue-comment-author" | ||||
title="John Doe" | title="John Doe" | ||||
> | > | ||||
<Connect(Avatar) | |||||
<withAppStateContext(Avatar) | |||||
className="little-spacer-right" | className="little-spacer-right" | ||||
hash="gravatarhash" | hash="gravatarhash" | ||||
name="John Doe" | name="John Doe" | ||||
className="issue-comment-author" | className="issue-comment-author" | ||||
title="John Doe" | title="John Doe" | ||||
> | > | ||||
<Connect(Avatar) | |||||
<withAppStateContext(Avatar) | |||||
className="little-spacer-right" | className="little-spacer-right" | ||||
hash="gravatarhash" | hash="gravatarhash" | ||||
name="John Doe" | name="John Doe" | ||||
className="issue-comment-author" | className="issue-comment-author" | ||||
title="user.x_deleted.john.doe" | title="user.x_deleted.john.doe" | ||||
> | > | ||||
<Connect(Avatar) | |||||
<withAppStateContext(Avatar) | |||||
className="little-spacer-right" | className="little-spacer-right" | ||||
hash="gravatarhash" | hash="gravatarhash" | ||||
name="john.doe" | name="john.doe" |
className="text-left text-top" | className="text-left text-top" | ||||
> | > | ||||
<p> | <p> | ||||
<Connect(Avatar) | |||||
<withAppStateContext(Avatar) | |||||
className="little-spacer-right" | className="little-spacer-right" | ||||
hash="gravatarhash" | hash="gravatarhash" | ||||
name="John Doe" | name="John Doe" | ||||
className="text-left text-top" | className="text-left text-top" | ||||
> | > | ||||
<p> | <p> | ||||
<Connect(Avatar) | |||||
<withAppStateContext(Avatar) | |||||
className="little-spacer-right" | className="little-spacer-right" | ||||
name="john.doe" | name="john.doe" | ||||
size={16} | size={16} |
item="luke" | item="luke" | ||||
key="luke" | key="luke" | ||||
> | > | ||||
<Connect(Avatar) | |||||
<withAppStateContext(Avatar) | |||||
className="spacer-right" | className="spacer-right" | ||||
name="Skywalker" | name="Skywalker" | ||||
size={16} | size={16} |
> | > | ||||
<span> | <span> | ||||
assigned_to | assigned_to | ||||
<Connect(Avatar) | |||||
<withAppStateContext(Avatar) | |||||
className="little-spacer-left little-spacer-right" | className="little-spacer-left little-spacer-right" | ||||
name="Luke Skywalker" | name="Luke Skywalker" | ||||
size={16} | size={16} | ||||
> | > | ||||
<span> | <span> | ||||
assigned_to | assigned_to | ||||
<Connect(Avatar) | |||||
<withAppStateContext(Avatar) | |||||
className="little-spacer-left little-spacer-right" | className="little-spacer-left little-spacer-right" | ||||
name="luke" | name="luke" | ||||
size={16} | size={16} |
import Level from '../../components/ui/Level'; | import Level from '../../components/ui/Level'; | ||||
import Rating from '../../components/ui/Rating'; | import Rating from '../../components/ui/Rating'; | ||||
import { formatMeasure } from '../../helpers/measures'; | import { formatMeasure } from '../../helpers/measures'; | ||||
import { getRatingTooltip } from './utils'; | |||||
import RatingTooltipContent from './RatingTooltipContent'; | |||||
interface Props { | interface Props { | ||||
className?: string; | className?: string; | ||||
return <span className={className}>{formattedValue != null ? formattedValue : '–'}</span>; | 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} />; | const rating = <Rating small={small} value={value} />; | ||||
if (tooltip) { | if (tooltip) { | ||||
return ( | return ( | ||||
<Tooltip overlay={tooltip}> | <Tooltip overlay={tooltip}> |
/* | |||||
* 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); |
import * as React from 'react'; | import * as React from 'react'; | ||||
import Measure from '../Measure'; | 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', () => { | it('renders trivial measure', () => { | ||||
expect( | expect( | ||||
shallow(<Measure metricKey="coverage" metricType="PERCENT" value="73.0" />) | shallow(<Measure metricKey="coverage" metricType="PERCENT" value="73.0" />) | ||||
).toMatchSnapshot(); | ).toMatchSnapshot(); | ||||
}); | }); | ||||
it('renders known RATING', () => { | |||||
it('renders RATING', () => { | |||||
expect( | expect( | ||||
shallow(<Measure metricKey="sqale_rating" metricType="RATING" value="3" />) | shallow(<Measure metricKey="sqale_rating" metricType="RATING" value="3" />) | ||||
).toMatchSnapshot(); | ).toMatchSnapshot(); | ||||
}); | }); | ||||
it('renders unknown RATING', () => { | |||||
expect( | |||||
shallow(<Measure metricKey="foo_rating" metricType="RATING" value="4" />) | |||||
).toMatchSnapshot(); | |||||
}); | |||||
it('renders undefined measure', () => { | it('renders undefined measure', () => { | ||||
expect( | expect( | ||||
shallow(<Measure metricKey="foo" metricType="PERCENT" value={undefined} />) | shallow(<Measure metricKey="foo" metricType="PERCENT" value={undefined} />) |
/* | |||||
* 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} | |||||
/> | |||||
); | |||||
} |
/> | /> | ||||
`; | `; | ||||
exports[`renders known RATING 1`] = ` | |||||
exports[`renders RATING 1`] = ` | |||||
<Tooltip | <Tooltip | ||||
overlay="tooltip" | |||||
overlay={ | |||||
<withAppStateContext(RatingTooltipContent) | |||||
metricKey="sqale_rating" | |||||
value="3" | |||||
/> | |||||
} | |||||
> | > | ||||
<span> | <span> | ||||
<Rating | <Rating | ||||
– | – | ||||
</span> | </span> | ||||
`; | `; | ||||
exports[`renders unknown RATING 1`] = ` | |||||
<Rating | |||||
value="4" | |||||
/> | |||||
`; |
// 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> | |||||
`; |
* along with this program; if not, write to the Free Software Foundation, | * along with this program; if not, write to the Free Software Foundation, | ||||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | * 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'; | import { Dict, Measure, MeasureEnhanced, MeasureIntern, Metric } from '../../types/types'; | ||||
const KNOWN_RATINGS = [ | |||||
export const KNOWN_RATINGS = [ | |||||
'sqale_rating', | 'sqale_rating', | ||||
'maintainability_rating', // Needed to provide the label for "new_maintainability_rating" | 'maintainability_rating', // Needed to provide the label for "new_maintainability_rating" | ||||
'reliability_rating', | 'reliability_rating', | ||||
export function getLeakValue(measure: MeasureIntern | undefined): string | undefined { | export function getLeakValue(measure: MeasureIntern | undefined): string | undefined { | ||||
return measure?.period?.value; | 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; | |||||
} |
import { ALM_DOCUMENTATION_PATHS } from '../../../../helpers/constants'; | import { ALM_DOCUMENTATION_PATHS } from '../../../../helpers/constants'; | ||||
import { translate } from '../../../../helpers/l10n'; | import { translate } from '../../../../helpers/l10n'; | ||||
import { AlmKeys } from '../../../../types/alm-settings'; | import { AlmKeys } from '../../../../types/alm-settings'; | ||||
import { AppState } from '../../../../types/types'; | |||||
import { AppState } from '../../../../types/appstate'; | |||||
import SentenceWithHighlights from '../../components/SentenceWithHighlights'; | import SentenceWithHighlights from '../../components/SentenceWithHighlights'; | ||||
export interface PublishStepsProps { | export interface PublishStepsProps { |
import { Dictionary } from 'lodash'; | import { Dictionary } from 'lodash'; | ||||
import * as React from 'react'; | import * as React from 'react'; | ||||
import withAppStateContext from '../../../app/components/app-state/withAppStateContext'; | 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 { CompilationInfo } from '../components/CompilationInfo'; | ||||
import CreateYmlFile from '../components/CreateYmlFile'; | import CreateYmlFile from '../components/CreateYmlFile'; | ||||
import { BuildTools } from '../types'; | import { BuildTools } from '../types'; |
import { translate } from '../../../helpers/l10n'; | import { translate } from '../../../helpers/l10n'; | ||||
import { getBaseUrl } from '../../../helpers/system'; | import { getBaseUrl } from '../../../helpers/system'; | ||||
import { AlmKeys } from '../../../types/alm-settings'; | import { AlmKeys } from '../../../types/alm-settings'; | ||||
import { AppState } from '../../../types/types'; | |||||
import { AppState } from '../../../types/appstate'; | |||||
import SentenceWithHighlights from './SentenceWithHighlights'; | import SentenceWithHighlights from './SentenceWithHighlights'; | ||||
export interface AllSetProps { | export interface AllSetProps { |
*/ | */ | ||||
import * as React from 'react'; | import * as React from 'react'; | ||||
import withAppStateContext from '../../../app/components/app-state/withAppStateContext'; | 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 { BuildTools } from '../types'; | ||||
import CFamily from './commands/CFamily'; | import CFamily from './commands/CFamily'; | ||||
import DotNet from './commands/DotNet'; | import DotNet from './commands/DotNet'; |
import withAppStateContext from '../../../app/components/app-state/withAppStateContext'; | import withAppStateContext from '../../../app/components/app-state/withAppStateContext'; | ||||
import { ClipboardIconButton } from '../../../components/controls/clipboard'; | import { ClipboardIconButton } from '../../../components/controls/clipboard'; | ||||
import { translate } from '../../../helpers/l10n'; | import { translate } from '../../../helpers/l10n'; | ||||
import { AppState } from '../../../types/types'; | |||||
import { AppState } from '../../../types/appstate'; | |||||
import FinishButton from '../components/FinishButton'; | import FinishButton from '../components/FinishButton'; | ||||
import GithubCFamilyExampleRepositories from '../components/GithubCFamilyExampleRepositories'; | import GithubCFamilyExampleRepositories from '../components/GithubCFamilyExampleRepositories'; | ||||
import Step from '../components/Step'; | import Step from '../components/Step'; |
AlmSettingsInstance, | AlmSettingsInstance, | ||||
ProjectAlmBindingResponse | ProjectAlmBindingResponse | ||||
} from '../../../types/alm-settings'; | } 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 AllSetStep from '../components/AllSetStep'; | ||||
import JenkinsfileStep from './JenkinsfileStep'; | import JenkinsfileStep from './JenkinsfileStep'; | ||||
import MultiBranchPipelineStep from './MultiBranchPipelineStep'; | import MultiBranchPipelineStep from './MultiBranchPipelineStep'; |
*/ | */ | ||||
import classNames from 'classnames'; | import classNames from 'classnames'; | ||||
import * as React from 'react'; | 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 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 { | interface Props { | ||||
appState: AppState; | |||||
className?: string; | className?: string; | ||||
enableGravatar: boolean; | |||||
gravatarServerUrl: string; | |||||
hash?: string; | hash?: string; | ||||
name?: string; | name?: string; | ||||
size: number; | 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 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 ( | return ( | ||||
<img | <img | ||||
alt={props.name} | |||||
className={classNames(props.className, 'rounded')} | |||||
height={props.size} | |||||
alt={name} | |||||
className={classNames(className, 'rounded')} | |||||
height={size} | |||||
src={url} | 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); |
*/ | */ | ||||
import { shallow } from 'enzyme'; | import { shallow } from 'enzyme'; | ||||
import * as React from 'react'; | 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}'; | const gravatarServerUrl = 'http://example.com/{EMAIL_MD5}.jpg?s={SIZE}'; | ||||
it('should be able to render with hash only', () => { | it('should be able to render with hash only', () => { | ||||
const avatar = shallow( | const avatar = shallow( | ||||
<Avatar | <Avatar | ||||
enableGravatar={true} | |||||
gravatarServerUrl={gravatarServerUrl} | |||||
appState={mockAppState({ | |||||
settings: { | |||||
[GlobalSettingKeys.EnableGravatar]: 'true', | |||||
[GlobalSettingKeys.GravatarServerUrl]: gravatarServerUrl | |||||
} | |||||
})} | |||||
hash="7daf6c79d4802916d83f6266e24850af" | hash="7daf6c79d4802916d83f6266e24850af" | ||||
name="Foo" | name="Foo" | ||||
size={30} | size={30} | ||||
it('falls back to dummy avatar', () => { | it('falls back to dummy avatar', () => { | ||||
const avatar = shallow( | const avatar = shallow( | ||||
<Avatar enableGravatar={false} gravatarServerUrl="" name="Foo Bar" size={30} /> | |||||
<Avatar appState={mockAppState({ settings: {} })} name="Foo Bar" size={30} /> | |||||
); | ); | ||||
expect(avatar).toMatchSnapshot(); | expect(avatar).toMatchSnapshot(); | ||||
}); | }); | ||||
it('do not fail when name is missing', () => { | it('do not fail when name is missing', () => { | ||||
const avatar = shallow( | const avatar = shallow( | ||||
<Avatar enableGravatar={false} gravatarServerUrl="" name={undefined} size={30} /> | |||||
<Avatar appState={mockAppState({ settings: {} })} name={undefined} size={30} /> | |||||
); | ); | ||||
expect(avatar.getElement()).toBeNull(); | expect(avatar.getElement()).toBeNull(); | ||||
}); | }); |
import * as React from 'react'; | import * as React from 'react'; | ||||
import withAppStateContext from '../../app/components/app-state/withAppStateContext'; | import withAppStateContext from '../../app/components/app-state/withAppStateContext'; | ||||
import { translate } from '../../helpers/l10n'; | import { translate } from '../../helpers/l10n'; | ||||
import { AppState } from '../../types/appstate'; | |||||
import { EditionKey } from '../../types/editions'; | import { EditionKey } from '../../types/editions'; | ||||
import { SystemUpgrade } from '../../types/system'; | import { SystemUpgrade } from '../../types/system'; | ||||
import { AppState } from '../../types/types'; | |||||
import { ResetButtonLink } from '../controls/buttons'; | import { ResetButtonLink } from '../controls/buttons'; | ||||
import Modal from '../controls/Modal'; | import Modal from '../controls/Modal'; | ||||
import { Alert, AlertVariant } from '../ui/Alert'; | import { Alert, AlertVariant } from '../ui/Alert'; |
/* | |||||
* 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 }; |
return metricKey.indexOf('new_') === 0; | 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[]) { | export function getDisplayMetrics(metrics: Metric[]) { | ||||
return metrics.filter(metric => !metric.hidden && !['DATA', 'DISTRIB'].includes(metric.type)); | return metrics.filter(metric => !metric.hidden && !['DATA', 'DISTRIB'].includes(metric.type)); | ||||
} | } |