--- /dev/null
+/*
+ * 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 { getJSON } from '../helpers/request';
+import { Feature } from '../types/features';
+
+export function getAvailableFeatures(): Promise<Feature[]> {
+ return getJSON('/api/features/list');
+}
--- /dev/null
+/*
+ * 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 { Feature } from '../../../types/features';
+
+export const DEFAULT_AVAILABLE_FEATURES = [];
+
+export const AvailableFeaturesContext = React.createContext<Feature[]>(DEFAULT_AVAILABLE_FEATURES);
--- /dev/null
+/*
+ * 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 { Feature } from '../../../../types/features';
+import withAvailableFeatures, { WithAvailableFeaturesProps } from '../withAvailableFeatures';
+
+jest.mock('../AvailableFeaturesContext', () => {
+ return {
+ AvailableFeaturesContext: {
+ Consumer: ({ children }: { children: (props: {}) => React.ReactNode }) => {
+ return children([Feature.MonoRepositoryPullRequestDecoration]);
+ }
+ }
+ };
+});
+
+class Wrapped extends React.Component<WithAvailableFeaturesProps> {
+ render() {
+ return <div />;
+ }
+}
+
+const UnderTest = withAvailableFeatures(Wrapped);
+
+it('should provide a way to check if a feature is available', () => {
+ const wrapper = shallow(<UnderTest />);
+ expect(wrapper.dive().type()).toBe(Wrapped);
+ expect(
+ wrapper
+ .dive<Wrapped>()
+ .props()
+ .hasFeature(Feature.MonoRepositoryPullRequestDecoration)
+ ).toBe(true);
+});
--- /dev/null
+/*
+ * 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 { getWrappedDisplayName } from '../../../components/hoc/utils';
+import { Feature } from '../../../types/features';
+import { AvailableFeaturesContext } from './AvailableFeaturesContext';
+
+export interface WithAvailableFeaturesProps {
+ hasFeature: (feature: Feature) => boolean;
+}
+
+export default function withAvailableFeatures<P>(
+ WrappedComponent: React.ComponentType<P & WithAvailableFeaturesProps>
+) {
+ return class WithAvailableFeatures extends React.PureComponent<
+ Omit<P, keyof WithAvailableFeaturesProps>
+ > {
+ static displayName = getWrappedDisplayName(WrappedComponent, 'withAvailableFeaturesContext');
+
+ render() {
+ return (
+ <AvailableFeaturesContext.Consumer>
+ {availableFeatures => (
+ <WrappedComponent
+ hasFeature={feature => availableFeatures.includes(feature)}
+ {...(this.props as P)}
+ />
+ )}
+ </AvailableFeaturesContext.Consumer>
+ );
+ }
+ };
+}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { getAvailableFeatures } from '../api/features';
import { getGlobalNavigation } from '../api/navigation';
import { getCurrentUser } from '../api/users';
import { installExtensionsHandler, installWebAnalyticsHandler } from '../helpers/extensionsHandler';
initApplication();
async function initApplication() {
- const [l10nBundle, currentUser, appState] = await Promise.all([
+ const [l10nBundle, currentUser, appState, availableFeatures] = await Promise.all([
loadL10nBundle(),
isMainApp() ? getCurrentUser() : undefined,
- isMainApp() ? getGlobalNavigation() : undefined
+ isMainApp() ? getGlobalNavigation() : undefined,
+ isMainApp() ? getAvailableFeatures() : undefined
]).catch(error => {
// eslint-disable-next-line no-console
console.error('Application failed to start', error);
});
const startReactApp = await import('./utils/startReactApp').then(i => i.default);
- startReactApp(l10nBundle.locale, currentUser, appState);
+ startReactApp(l10nBundle.locale, currentUser, appState, availableFeatures);
}
function isMainApp() {
import { omitNil } from '../../helpers/request';
import { getBaseUrl } from '../../helpers/system';
import { AppState } from '../../types/appstate';
+import { Feature } from '../../types/features';
import { CurrentUser } from '../../types/users';
import AdminContainer from '../components/AdminContainer';
import App from '../components/App';
import { DEFAULT_APP_STATE } from '../components/app-state/AppStateContext';
import AppStateContextProvider from '../components/app-state/AppStateContextProvider';
+import {
+ AvailableFeaturesContext,
+ DEFAULT_AVAILABLE_FEATURES
+} from '../components/available-features/AvailableFeaturesContext';
import ComponentContainer from '../components/ComponentContainer';
import CurrentUserContextProvider from '../components/current-user/CurrentUserContextProvider';
import GlobalAdminPageExtension from '../components/extensions/GlobalAdminPageExtension';
export default function startReactApp(
lang: string,
currentUser?: CurrentUser,
- appState?: AppState
+ appState?: AppState,
+ availableFeatures?: Feature[]
) {
exportModulesAsGlobals();
render(
<HelmetProvider>
<AppStateContextProvider appState={appState ?? DEFAULT_APP_STATE}>
- <CurrentUserContextProvider currentUser={currentUser}>
- <IntlProvider defaultLocale={lang} locale={lang}>
- <GlobalMessagesContainer />
- <BrowserRouter basename={getBaseUrl()}>
- <Routes>
- {renderRedirects()}
+ <AvailableFeaturesContext.Provider value={availableFeatures ?? DEFAULT_AVAILABLE_FEATURES}>
+ <CurrentUserContextProvider currentUser={currentUser}>
+ <IntlProvider defaultLocale={lang} locale={lang}>
+ <GlobalMessagesContainer />
+ <BrowserRouter basename={getBaseUrl()}>
+ <Routes>
+ {renderRedirects()}
+ <Route path="formatting/help" element={<FormattingHelp />} />
- <Route path="formatting/help" element={<FormattingHelp />} />
+ <Route element={<SimpleContainer />}>{maintenanceRoutes()}</Route>
- <Route element={<SimpleContainer />}>{maintenanceRoutes()}</Route>
+ <Route element={<MigrationContainer />}>
+ {sessionsRoutes()}
- <Route element={<MigrationContainer />}>
- {sessionsRoutes()}
+ <Route path="/" element={<App />}>
+ <Route index={true} element={<Landing />} />
- <Route path="/" element={<App />}>
- <Route index={true} element={<Landing />} />
+ <Route element={<GlobalContainer />}>
+ {accountRoutes()}
- <Route element={<GlobalContainer />}>
- {accountRoutes()}
+ {codingRulesRoutes()}
- {codingRulesRoutes()}
+ {documentationRoutes()}
- {documentationRoutes()}
+ <Route
+ path="extension/:pluginKey/:extensionKey"
+ element={<GlobalPageExtension />}
+ />
- <Route
- path="extension/:pluginKey/:extensionKey"
- element={<GlobalPageExtension />}
- />
-
- {globalIssuesRoutes()}
+ {globalIssuesRoutes()}
- {projectsRoutes()}
+ {projectsRoutes()}
- {qualityGatesRoutes()}
- {qualityProfilesRoutes()}
+ {qualityGatesRoutes()}
+ {qualityProfilesRoutes()}
- <Route path="portfolios" element={<PortfoliosPage />} />
- {webAPIRoutes()}
+ <Route path="portfolios" element={<PortfoliosPage />} />
+ {webAPIRoutes()}
- {renderComponentRoutes()}
+ {renderComponentRoutes()}
- {renderAdminRoutes()}
- </Route>
- <Route
- // We don't want this route to have any menu.
- // That is why we can not have it under the accountRoutes
- path="account/reset_password"
- element={<ResetPassword />}
- />
+ {renderAdminRoutes()}
+ </Route>
+ <Route
+ // We don't want this route to have any menu.
+ // That is why we can not have it under the accountRoutes
+ path="account/reset_password"
+ element={<ResetPassword />}
+ />
- <Route
- // We don't want this route to have any menu. This is why we define it here
- // rather than under the admin routes.
- path="admin/change_admin_password"
- element={<ChangeAdminPasswordApp />}
- />
+ <Route
+ // We don't want this route to have any menu. This is why we define it here
+ // rather than under the admin routes.
+ path="admin/change_admin_password"
+ element={<ChangeAdminPasswordApp />}
+ />
- <Route
- // We don't want this route to have any menu. This is why we define it here
- // rather than under the admin routes.
- path="admin/plugin_risk_consent"
- element={<PluginRiskConsent />}
- />
- <Route path="not_found" element={<NotFound />} />
- <Route path="*" element={<NotFound />} />
+ <Route
+ // We don't want this route to have any menu. This is why we define it here
+ // rather than under the admin routes.
+ path="admin/plugin_risk_consent"
+ element={<PluginRiskConsent />}
+ />
+ <Route path="not_found" element={<NotFound />} />
+ <Route path="*" element={<NotFound />} />
+ </Route>
</Route>
- </Route>
- </Routes>
- </BrowserRouter>
- </IntlProvider>
- </CurrentUserContextProvider>
+ </Routes>
+ </BrowserRouter>
+ </IntlProvider>
+ </CurrentUserContextProvider>
+ </AvailableFeaturesContext.Provider>
</AppStateContextProvider>
</HelmetProvider>,
el
import * as React from 'react';
import { FormattedMessage } from 'react-intl';
import { Link } from 'react-router-dom';
-import withAppStateContext from '../../../../app/components/app-state/withAppStateContext';
+import withAvailableFeatures, {
+ WithAvailableFeaturesProps
+} from '../../../../app/components/available-features/withAvailableFeatures';
import Toggle from '../../../../components/controls/Toggle';
import { Alert } from '../../../../components/ui/Alert';
import MandatoryFieldMarker from '../../../../components/ui/MandatoryFieldMarker';
AlmSettingsInstance,
ProjectAlmBindingResponse
} from '../../../../types/alm-settings';
-import { AppState } from '../../../../types/appstate';
-import { EditionKey } from '../../../../types/editions';
+import { Feature } from '../../../../types/features';
import { Dict } from '../../../../types/types';
-export interface AlmSpecificFormProps {
+export interface AlmSpecificFormProps extends WithAvailableFeaturesProps {
alm: AlmKeys;
instances: AlmSettingsInstance[];
formData: Omit<ProjectAlmBindingResponse, 'alm'>;
onFieldChange: (id: keyof ProjectAlmBindingResponse, value: string | boolean) => void;
- appState: AppState;
}
interface LabelProps {
const {
alm,
instances,
- formData: { repository, slug, summaryCommentEnabled, monorepo },
- appState
+ formData: { repository, slug, summaryCommentEnabled, monorepo }
} = props;
let formFields: JSX.Element;
break;
}
- // This feature trigger will be replaced when SONAR-14349 is implemented
- const monorepoEnabled = [EditionKey.enterprise, EditionKey.datacenter].includes(
- appState.edition as EditionKey
- );
+ const monorepoEnabled = props.hasFeature(Feature.MonoRepositoryPullRequestDecoration);
return (
<>
);
}
-export default withAppStateContext(AlmSpecificForm);
+export default withAvailableFeatures(AlmSpecificForm);
import { shallow } from 'enzyme';
import * as React from 'react';
import { mockAlmSettingsInstance } from '../../../../../helpers/mocks/alm-settings';
-import { mockAppState } from '../../../../../helpers/testMocks';
import { AlmKeys, AlmSettingsInstance } from '../../../../../types/alm-settings';
-import { EditionKey } from '../../../../../types/editions';
import { AlmSpecificForm, AlmSpecificFormProps } from '../AlmSpecificForm';
it.each([
);
it('should render the monorepo field when the feature is supported', () => {
- expect(
- shallowRender(AlmKeys.Azure, { appState: mockAppState({ edition: EditionKey.enterprise }) })
- ).toMatchSnapshot();
+ expect(shallowRender(AlmKeys.Azure, { hasFeature: jest.fn(() => true) })).toMatchSnapshot();
});
function shallowRender(alm: AlmKeys, props: Partial<AlmSpecificFormProps> = {}) {
monorepo: false
}}
onFieldChange={jest.fn()}
- appState={mockAppState({ edition: EditionKey.developer })}
+ hasFeature={jest.fn()}
{...props}
/>
);
/>
</div>
</div>
- <withAppStateContext(AlmSpecificForm)
+ <withAvailableFeaturesContext(AlmSpecificForm)
alm="github"
formData={
Object {
/>
</div>
</div>
- <withAppStateContext(AlmSpecificForm)
+ <withAvailableFeaturesContext(AlmSpecificForm)
alm="github"
formData={
Object {
--- /dev/null
+/*
+ * 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.
+ */
+
+export enum Feature {
+ MonoRepositoryPullRequestDecoration = 'monorepo'
+}