--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 { SortAscIcon } from '@primer/octicons-react';
+import { OcticonHoc } from './Icon';
+
+export const SortAscendIcon = OcticonHoc(SortAscIcon, 'SortAscendIcon');
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 { SortDescIcon } from '@primer/octicons-react';
+import { OcticonHoc } from './Icon';
+
+export const SortDescendIcon = OcticonHoc(SortDescIcon, 'SortDescendIcon');
export { SeverityInfoIcon } from './SeverityInfoIcon';
export { SeverityMajorIcon } from './SeverityMajorIcon';
export { SeverityMinorIcon } from './SeverityMinorIcon';
+export { SortAscendIcon } from './SortAscendIcon';
+export { SortDescendIcon } from './SortDescendIcon';
export { StarIcon } from './StarIcon';
export { StatusConfirmedIcon } from './StatusConfirmedIcon';
export { StatusOpenIcon } from './StatusOpenIcon';
id?: string;
label: string | ReactNode;
required?: boolean;
+ requiredAriaLabel?: string;
title?: string;
}
htmlFor,
title,
ariaLabel,
+ requiredAriaLabel,
}: Props) {
return (
<FieldWrapper className={className} id={id}>
<label aria-label={ariaLabel} className="sw-mb-2" htmlFor={htmlFor} title={title}>
<Highlight className="sw-flex sw-items-center sw-gap-2">
{label}
- {required && <RequiredIcon className="sw--ml-1" />}
+ {required && (
+ <RequiredIcon aria-label={requiredAriaLabel ?? 'required'} className="sw--ml-1" />
+ )}
{help}
</Highlight>
</label>
<ReactModal
aria={{ labelledby: '#modal_header_title' }}
- className={classNames('design-system-modal-contents', { large: isLarge })}
+ className={classNames('design-system-modal-contents modal', { large: isLarge })}
isOpen={isOpen}
onRequestClose={onClose}
overlayClassName="design-system-modal-overlay"
'/project/activity',
'/code',
'/project/extension/securityreport/securityreport',
+ '/projects',
];
export default function GlobalContainer() {
left: 50%;
width: 350px;
margin-left: -175px;
+ z-index: 8600;
`;
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import {
+ ButtonSecondary,
+ FormField,
+ InputField,
+ InputTextArea,
+ Modal,
+ RadioButton,
+} from 'design-system';
import * as React from 'react';
import { createApplication } from '../../../api/application';
-import Radio from '../../../components/controls/Radio';
-import SimpleModal from '../../../components/controls/SimpleModal';
-import { ResetButtonLink, SubmitButton } from '../../../components/controls/buttons';
-import DeferredSpinner from '../../../components/ui/DeferredSpinner';
-import MandatoryFieldMarker from '../../../components/ui/MandatoryFieldMarker';
import MandatoryFieldsExplanation from '../../../components/ui/MandatoryFieldsExplanation';
import { translate } from '../../../helpers/l10n';
import { ComponentQualifier, Visibility } from '../../../types/component';
key: string;
name: string;
visibility: Visibility;
+ submitting: boolean;
}
export default class CreateApplicationForm extends React.PureComponent<Props, State> {
key: '',
name: '',
visibility: Visibility.Public,
+ submitting: false,
};
}
handleFormSubmit = () => {
const { name, description, key, visibility } = this.state;
- return createApplication(name, description, key.length > 0 ? key : undefined, visibility).then(
- ({ application }) => {
+ this.setState({ submitting: true });
+ return createApplication(name, description, key.length > 0 ? key : undefined, visibility)
+ .then(({ application }) => {
if (this.mounted) {
+ this.setState({ submitting: false });
this.props.onCreate({
key: application.key,
qualifier: ComponentQualifier.Application,
});
}
- }
+ })
+ .catch(() => {
+ this.setState({ submitting: false });
+ });
+ };
+
+ renderForm = () => {
+ const { name, description, key, visibility } = this.state;
+
+ return (
+ <form onSubmit={this.props.onClose} id="create-application-form">
+ <MandatoryFieldsExplanation className="modal-field" />
+
+ <FormField
+ htmlFor="view-edit-name"
+ label={translate('name')}
+ required
+ requiredAriaLabel={translate('field_required')}
+ >
+ <InputField
+ autoFocus
+ id="view-edit-name"
+ maxLength={100}
+ name="name"
+ onChange={this.handleNameChange}
+ type="text"
+ size="full"
+ value={name}
+ />
+ </FormField>
+ <FormField htmlFor="view-edit-description" label={translate('description')}>
+ <InputTextArea
+ id="view-edit-description"
+ name="description"
+ onChange={this.handleDescriptionChange}
+ size="full"
+ value={description}
+ />
+ </FormField>
+ <FormField
+ htmlFor="view-edit-key"
+ label={translate('key')}
+ description={translate('onboarding.create_application.key.description')}
+ >
+ <InputField
+ autoComplete="off"
+ id="view-edit-key"
+ maxLength={256}
+ name="key"
+ onChange={this.handleKeyChange}
+ type="text"
+ size="full"
+ value={key}
+ />
+ </FormField>
+
+ <FormField label={translate('visibility')}>
+ {[Visibility.Public, Visibility.Private].map((v) => (
+ <RadioButton
+ key={v}
+ checked={visibility === v}
+ value={v}
+ onCheck={this.handleVisibilityChange}
+ >
+ {translate('visibility', v)}
+ </RadioButton>
+ ))}
+ </FormField>
+ </form>
);
};
render() {
- const { name, description, key, visibility } = this.state;
+ const { submitting } = this.state;
const header = translate('qualifiers.create.APP');
const submitDisabled = !this.state.name.length;
return (
- <SimpleModal
- header={header}
+ <Modal
onClose={this.props.onClose}
- onSubmit={this.handleFormSubmit}
- size="small"
- >
- {({ onCloseClick, onFormSubmit, submitting }) => (
- <form onSubmit={onFormSubmit}>
- <div className="modal-head">
- <h2>{header}</h2>
- </div>
-
- <div className="modal-body">
- <MandatoryFieldsExplanation className="modal-field" />
-
- <div className="modal-field">
- <label htmlFor="view-edit-name">
- {translate('name')}
- <MandatoryFieldMarker />
- </label>
- <input
- autoFocus
- id="view-edit-name"
- maxLength={100}
- name="name"
- onChange={this.handleNameChange}
- size={50}
- type="text"
- value={name}
- />
- </div>
- <div className="modal-field">
- <label htmlFor="view-edit-description">{translate('description')}</label>
- <textarea
- id="view-edit-description"
- name="description"
- onChange={this.handleDescriptionChange}
- value={description}
- />
- </div>
- <div className="modal-field">
- <label htmlFor="view-edit-key">{translate('key')}</label>
- <input
- autoComplete="off"
- id="view-edit-key"
- maxLength={256}
- name="key"
- onChange={this.handleKeyChange}
- size={256}
- type="text"
- value={key}
- />
- <p className="modal-field-description">
- {translate('onboarding.create_application.key.description')}
- </p>
- </div>
-
- <div className="modal-field">
- <label>{translate('visibility')}</label>
- <div className="little-spacer-top">
- {[Visibility.Public, Visibility.Private].map((v) => (
- <Radio
- className={`big-spacer-right visibility-${v}`}
- key={v}
- checked={visibility === v}
- value={v}
- onCheck={this.handleVisibilityChange}
- >
- {translate('visibility', v)}
- </Radio>
- ))}
- </div>
- </div>
- </div>
-
- <div className="modal-foot">
- <DeferredSpinner className="spacer-right" loading={submitting} />
- <SubmitButton disabled={submitting || submitDisabled}>
- {translate('create')}
- </SubmitButton>
- <ResetButtonLink
- className="js-modal-close"
- id="view-edit-cancel"
- onClick={onCloseClick}
- >
- {translate('cancel')}
- </ResetButtonLink>
- </div>
- </form>
- )}
- </SimpleModal>
+ headerTitle={header}
+ isScrollable
+ loading={submitting}
+ body={this.renderForm()}
+ primaryButton={
+ <ButtonSecondary
+ disabled={submitting || submitDisabled}
+ form="create-application-form"
+ onClick={() => {
+ this.handleFormSubmit();
+ }}
+ type="submit"
+ >
+ {translate('create')}
+ </ButtonSecondary>
+ }
+ secondaryButtonLabel={translate('cancel')}
+ />
);
}
}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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 { createApplication } from '../../../../api/application';
-import SimpleModal from '../../../../components/controls/SimpleModal';
-import { mockEvent, waitAndUpdate } from '../../../../helpers/testUtils';
-import { ComponentQualifier, Visibility } from '../../../../types/component';
-import CreateApplicationForm from '../CreateApplicationForm';
-
-jest.mock('../../../../api/application', () => ({
- createApplication: jest.fn().mockResolvedValue({ application: { key: 'foo' } }),
-}));
-
-beforeEach(jest.clearAllMocks);
-
-it('should render correctly', () => {
- const wrapper = shallowRender();
- expect(wrapper).toMatchSnapshot('default');
- expect(wrapper.find(SimpleModal).dive()).toMatchSnapshot('form');
-});
-
-it('should correctly create application on form submit', async () => {
- const onCreate = jest.fn();
- const wrapper = shallowRender({ onCreate });
- const instance = wrapper.instance();
-
- instance.handleDescriptionChange(mockEvent({ currentTarget: { value: 'description' } }));
- instance.handleKeyChange(mockEvent({ currentTarget: { value: 'key' } }));
- instance.handleNameChange(mockEvent({ currentTarget: { value: 'name' } }));
- instance.handleVisibilityChange(Visibility.Private);
-
- wrapper.find(SimpleModal).props().onSubmit();
- expect(createApplication).toHaveBeenCalledWith('name', 'description', 'key', Visibility.Private);
- await waitAndUpdate(wrapper);
-
- expect(onCreate).toHaveBeenCalledWith(
- expect.objectContaining({
- key: 'foo',
- qualifier: ComponentQualifier.Application,
- })
- );
-
- // Can call the WS without any key.
- instance.handleKeyChange(mockEvent({ currentTarget: { value: '' } }));
- instance.handleFormSubmit();
- expect(createApplication).toHaveBeenCalledWith(
- 'name',
- 'description',
- undefined,
- Visibility.Private
- );
-});
-
-function shallowRender(props?: Partial<CreateApplicationForm['props']>) {
- return shallow<CreateApplicationForm>(
- <CreateApplicationForm onClose={jest.fn()} onCreate={jest.fn()} {...props} />
- );
-}
+++ /dev/null
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly: default 1`] = `
-<SimpleModal
- header="qualifiers.create.APP"
- onClose={[MockFunction]}
- onSubmit={[Function]}
- size="small"
->
- <Component />
-</SimpleModal>
-`;
-
-exports[`should render correctly: form 1`] = `
-<Modal
- contentLabel="qualifiers.create.APP"
- onRequestClose={[MockFunction]}
- size="small"
->
- <form
- onSubmit={[Function]}
- >
- <div
- className="modal-head"
- >
- <h2>
- qualifiers.create.APP
- </h2>
- </div>
- <div
- className="modal-body"
- >
- <MandatoryFieldsExplanation
- className="modal-field"
- />
- <div
- className="modal-field"
- >
- <label
- htmlFor="view-edit-name"
- >
- name
- <MandatoryFieldMarker />
- </label>
- <input
- autoFocus={true}
- id="view-edit-name"
- maxLength={100}
- name="name"
- onChange={[Function]}
- size={50}
- type="text"
- value=""
- />
- </div>
- <div
- className="modal-field"
- >
- <label
- htmlFor="view-edit-description"
- >
- description
- </label>
- <textarea
- id="view-edit-description"
- name="description"
- onChange={[Function]}
- value=""
- />
- </div>
- <div
- className="modal-field"
- >
- <label
- htmlFor="view-edit-key"
- >
- key
- </label>
- <input
- autoComplete="off"
- id="view-edit-key"
- maxLength={256}
- name="key"
- onChange={[Function]}
- size={256}
- type="text"
- value=""
- />
- <p
- className="modal-field-description"
- >
- onboarding.create_application.key.description
- </p>
- </div>
- <div
- className="modal-field"
- >
- <label>
- visibility
- </label>
- <div
- className="little-spacer-top"
- >
- <Radio
- checked={true}
- className="big-spacer-right visibility-public"
- key="public"
- onCheck={[Function]}
- value="public"
- >
- visibility.public
- </Radio>
- <Radio
- checked={false}
- className="big-spacer-right visibility-private"
- key="private"
- onCheck={[Function]}
- value="private"
- >
- visibility.private
- </Radio>
- </div>
- </div>
- </div>
- <div
- className="modal-foot"
- >
- <DeferredSpinner
- className="spacer-right"
- loading={false}
- />
- <SubmitButton
- disabled={true}
- >
- create
- </SubmitButton>
- <ResetButtonLink
- className="js-modal-close"
- id="view-edit-cancel"
- onClick={[Function]}
- >
- cancel
- </ResetButtonLink>
- </div>
- </form>
-</Modal>
-`;
import { ComponentQualifier } from '../../../types/component';
import { RawQuery } from '../../../types/types';
import { CurrentUser, isLoggedIn } from '../../../types/users';
-import { hasFilterParams, parseUrlQuery, Query } from '../query';
+import { Query, hasFilterParams, parseUrlQuery } from '../query';
import '../styles.css';
import { Facets, Project } from '../types';
-import { fetchProjects, parseSorting, SORTING_SWITCH } from '../utils';
+import { SORTING_SWITCH, fetchProjects, parseSorting } from '../utils';
import PageHeader from './PageHeader';
import PageSidebar from './PageSidebar';
import ProjectsList from './ProjectsList';
);
renderHeader = () => (
- <div className="layout-page-header-panel layout-page-main-header">
- <div className="layout-page-header-panel-inner layout-page-main-header-inner">
- <div className="layout-page-main-inner">
- <PageHeader
- currentUser={this.props.currentUser}
- loading={this.state.loading}
- onPerspectiveChange={this.handlePerspectiveChange}
- onQueryChange={this.updateLocationQuery}
- onSortChange={this.handleSortChange}
- query={this.state.query}
- selectedSort={this.getSort()}
- total={this.state.total}
- view={this.getView()}
- />
- </div>
- </div>
+ <div style={{ height: '120px' }}>
+ <PageHeader
+ currentUser={this.props.currentUser}
+ onPerspectiveChange={this.handlePerspectiveChange}
+ onQueryChange={this.updateLocationQuery}
+ onSortChange={this.handleSortChange}
+ query={this.state.query}
+ selectedSort={this.getSort()}
+ total={this.state.total}
+ view={this.getView()}
+ />
</div>
);
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { ButtonSecondary } from 'design-system';
import * as React from 'react';
import { getComponentNavigation } from '../../../api/navigation';
import withAppStateContext from '../../../app/components/app-state/withAppStateContext';
import withCurrentUserContext from '../../../app/components/current-user/withCurrentUserContext';
import CreateApplicationForm from '../../../app/components/extensions/CreateApplicationForm';
-import { Button } from '../../../components/controls/buttons';
import { Router, withRouter } from '../../../components/hoc/withRouter';
import { translate } from '../../../helpers/l10n';
import { getComponentAdminUrl, getComponentOverviewUrl } from '../../../helpers/urls';
};
return (
- <div className={className}>
- <Button className="button-primary" onClick={() => setShowForm(true)}>
+ <>
+ <ButtonSecondary onClick={() => setShowForm(true)} className={className}>
{translate('projects.create_application')}
- </Button>
+ </ButtonSecondary>
{showForm && (
<CreateApplicationForm
onCreate={handleComponentCreate}
/>
)}
- </div>
+ </>
);
}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import classNames from 'classnames';
+import styled from '@emotion/styled';
+import { InputSearch, LightLabel, LightPrimary } from 'design-system';
import * as React from 'react';
import HomePageSelect from '../../../components/controls/HomePageSelect';
-import { translate } from '../../../helpers/l10n';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
import { RawQuery } from '../../../types/types';
import { CurrentUser, isLoggedIn } from '../../../types/users';
-import SearchFilterContainer from '../filters/SearchFilterContainer';
import ApplicationCreation from './ApplicationCreation';
import PerspectiveSelect from './PerspectiveSelect';
import ProjectCreationMenu from './ProjectCreationMenu';
interface Props {
currentUser: CurrentUser;
- loading: boolean;
onPerspectiveChange: (x: { view: string }) => void;
onQueryChange: (change: RawQuery) => void;
onSortChange: (sort: string, desc: boolean) => void;
view: string;
}
+const MIN_SEARCH_QUERY_LENGTH = 2;
+
export default function PageHeader(props: Props) {
- const { loading, total, currentUser, view } = props;
+ const { total, currentUser, view } = props;
const defaultOption = isLoggedIn(currentUser) ? 'name' : 'analysis_date';
+ const handleSearch = (search?: string) => {
+ props.onQueryChange({ search });
+ };
+
return (
- <div className="page-header">
- <div className="display-flex-center projects-header-row display-flex-space-between">
- <SearchFilterContainer onQueryChange={props.onQueryChange} query={props.query} />
- <div className="display-flex-center">
- <ProjectCreationMenu className="little-spacer-right" />
- <ApplicationCreation className="little-spacer-right" />
- <HomePageSelect
- className="spacer-left little-spacer-right"
- currentPage={{ type: 'PROJECTS' }}
+ <StyledHeader className="it__page-header sw-flex sw-flex-col sw-z-project-list-header new-background sw-fixed sw-py-6 sw-pl-5">
+ <div className="sw-flex sw-justify-end sw-mb-4">
+ <ProjectCreationMenu />
+ <ApplicationCreation className="sw-ml-2" />
+ </div>
+ <div className="sw-flex sw-justify-between">
+ <div className="sw-flex">
+ <InputSearch
+ className="sw-w-abs-300 sw-mr-4 it__page-header-search"
+ minLength={MIN_SEARCH_QUERY_LENGTH}
+ onChange={handleSearch}
+ size="large"
+ placeholder={translate('projects.search')}
+ value={props.query.search ?? ''}
+ tooShortText={translateWithParameters('select2.tooShort', MIN_SEARCH_QUERY_LENGTH)}
+ searchInputAriaLabel={translate('search_verb')}
+ clearIconAriaLabel={translate('clear')}
+ />
+ <PerspectiveSelect onChange={props.onPerspectiveChange} view={view} />
+ <ProjectsSortingSelect
+ defaultOption={defaultOption}
+ onChange={props.onSortChange}
+ selectedSort={props.selectedSort}
+ view={view}
/>
</div>
- </div>
- <div className="spacer-top projects-header-row display-flex-space-between">
- <div
- className={classNames('display-flex-center', {
- 'is-loading': loading,
- })}
- >
+ <div className="sw-flex sw-items-center">
{total != null && (
- <span className="projects-total-label">
- <strong id="projects-total">{total}</strong> {translate('projects_')}
- </span>
+ <>
+ <LightPrimary id="projects-total" className="sw-font-semibold sw-mr-1">
+ {total}
+ </LightPrimary>
+ <LightLabel>{translate('projects_')}</LightLabel>
+ </>
)}
- </div>
-
- <div className="display-flex-center">
- <PerspectiveSelect
- className="projects-topbar-item"
- onChange={props.onPerspectiveChange}
- view={view}
- />
-
- <div className={classNames('projects-topbar-item')}>
- <ProjectsSortingSelect
- defaultOption={defaultOption}
- onChange={props.onSortChange}
- selectedSort={props.selectedSort}
- view={view}
- />
- </div>
+ <HomePageSelect currentPage={{ type: 'PROJECTS' }} />
</div>
</div>
- </div>
+ </StyledHeader>
);
}
+
+const StyledHeader = styled.div`
+ @media (max-width: 1320px) {
+ left: 301px;
+ }
+
+ right: 0;
+ left: calc(50vw - 369px);
+ top: 52px;
+ min-width: 740px;
+ max-width: 980px;
+`;
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { omit } from 'lodash';
+import { InputSelect, LabelValueSelectOption, StyledPageTitle } from 'design-system';
import * as React from 'react';
-import { components, OptionProps } from 'react-select';
-import Select from '../../../components/controls/Select';
-import ListIcon from '../../../components/icons/ListIcon';
import { translate } from '../../../helpers/l10n';
import { VIEWS } from '../utils';
interface Props {
- className?: string;
onChange: (x: { view: string }) => void;
view: string;
}
this.props.onChange({ view: option.value });
};
- perspectiveOptionRender = (props: OptionProps<PerspectiveOption, false>) => {
- const { data, className } = props;
- return (
- <components.Option
- {...omit(props, ['children', 'className'])}
- className={`it__projects-perspective-option-${data.value} ${className}`}
- >
- <ListIcon className="little-spacer-right" />
- {props.children}
- </components.Option>
- );
- };
-
render() {
const { view } = this.props;
const options: PerspectiveOption[] = [
})),
];
return (
- <div className={this.props.className}>
- <label id="aria-projects-perspective">{translate('projects.perspective')}:</label>
- <Select
+ <div className="sw-flex sw-items-center">
+ <StyledPageTitle
+ id="aria-projects-perspective"
+ as="label"
+ className="sw-body-sm-highlight sw-mr-2"
+ >
+ {translate('projects.perspective')}
+ </StyledPageTitle>
+ <InputSelect
aria-labelledby="aria-projects-perspective"
- className="little-spacer-left input-medium it__projects-perspective-select"
- isClearable={false}
- onChange={this.handleChange}
- components={{
- Option: this.perspectiveOptionRender,
- }}
+ className="sw-mr-4 sw-body-sm"
+ onChange={(data: LabelValueSelectOption<string>) => this.handleChange(data)}
options={options}
- isSearchable={false}
+ placeholder={translate('project_activity.filter_events')}
+ size="small"
value={options.find((option) => option.value === view)}
/>
</div>
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import {
+ ButtonSecondary,
+ ChevronDownIcon,
+ Dropdown,
+ ItemDivider,
+ ItemLink,
+ PopupPlacement,
+ PopupZLevel,
+} from 'design-system';
import * as React from 'react';
import { getAlmSettings } from '../../../api/alm-settings';
import withCurrentUserContext from '../../../app/components/current-user/withCurrentUserContext';
-import Link from '../../../components/common/Link';
-import { Button } from '../../../components/controls/buttons';
-import Dropdown from '../../../components/controls/Dropdown';
-import DropdownIcon from '../../../components/icons/DropdownIcon';
-import EllipsisIcon from '../../../components/icons/EllipsisIcon';
import { IMPORT_COMPATIBLE_ALMS } from '../../../helpers/constants';
import { translate } from '../../../helpers/l10n';
import { hasGlobalPermission } from '../../../helpers/users';
import ProjectCreationMenuItem from './ProjectCreationMenuItem';
interface Props {
- className?: string;
currentUser: LoggedInUser;
}
};
render() {
- const { className, currentUser } = this.props;
+ const { currentUser } = this.props;
const { boundAlms } = this.state;
const canCreateProject = hasGlobalPermission(currentUser, Permissions.ProjectCreation);
return (
<Dropdown
- className={className}
- onOpen={this.fetchAlmBindings}
+ id="project-creation-menu"
+ size="auto"
+ placement={PopupPlacement.BottomRight}
+ zLevel={PopupZLevel.Global}
overlay={
- <ul className="menu">
+ <>
{[...boundAlms, 'manual'].map((alm) => (
- <li className="little-spacer-bottom" key={alm}>
- <ProjectCreationMenuItem alm={alm} />
- </li>
+ <ProjectCreationMenuItem alm={alm} key={alm} />
))}
+ <ItemDivider />
{boundAlms.length < IMPORT_COMPATIBLE_ALMS.length && (
- <li className="bordered-top little-padded-top">
- <Link className="display-flex-center" to={{ pathname: '/projects/create' }}>
- <EllipsisIcon className="spacer-right" size={16} />
- {translate('more')}
- </Link>
- </li>
+ <ItemLink to={{ pathname: '/projects/create' }}>{translate('more')}</ItemLink>
)}
- </ul>
+ </>
}
>
- <Button className="button-primary">
+ <ButtonSecondary>
{translate('projects.add')}
- <DropdownIcon className="spacer-left " />
- </Button>
+ <ChevronDownIcon className="sw-ml-1" />
+ </ButtonSecondary>
</Dropdown>
);
}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { ItemLink } from 'design-system';
import * as React from 'react';
-import Link from '../../../components/common/Link';
-import ChevronsIcon from '../../../components/icons/ChevronsIcon';
import { translate } from '../../../helpers/l10n';
import { getBaseUrl } from '../../../helpers/system';
import { queryToSearch } from '../../../helpers/urls';
almIcon = 'bitbucket';
}
return (
- <Link
+ <ItemLink
className="display-flex-center"
to={{ pathname: '/projects/create', search: queryToSearch({ mode: alm }) }}
>
- {alm === 'manual' ? (
- <ChevronsIcon className="spacer-right" />
- ) : (
+ {alm !== 'manual' && (
<img
alt={alm}
className="spacer-right"
/>
)}
{translate('my_account.add_project', alm)}
- </Link>
+ </ItemLink>
);
}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import {
+ InputSelect,
+ InteractiveIcon,
+ LabelValueSelectOption,
+ SortAscendIcon,
+ SortDescendIcon,
+ StyledPageTitle,
+} from 'design-system';
import { omit, sortBy } from 'lodash';
import * as React from 'react';
-import { components, OptionProps } from 'react-select';
-import { colors } from '../../../app/theme';
-import { ButtonIcon } from '../../../components/controls/buttons';
-import Select from '../../../components/controls/Select';
+import { OptionProps, components } from 'react-select';
import Tooltip from '../../../components/controls/Tooltip';
-import SortAscIcon from '../../../components/icons/SortAscIcon';
-import SortDescIcon from '../../../components/icons/SortDescIcon';
import { translate } from '../../../helpers/l10n';
-import { parseSorting, SORTING_LEAK_METRICS, SORTING_METRICS } from '../utils';
+import { SORTING_LEAK_METRICS, SORTING_METRICS, parseSorting } from '../utils';
interface Props {
className?: string;
const { sortDesc, value } = this.getSorting();
return (
- <div className={this.props.className}>
- <label id="aria-projects-sort">{translate('projects.sort_by')}:</label>
- <Select
+ <div className="sw-flex sw-items-center">
+ <StyledPageTitle
+ id="aria-projects-sort"
+ as="label"
+ className="sw-body-sm-highlight sw-mr-2"
+ >
+ {translate('projects.sort_by')}
+ </StyledPageTitle>
+ <InputSelect
aria-labelledby="aria-projects-sort"
- className="little-spacer-left input-medium it__projects-sort-select"
- isClearable={false}
- onChange={this.handleSortChange}
+ className="sw-body-sm"
+ onChange={(data: LabelValueSelectOption<string>) => this.handleSortChange(data)}
+ options={this.getOptions()}
components={{
Option: this.projectsSortingSelectOption,
}}
- options={this.getOptions()}
- isSearchable={false}
+ placeholder={translate('project_activity.filter_events')}
+ size="small"
value={value}
/>
<Tooltip
sortDesc ? translate('projects.sort_descending') : translate('projects.sort_ascending')
}
>
- <ButtonIcon
+ <InteractiveIcon
+ Icon={sortDesc ? SortDescendIcon : SortAscendIcon}
aria-label={
sortDesc
? translate('projects.sort_descending')
: translate('projects.sort_ascending')
}
- className="js-projects-sorting-invert spacer-left"
- color={colors.gray52}
+ className="js-projects-invert-sort sw-ml-2"
onClick={this.handleDescToggle}
innerRef={(sortButtonRef) => {
this.sortOrderButtonNode = sortButtonRef;
}}
- >
- {sortDesc ? <SortDescIcon className="" /> : <SortAscIcon className="" />}
- </ButtonIcon>
+ />
</Tooltip>
</div>
);
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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, ShallowWrapper } from 'enzyme';
-import * as React from 'react';
-import { getComponentNavigation } from '../../../../api/navigation';
-import CreateApplicationForm from '../../../../app/components/extensions/CreateApplicationForm';
-import { Button } from '../../../../components/controls/buttons';
-import { mockAppState, mockLoggedInUser, mockRouter } from '../../../../helpers/testMocks';
-import { queryToSearch } from '../../../../helpers/urls';
-import { ComponentQualifier } from '../../../../types/component';
-import { ApplicationCreation, ApplicationCreationProps } from '../ApplicationCreation';
-
-jest.mock('../../../../api/navigation', () => ({
- getComponentNavigation: jest.fn().mockResolvedValue({}),
-}));
-
-it('should render correctly', () => {
- expect(shallowRender()).toMatchSnapshot('available');
- expect(
- shallowRender({ appState: mockAppState({ qualifiers: [ComponentQualifier.Portfolio] }) })
- ).toMatchSnapshot('unavailable');
- expect(
- shallowRender({ currentUser: mockLoggedInUser({ permissions: { global: ['otherrights'] } }) })
- ).toMatchSnapshot('not allowed');
-});
-
-it('should show form and callback when submitted - admin', async () => {
- (getComponentNavigation as jest.Mock).mockResolvedValueOnce({
- configuration: { showSettings: true },
- });
- const routerPush = jest.fn();
- const wrapper = shallowRender({ router: mockRouter({ push: routerPush }) });
-
- await openAndSubmitForm(wrapper);
-
- expect(routerPush).toHaveBeenCalledWith({
- pathname: '/project/admin/extension/developer-server/application-console',
- search: queryToSearch({
- id: 'new app',
- }),
- });
-});
-
-it('should show form and callback when submitted - user', async () => {
- (getComponentNavigation as jest.Mock).mockResolvedValueOnce({
- configuration: { showSettings: false },
- });
- const routerPush = jest.fn();
- const wrapper = shallowRender({ router: mockRouter({ push: routerPush }) });
-
- await openAndSubmitForm(wrapper);
-
- expect(routerPush).toHaveBeenCalledWith({
- pathname: '/dashboard',
- search: queryToSearch({
- id: 'new app',
- }),
- });
-});
-
-async function openAndSubmitForm(wrapper: ShallowWrapper) {
- wrapper.find(Button).simulate('click');
-
- const creationForm = wrapper.find(CreateApplicationForm);
- expect(creationForm.exists()).toBe(true);
-
- await creationForm
- .props()
- .onCreate({ key: 'new app', qualifier: ComponentQualifier.Application });
- expect(getComponentNavigation).toHaveBeenCalled();
- expect(wrapper.find(CreateApplicationForm).exists()).toBe(false);
-}
-
-function shallowRender(overrides: Partial<ApplicationCreationProps> = {}) {
- return shallow(
- <ApplicationCreation
- appState={mockAppState({ qualifiers: [ComponentQualifier.Application] })}
- currentUser={mockLoggedInUser({ permissions: { global: ['applicationcreator'] } })}
- router={mockRouter()}
- {...overrides}
- />
- );
-}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 userEvent from '@testing-library/user-event';
+import * as React from 'react';
+import { createApplication } from '../../../../api/application';
+import { getComponentNavigation } from '../../../../api/navigation';
+import { mockAppState, mockLoggedInUser, mockRouter } from '../../../../helpers/testMocks';
+import { renderComponent } from '../../../../helpers/testReactTestingUtils';
+import { byRole, byText } from '../../../../helpers/testSelector';
+import { queryToSearch } from '../../../../helpers/urls';
+import { ComponentQualifier, Visibility } from '../../../../types/component';
+import { FCProps } from '../../../../types/misc';
+import { LoggedInUser } from '../../../../types/users';
+import { ApplicationCreation } from '../ApplicationCreation';
+
+jest.mock('../../../../api/application', () => ({
+ createApplication: jest.fn().mockResolvedValue({}),
+}));
+jest.mock('../../../../api/navigation', () => ({
+ getComponentNavigation: jest.fn().mockResolvedValue({}),
+}));
+
+const ui = {
+ buttonAddApplication: byRole('button', { name: 'projects.create_application' }),
+ createApplicationHeader: byText('qualifiers.create.APP'),
+ mandatoryFieldWarning: byText('fields_marked_with_x_required'),
+ formNameField: byText('name'),
+ formKeyField: byText('key'),
+ formDescriptionField: byText('description'),
+ formVisibilityField: byText('visibility'),
+ formRadioButtonPrivate: byRole('radio', { name: 'visibility.private' }),
+ formCreateButton: byRole('button', { name: 'create' }),
+ formCancelButton: byRole('button', { name: 'cancel' }),
+};
+
+beforeEach(() => {
+ jest.clearAllMocks();
+});
+
+it('should be able to create application when user is logged in and has permission', async () => {
+ const user = userEvent.setup();
+ const routerPush = jest.fn();
+ const router = mockRouter({ push: routerPush });
+
+ jest.mocked(getComponentNavigation).mockResolvedValueOnce({
+ configuration: { showSettings: true },
+ name: 'name',
+ breadcrumbs: [{ key: 'b-key', qualifier: 'qual', name: 'b-name' }],
+ key: 'key',
+ });
+ jest.mocked(createApplication).mockResolvedValueOnce({
+ application: {
+ key: 'app',
+ name: 'app',
+ description: 'app',
+ visibility: Visibility.Public,
+ },
+ });
+
+ renderApplicationCreation(
+ { router },
+ mockLoggedInUser({ permissions: { global: ['admin', 'applicationcreator'] } })
+ );
+
+ await user.click(ui.buttonAddApplication.get());
+ expect(ui.createApplicationHeader.get()).toBeInTheDocument();
+ expect(ui.mandatoryFieldWarning.get()).toBeInTheDocument();
+ expect(ui.formNameField.get()).toBeInTheDocument();
+ expect(ui.formDescriptionField.get()).toBeInTheDocument();
+ expect(ui.formKeyField.get()).toBeInTheDocument();
+ expect(ui.formVisibilityField.get()).toBeInTheDocument();
+ expect(ui.formCreateButton.get()).toBeInTheDocument();
+ expect(ui.formCancelButton.get()).toBeInTheDocument();
+ await user.click(ui.formCancelButton.get());
+ expect(ui.createApplicationHeader.query()).not.toBeInTheDocument();
+
+ await user.click(ui.buttonAddApplication.get());
+ await user.click(ui.formNameField.get());
+ await user.keyboard('app');
+ await user.click(ui.formDescriptionField.get());
+ await user.keyboard('app description');
+ await user.click(ui.formKeyField.get());
+ await user.keyboard('app-key');
+ await user.click(ui.formRadioButtonPrivate.get());
+ await user.click(ui.formCreateButton.get());
+ expect(createApplication).toHaveBeenCalledWith(
+ 'app',
+ 'app description',
+ 'app-key',
+ Visibility.Private
+ );
+ expect(routerPush).toHaveBeenCalledWith({
+ pathname: '/project/admin/extension/developer-server/application-console',
+ search: queryToSearch({
+ id: 'app',
+ }),
+ });
+});
+
+function renderApplicationCreation(
+ props: Partial<FCProps<typeof ApplicationCreation>> = {},
+ currentUser: LoggedInUser = mockLoggedInUser()
+) {
+ return renderComponent(
+ <ApplicationCreation
+ currentUser={currentUser}
+ router={mockRouter()}
+ appState={mockAppState({ qualifiers: [ComponentQualifier.Application] })}
+ {...props}
+ />
+ );
+}
* 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 userEvent from '@testing-library/user-event';
import * as React from 'react';
+import { getAlmSettings } from '../../../../api/alm-settings';
+import CurrentUserContextProvider from '../../../../app/components/current-user/CurrentUserContextProvider';
+import { mockAppState, mockCurrentUser, mockLoggedInUser } from '../../../../helpers/testMocks';
+import { renderComponent } from '../../../../helpers/testReactTestingUtils';
+import { byLabelText, byRole, byText } from '../../../../helpers/testSelector';
+import { AlmKeys } from '../../../../types/alm-settings';
+import { ComponentQualifier } from '../../../../types/component';
+import { FCProps } from '../../../../types/misc';
+import { CurrentUser } from '../../../../types/users';
import PageHeader from '../PageHeader';
-it('should render correctly', () => {
- expect(shallowRender()).toMatchSnapshot();
+jest.mock('../../../../api/alm-settings', () => ({
+ getAlmSettings: jest.fn().mockResolvedValue([]),
+}));
+jest.mock('../../../../api/components', () => ({
+ searchProjects: jest.fn().mockResolvedValue({}),
+}));
+
+const ui = {
+ buttonAddProject: byRole('button', { name: 'projects.add' }),
+ buttonAddApplication: byRole('button', { name: 'projects.create_application' }),
+ searchBar: byLabelText('search_verb'),
+ selectPerspective: byLabelText('projects.perspective'),
+ selectSort: byLabelText('projects.sort_by'),
+ buttonSortProject: byLabelText('projects.sort_ascending'),
+ projectNumber: byText(12),
+ projectsText: byText('projects_'),
+ buttonHomePage: byLabelText('homepage.check'),
+ selectOptionAzure: byText('my_account.add_project.azure'),
+ selectOptionAzureImage: byRole('img', { name: 'azure' }),
+ selectOptionGitlab: byText('my_account.add_project.gitlab'),
+ selectOptionGitlabImage: byRole('img', { name: 'gitlab' }),
+ selectOptionBitbucket: byText('my_account.add_project.bitbucket'),
+ selectOptionBitbucketCloud: byText('my_account.add_project.bitbucketcloud'),
+ selectOptionManual: byText('my_account.add_project.manual'),
+ selectOptionMore: byText('more'),
+ selectOptionNewCode: byText('projects.view.new_code'),
+ selectOptionAnalysisDate: byText('projects.sorting.analysis_date'),
+ mandatoryFieldWarning: byText('fields_marked_with_x_required'),
+};
+
+beforeEach(() => {
+ jest.clearAllMocks();
});
-it('should render correctly while loading', () => {
- expect(shallowRender({ loading: true, total: 2 })).toMatchSnapshot();
+it('should work correctly for logged in user with edit permission', async () => {
+ const user = userEvent.setup();
+ const onQueryChangeMock = jest.fn();
+ const onPerspectiveChangeMock = jest.fn();
+ const onSortChangeMock = jest.fn();
+
+ jest.mocked(getAlmSettings).mockResolvedValueOnce([
+ { alm: AlmKeys.Azure, key: 'azure', url: 'https://azure.foo' },
+ { alm: AlmKeys.GitLab, key: 'gitlab', url: 'https://gitlab.foo' },
+ ]);
+
+ renderPageHeader(
+ {
+ total: 12,
+ onQueryChange: onQueryChangeMock,
+ onPerspectiveChange: onPerspectiveChangeMock,
+ onSortChange: onSortChangeMock,
+ },
+ mockLoggedInUser({ permissions: { global: ['admin', 'provisioning', 'applicationcreator'] } })
+ );
+ expect(getAlmSettings).toHaveBeenCalled();
+ expect(ui.buttonAddProject.get()).toBeInTheDocument();
+ expect(ui.buttonAddApplication.get()).toBeInTheDocument();
+ expect(ui.searchBar.get()).toBeInTheDocument();
+ expect(ui.selectPerspective.get()).toBeInTheDocument();
+ expect(ui.selectSort.get()).toBeInTheDocument();
+ expect(ui.buttonSortProject.get()).toBeInTheDocument();
+ expect(ui.projectNumber.get()).toBeInTheDocument();
+ expect(ui.projectsText.get()).toBeInTheDocument();
+ await expect(ui.buttonHomePage.get()).toHaveATooltipWithContent('homepage.check');
+
+ // project creation
+ await user.click(ui.buttonAddProject.get());
+ expect(ui.selectOptionAzure.get()).toHaveAttribute('href', '/projects/create?mode=azure');
+ expect(ui.selectOptionAzureImage.get()).toBeInTheDocument();
+ expect(ui.selectOptionGitlab.get()).toHaveAttribute('href', '/projects/create?mode=gitlab');
+ expect(ui.selectOptionGitlabImage.get()).toBeInTheDocument();
+ expect(ui.selectOptionManual.get()).toHaveAttribute('href', '/projects/create?mode=manual');
+ expect(ui.selectOptionMore.get()).toHaveAttribute('href', '/projects/create');
+
+ // search projects
+ await user.click(ui.searchBar.get());
+ await user.keyboard('2');
+ expect(onQueryChangeMock).toHaveBeenCalledWith({ search: 'test2' });
+
+ // perpective select
+ await user.click(ui.selectPerspective.get());
+ await user.click(ui.selectOptionNewCode.get());
+ expect(onPerspectiveChangeMock).toHaveBeenCalledWith({ view: 'leak' });
+
+ // sort ascending
+ await user.click(ui.buttonSortProject.get());
+ expect(onSortChangeMock).toHaveBeenCalledWith('size', true);
+
+ // sort select
+ await user.click(ui.selectSort.get());
+ await user.click(ui.selectOptionAnalysisDate.get());
+ expect(onSortChangeMock).toHaveBeenCalledWith('analysis_date', false);
});
-it('should not render projects total', () => {
- expect(shallowRender({ total: undefined }).find('#projects-total').exists()).toBe(false);
+it('should work correctly for logged in user without edit permission', async () => {
+ renderPageHeader({ total: 12 }, mockLoggedInUser());
+ expect(getAlmSettings).not.toHaveBeenCalled();
+ expect(ui.buttonAddProject.query()).not.toBeInTheDocument();
+ expect(ui.buttonAddApplication.query()).not.toBeInTheDocument();
+ expect(ui.searchBar.get()).toBeInTheDocument();
+ expect(ui.selectPerspective.get()).toBeInTheDocument();
+ expect(ui.selectSort.get()).toBeInTheDocument();
+ expect(ui.buttonSortProject.get()).toBeInTheDocument();
+ expect(ui.projectNumber.get()).toBeInTheDocument();
+ expect(ui.projectsText.get()).toBeInTheDocument();
+ await expect(ui.buttonHomePage.get()).toHaveATooltipWithContent('homepage.check');
});
-it('should render switch the default sorting option for anonymous users', () => {
- expect(
- shallowRender({
- currentUser: { isLoggedIn: true },
- open: true,
- }).find('ProjectsSortingSelect')
- ).toMatchSnapshot();
-
- expect(
- shallowRender({
- currentUser: { isLoggedIn: false },
- open: true,
- view: 'leak',
- }).find('ProjectsSortingSelect')
- ).toMatchSnapshot();
+it('should work correctly for anonymous user', () => {
+ renderPageHeader({ total: 12 }, mockCurrentUser());
+ expect(getAlmSettings).not.toHaveBeenCalled();
+ expect(ui.buttonAddProject.query()).not.toBeInTheDocument();
+ expect(ui.buttonAddApplication.query()).not.toBeInTheDocument();
+ expect(ui.searchBar.get()).toBeInTheDocument();
+ expect(ui.selectPerspective.get()).toBeInTheDocument();
+ expect(ui.selectSort.get()).toBeInTheDocument();
+ expect(ui.buttonSortProject.get()).toBeInTheDocument();
+ expect(ui.projectNumber.get()).toBeInTheDocument();
+ expect(ui.projectsText.get()).toBeInTheDocument();
+ expect(ui.buttonHomePage.query()).not.toBeInTheDocument();
+});
+
+it('should not render total if not defined', () => {
+ renderPageHeader({ total: undefined }, mockCurrentUser());
+ expect(ui.projectsText.query()).not.toBeInTheDocument();
+});
+
+it('should render alm correctly even with wrong data', async () => {
+ const user = userEvent.setup();
+
+ jest.mocked(getAlmSettings).mockResolvedValueOnce([
+ { alm: AlmKeys.Azure, key: 'azure1' },
+ { alm: AlmKeys.Azure, key: 'azure2' },
+ { alm: AlmKeys.BitbucketServer, url: 'b1', key: 'bbs' },
+ { alm: AlmKeys.BitbucketCloud, key: 'bbc' },
+ { alm: AlmKeys.GitLab, key: 'gitlab', url: 'https://gitlab.foo' },
+ ]);
+
+ renderPageHeader(
+ {},
+ mockLoggedInUser({ permissions: { global: ['admin', 'provisioning', 'applicationcreator'] } })
+ );
+
+ await user.click(ui.buttonAddProject.get());
+ expect(ui.selectOptionAzure.query()).not.toBeInTheDocument();
+ expect(ui.selectOptionGitlab.get()).toHaveAttribute('href', '/projects/create?mode=gitlab');
+ expect(ui.selectOptionBitbucket.get()).toHaveAttribute('href', '/projects/create?mode=bitbucket');
+ expect(ui.selectOptionBitbucketCloud.get()).toHaveAttribute(
+ 'href',
+ '/projects/create?mode=bitbucketcloud'
+ );
});
-function shallowRender(props?: {}) {
- return shallow(
- <PageHeader
- currentUser={{ isLoggedIn: false, dismissedNotices: {} }}
- loading={false}
- onPerspectiveChange={jest.fn()}
- onQueryChange={jest.fn()}
- onSortChange={jest.fn()}
- query={{ search: 'test' }}
- selectedSort="size"
- total={12}
- view="overall"
- {...props}
- />
+function renderPageHeader(
+ props: Partial<FCProps<typeof PageHeader>> = {},
+ currentUser: CurrentUser = mockLoggedInUser()
+) {
+ return renderComponent(
+ <CurrentUserContextProvider currentUser={currentUser}>
+ <PageHeader
+ currentUser={currentUser}
+ onPerspectiveChange={jest.fn()}
+ onQueryChange={jest.fn()}
+ onSortChange={jest.fn()}
+ query={{ search: 'test' }}
+ selectedSort="size"
+ view="overall"
+ {...props}
+ />
+ </CurrentUserContextProvider>,
+ '/',
+ { appState: mockAppState({ qualifiers: [ComponentQualifier.Application] }) }
);
}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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 ListIcon from '../../../../components/icons/ListIcon';
-import { mockReactSelectOptionProps } from '../../../../helpers/mocks/react-select';
-import PerspectiveSelect from '../PerspectiveSelect';
-
-it('should render correctly', () => {
- expect(shallow(<PerspectiveSelect onChange={jest.fn()} view="overall" />)).toMatchSnapshot();
-});
-
-it('should render option correctly', () => {
- const wrapper = shallowRender();
- const OptionRender = wrapper.instance().perspectiveOptionRender;
- const option = shallow(
- <OptionRender {...mockReactSelectOptionProps({ value: 'test', label: 'test' })} />
- );
- expect(option.find(ListIcon).type).toBeDefined();
-});
-
-it('should handle perspective change correctly', () => {
- const onChange = jest.fn();
- const instance = shallowRender({ onChange }).instance();
- instance.handleChange({ label: 'overall', value: 'overall' });
- instance.handleChange({ label: 'leak', value: 'leak' });
- expect(onChange.mock.calls).toMatchSnapshot();
-});
-
-function shallowRender(overrides: Partial<PerspectiveSelect['props']> = {}) {
- return shallow<PerspectiveSelect>(
- <PerspectiveSelect onChange={jest.fn()} view="overall" {...overrides} />
- );
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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 { getAlmSettings } from '../../../../api/alm-settings';
-import { mockLoggedInUser } from '../../../../helpers/testMocks';
-import { waitAndUpdate } from '../../../../helpers/testUtils';
-import { AlmKeys } from '../../../../types/alm-settings';
-import { ProjectCreationMenu } from '../ProjectCreationMenu';
-
-jest.mock('../../../../api/alm-settings', () => ({
- getAlmSettings: jest.fn().mockResolvedValue([]),
-}));
-
-beforeEach(() => {
- jest.clearAllMocks();
-});
-
-it('should render correctly', () => {
- expect(shallowRender()).toMatchSnapshot('default');
- expect(
- shallowRender({ currentUser: mockLoggedInUser({ permissions: { global: [] } }) })
- ).toMatchSnapshot('not allowed');
-});
-
-it('should fetch alm bindings on mount', async () => {
- const wrapper = shallowRender();
- await waitAndUpdate(wrapper);
- expect(getAlmSettings).toHaveBeenCalled();
-});
-
-it('should not fetch alm bindings if user cannot create projects', async () => {
- const wrapper = shallowRender({ currentUser: mockLoggedInUser({ permissions: { global: [] } }) });
- await waitAndUpdate(wrapper);
- expect(getAlmSettings).not.toHaveBeenCalled();
-});
-
-it('should filter alm bindings appropriately', async () => {
- (getAlmSettings as jest.Mock)
- .mockResolvedValueOnce([
- // Only faulty configs.
- { alm: AlmKeys.Azure }, // Missing some configuration; will be ignored.
- { alm: AlmKeys.GitLab }, // Missing some configuration; will be ignored.
- ])
- .mockResolvedValueOnce([
- // All correct configs.
- { alm: AlmKeys.Azure, url: 'http://ado.example.com' },
- { alm: AlmKeys.BitbucketServer, url: 'b1' },
- { alm: AlmKeys.GitHub },
- { alm: AlmKeys.GitLab, url: 'gitlab.com' },
- ])
- .mockResolvedValueOnce([
- // All correct configs.
- { alm: AlmKeys.Azure, url: 'http://ado.example.com' },
- { alm: AlmKeys.BitbucketCloud },
- { alm: AlmKeys.GitHub },
- { alm: AlmKeys.GitLab, url: 'gitlab.com' },
- ])
- .mockResolvedValueOnce([
- // Special case for BBS with BBC
- { alm: AlmKeys.Azure, url: 'http://ado.example.com' },
- { alm: AlmKeys.BitbucketServer, url: 'b1' },
- { alm: AlmKeys.BitbucketCloud },
- { alm: AlmKeys.GitHub },
- { alm: AlmKeys.GitLab, url: 'gitlab.com' },
- ])
- .mockResolvedValueOnce([
- // Only duplicate ALMs; should all be ignored.
- { alm: AlmKeys.Azure, url: 'http://ado.example.com' },
- { alm: AlmKeys.Azure, url: 'http://ado.example.com' },
- { alm: AlmKeys.BitbucketServer, url: 'b1' },
- { alm: AlmKeys.BitbucketServer, url: 'b1' },
- { alm: AlmKeys.GitHub },
- { alm: AlmKeys.GitHub },
- { alm: AlmKeys.GitLab, url: 'gitlab.com' },
- { alm: AlmKeys.GitLab, url: 'gitlab.com' },
- ]);
-
- let wrapper = shallowRender();
- await waitAndUpdate(wrapper);
- expect(wrapper.state().boundAlms).toEqual([]);
-
- wrapper = shallowRender();
- await waitAndUpdate(wrapper);
- expect(wrapper.state().boundAlms).toEqual([
- AlmKeys.Azure,
- AlmKeys.BitbucketServer,
- AlmKeys.GitHub,
- AlmKeys.GitLab,
- ]);
-
- wrapper = shallowRender();
- await waitAndUpdate(wrapper);
- expect(wrapper.state().boundAlms).toEqual([
- AlmKeys.Azure,
- AlmKeys.BitbucketCloud,
- AlmKeys.GitHub,
- AlmKeys.GitLab,
- ]);
-
- wrapper = shallowRender();
- await waitAndUpdate(wrapper);
- expect(wrapper.state().boundAlms).toEqual([
- AlmKeys.Azure,
- AlmKeys.BitbucketServer,
- AlmKeys.BitbucketCloud,
- AlmKeys.GitHub,
- AlmKeys.GitLab,
- ]);
-
- wrapper = shallowRender();
- await waitAndUpdate(wrapper);
- expect(wrapper.state().boundAlms).toEqual([
- AlmKeys.Azure,
- AlmKeys.BitbucketServer,
- AlmKeys.GitHub,
- AlmKeys.GitLab,
- ]);
-});
-
-function shallowRender(overrides: Partial<ProjectCreationMenu['props']> = {}) {
- return shallow<ProjectCreationMenu>(
- <ProjectCreationMenu
- currentUser={mockLoggedInUser({ permissions: { global: ['provisioning'] } })}
- {...overrides}
- />
- );
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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 { AlmKeys } from '../../../../types/alm-settings';
-import ProjectCreationMenuItem, { ProjectCreationMenuItemProps } from '../ProjectCreationMenuItem';
-
-it('should render correctly', () => {
- expect(shallowRender()).toMatchSnapshot('bitbucket');
- expect(shallowRender({ alm: 'manual' })).toMatchSnapshot('manual');
-});
-
-function shallowRender(overrides: Partial<ProjectCreationMenuItemProps> = {}) {
- return shallow(<ProjectCreationMenuItem alm={AlmKeys.BitbucketServer} {...overrides} />);
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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 { GroupBase } from 'react-select';
-import { mockReactSelectOptionProps } from '../../../../helpers/mocks/react-select';
-import { click } from '../../../../helpers/testUtils';
-import ProjectsSortingSelect, { Option } from '../ProjectsSortingSelect';
-
-it('should render correctly for overall view', () => {
- expect(shallowRender()).toMatchSnapshot();
-});
-
-it('should render correctly for leak view', () => {
- expect(
- shallowRender({ defaultOption: 'analysis_date', selectedSort: 'new_coverage', view: 'leak' })
- ).toMatchSnapshot();
-});
-
-it('should handle the descending sort direction', () => {
- expect(shallowRender({ selectedSort: '-vulnerability' })).toMatchSnapshot();
-});
-
-it('should render option correctly', () => {
- const wrapper = shallowRender();
- const SortOption = wrapper.instance().projectsSortingSelectOption;
- expect(
- shallow(
- <SortOption
- {...mockReactSelectOptionProps<Option, false, GroupBase<Option>>({
- label: 'foo',
- value: 'foo',
- short: 'fo',
- })}
- />
- )
- ).toMatchSnapshot();
- expect(
- shallow(
- <SortOption
- {...mockReactSelectOptionProps<Option, false, GroupBase<Option>>({
- label: 'foo',
- value: 'foo',
- })}
- />
- )
- ).toMatchSnapshot();
-});
-
-it('changes sorting', () => {
- const onChange = jest.fn();
- const instance = shallowRender({
- selectedSort: '-vulnerability',
- onChange,
- }).instance() as ProjectsSortingSelect;
- instance.handleSortChange({ label: 'size', value: 'size' });
- expect(onChange).toHaveBeenCalledWith('size', true);
-});
-
-it('reverses sorting', () => {
- const onChange = jest.fn();
- const wrapper = shallowRender({ selectedSort: '-size', onChange });
- click(wrapper.find('ButtonIcon'));
- expect(onChange).toHaveBeenCalledWith('size', false);
-
- const node = document.createElement('div');
- node.focus = jest.fn();
- wrapper.instance().sortOrderButtonNode = node;
-
- click(wrapper.find('ButtonIcon'));
-
- expect(node.focus).toHaveBeenCalled();
-});
-
-function shallowRender(overrides: Partial<ProjectsSortingSelect['props']> = {}) {
- return shallow<ProjectsSortingSelect>(
- <ProjectsSortingSelect
- defaultOption="name"
- onChange={jest.fn()}
- selectedSort="name"
- view="overall"
- {...overrides}
- />
- );
-}
list_of_projects
</h2>
<div
- className="layout-page-header-panel layout-page-main-header"
+ style={
+ {
+ "height": "120px",
+ }
+ }
>
- <div
- className="layout-page-header-panel-inner layout-page-main-header-inner"
- >
- <div
- className="layout-page-main-inner"
- >
- <PageHeader
- currentUser={
- {
- "dismissedNotices": {},
- "isLoggedIn": true,
- }
- }
- loading={false}
- onPerspectiveChange={[Function]}
- onQueryChange={[Function]}
- onSortChange={[Function]}
- query={
- {
- "coverage": undefined,
- "duplications": undefined,
- "gate": undefined,
- "languages": undefined,
- "maintainability": undefined,
- "new_coverage": undefined,
- "new_duplications": undefined,
- "new_lines": undefined,
- "new_maintainability": undefined,
- "new_reliability": undefined,
- "new_security": undefined,
- "new_security_review_rating": undefined,
- "qualifier": undefined,
- "reliability": undefined,
- "search": undefined,
- "security": undefined,
- "security_review_rating": undefined,
- "size": undefined,
- "sort": undefined,
- "tags": undefined,
- "view": undefined,
- }
- }
- selectedSort="name"
- total={0}
- view="overall"
- />
- </div>
- </div>
+ <PageHeader
+ currentUser={
+ {
+ "dismissedNotices": {},
+ "isLoggedIn": true,
+ }
+ }
+ onPerspectiveChange={[Function]}
+ onQueryChange={[Function]}
+ onSortChange={[Function]}
+ query={
+ {
+ "coverage": undefined,
+ "duplications": undefined,
+ "gate": undefined,
+ "languages": undefined,
+ "maintainability": undefined,
+ "new_coverage": undefined,
+ "new_duplications": undefined,
+ "new_lines": undefined,
+ "new_maintainability": undefined,
+ "new_reliability": undefined,
+ "new_security": undefined,
+ "new_security_review_rating": undefined,
+ "qualifier": undefined,
+ "reliability": undefined,
+ "search": undefined,
+ "security": undefined,
+ "security_review_rating": undefined,
+ "size": undefined,
+ "sort": undefined,
+ "tags": undefined,
+ "view": undefined,
+ }
+ }
+ selectedSort="name"
+ total={0}
+ view="overall"
+ />
</div>
<div
className="layout-page-main-inner"
+++ /dev/null
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly: available 1`] = `
-<div>
- <Button
- className="button-primary"
- onClick={[Function]}
- >
- projects.create_application
- </Button>
-</div>
-`;
-
-exports[`should render correctly: not allowed 1`] = `""`;
-
-exports[`should render correctly: unavailable 1`] = `""`;
+++ /dev/null
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly 1`] = `
-<div
- className="page-header"
->
- <div
- className="display-flex-center projects-header-row display-flex-space-between"
- >
- <SearchFilterContainer
- onQueryChange={[MockFunction]}
- query={
- {
- "search": "test",
- }
- }
- />
- <div
- className="display-flex-center"
- >
- <withCurrentUserContext(ProjectCreationMenu)
- className="little-spacer-right"
- />
- <withCurrentUserContext(withRouter(withAppStateContext(ApplicationCreation)))
- className="little-spacer-right"
- />
- <withCurrentUserContext(HomePageSelect)
- className="spacer-left little-spacer-right"
- currentPage={
- {
- "type": "PROJECTS",
- }
- }
- />
- </div>
- </div>
- <div
- className="spacer-top projects-header-row display-flex-space-between"
- >
- <div
- className="display-flex-center"
- >
- <span
- className="projects-total-label"
- >
- <strong
- id="projects-total"
- >
- 12
- </strong>
-
- projects_
- </span>
- </div>
- <div
- className="display-flex-center"
- >
- <PerspectiveSelect
- className="projects-topbar-item"
- onChange={[MockFunction]}
- view="overall"
- />
- <div
- className="projects-topbar-item"
- >
- <ProjectsSortingSelect
- defaultOption="analysis_date"
- onChange={[MockFunction]}
- selectedSort="size"
- view="overall"
- />
- </div>
- </div>
- </div>
-</div>
-`;
-
-exports[`should render correctly while loading 1`] = `
-<div
- className="page-header"
->
- <div
- className="display-flex-center projects-header-row display-flex-space-between"
- >
- <SearchFilterContainer
- onQueryChange={[MockFunction]}
- query={
- {
- "search": "test",
- }
- }
- />
- <div
- className="display-flex-center"
- >
- <withCurrentUserContext(ProjectCreationMenu)
- className="little-spacer-right"
- />
- <withCurrentUserContext(withRouter(withAppStateContext(ApplicationCreation)))
- className="little-spacer-right"
- />
- <withCurrentUserContext(HomePageSelect)
- className="spacer-left little-spacer-right"
- currentPage={
- {
- "type": "PROJECTS",
- }
- }
- />
- </div>
- </div>
- <div
- className="spacer-top projects-header-row display-flex-space-between"
- >
- <div
- className="display-flex-center is-loading"
- >
- <span
- className="projects-total-label"
- >
- <strong
- id="projects-total"
- >
- 2
- </strong>
-
- projects_
- </span>
- </div>
- <div
- className="display-flex-center"
- >
- <PerspectiveSelect
- className="projects-topbar-item"
- onChange={[MockFunction]}
- view="overall"
- />
- <div
- className="projects-topbar-item"
- >
- <ProjectsSortingSelect
- defaultOption="analysis_date"
- onChange={[MockFunction]}
- selectedSort="size"
- view="overall"
- />
- </div>
- </div>
- </div>
-</div>
-`;
-
-exports[`should render switch the default sorting option for anonymous users 1`] = `
-<ProjectsSortingSelect
- defaultOption="name"
- onChange={[MockFunction]}
- selectedSort="size"
- view="overall"
-/>
-`;
-
-exports[`should render switch the default sorting option for anonymous users 2`] = `
-<ProjectsSortingSelect
- defaultOption="analysis_date"
- onChange={[MockFunction]}
- selectedSort="size"
- view="leak"
-/>
-`;
+++ /dev/null
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should handle perspective change correctly 1`] = `
-[
- [
- {
- "view": "overall",
- },
- ],
- [
- {
- "view": "leak",
- },
- ],
-]
-`;
-
-exports[`should render correctly 1`] = `
-<div>
- <label
- id="aria-projects-perspective"
- >
- projects.perspective
- :
- </label>
- <Select
- aria-labelledby="aria-projects-perspective"
- className="little-spacer-left input-medium it__projects-perspective-select"
- components={
- {
- "Option": [Function],
- }
- }
- isClearable={false}
- isSearchable={false}
- onChange={[Function]}
- options={
- [
- {
- "label": "projects.view.overall",
- "value": "overall",
- },
- {
- "label": "projects.view.new_code",
- "value": "leak",
- },
- ]
- }
- value={
- {
- "label": "projects.view.overall",
- "value": "overall",
- }
- }
- />
-</div>
-`;
+++ /dev/null
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly: default 1`] = `
-<Dropdown
- onOpen={[Function]}
- overlay={
- <ul
- className="menu"
- >
- <li
- className="little-spacer-bottom"
- >
- <ProjectCreationMenuItem
- alm="manual"
- />
- </li>
- <li
- className="bordered-top little-padded-top"
- >
- <ForwardRef(Link)
- className="display-flex-center"
- to={
- {
- "pathname": "/projects/create",
- }
- }
- >
- <EllipsisIcon
- className="spacer-right"
- size={16}
- />
- more
- </ForwardRef(Link)>
- </li>
- </ul>
- }
->
- <Button
- className="button-primary"
- >
- projects.add
- <DropdownIcon
- className="spacer-left "
- />
- </Button>
-</Dropdown>
-`;
-
-exports[`should render correctly: not allowed 1`] = `""`;
+++ /dev/null
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly: bitbucket 1`] = `
-<ForwardRef(Link)
- className="display-flex-center"
- to={
- {
- "pathname": "/projects/create",
- "search": "?mode=bitbucket",
- }
- }
->
- <img
- alt="bitbucket"
- className="spacer-right"
- src="/images/alm/bitbucket.svg"
- width={16}
- />
- my_account.add_project.bitbucket
-</ForwardRef(Link)>
-`;
-
-exports[`should render correctly: manual 1`] = `
-<ForwardRef(Link)
- className="display-flex-center"
- to={
- {
- "pathname": "/projects/create",
- "search": "?mode=manual",
- }
- }
->
- <ChevronsIcon
- className="spacer-right"
- />
- my_account.add_project.manual
-</ForwardRef(Link)>
-`;
+++ /dev/null
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should handle the descending sort direction 1`] = `
-<div>
- <label
- id="aria-projects-sort"
- >
- projects.sort_by
- :
- </label>
- <Select
- aria-labelledby="aria-projects-sort"
- className="little-spacer-left input-medium it__projects-sort-select"
- components={
- {
- "Option": [Function],
- }
- }
- isClearable={false}
- isSearchable={false}
- onChange={[Function]}
- options={
- [
- {
- "className": undefined,
- "label": "projects.sorting.name",
- "value": "name",
- },
- {
- "className": undefined,
- "label": "projects.sorting.analysis_date",
- "value": "analysis_date",
- },
- {
- "className": undefined,
- "label": "projects.sorting.reliability",
- "value": "reliability",
- },
- {
- "className": undefined,
- "label": "projects.sorting.security",
- "value": "security",
- },
- {
- "className": undefined,
- "label": "projects.sorting.security_review",
- "value": "security_review",
- },
- {
- "className": undefined,
- "label": "projects.sorting.maintainability",
- "value": "maintainability",
- },
- {
- "className": undefined,
- "label": "projects.sorting.coverage",
- "value": "coverage",
- },
- {
- "className": undefined,
- "label": "projects.sorting.duplications",
- "value": "duplications",
- },
- {
- "className": undefined,
- "label": "projects.sorting.size",
- "value": "size",
- },
- ]
- }
- />
- <Tooltip
- mouseLeaveDelay={1}
- overlay="projects.sort_descending"
- >
- <ButtonIcon
- aria-label="projects.sort_descending"
- className="js-projects-sorting-invert spacer-left"
- color="#525252"
- innerRef={[Function]}
- onClick={[Function]}
- >
- <SortDescIcon />
- </ButtonIcon>
- </Tooltip>
-</div>
-`;
-
-exports[`should render correctly for leak view 1`] = `
-<div>
- <label
- id="aria-projects-sort"
- >
- projects.sort_by
- :
- </label>
- <Select
- aria-labelledby="aria-projects-sort"
- className="little-spacer-left input-medium it__projects-sort-select"
- components={
- {
- "Option": [Function],
- }
- }
- isClearable={false}
- isSearchable={false}
- onChange={[Function]}
- options={
- [
- {
- "className": undefined,
- "label": "projects.sorting.analysis_date",
- "value": "analysis_date",
- },
- {
- "className": undefined,
- "label": "projects.sorting.name",
- "value": "name",
- },
- {
- "className": "projects-leak-sorting-option",
- "label": "projects.sorting.new_reliability",
- "value": "new_reliability",
- },
- {
- "className": "projects-leak-sorting-option",
- "label": "projects.sorting.new_security",
- "value": "new_security",
- },
- {
- "className": "projects-leak-sorting-option",
- "label": "projects.sorting.new_security_review",
- "value": "new_security_review",
- },
- {
- "className": "projects-leak-sorting-option",
- "label": "projects.sorting.new_maintainability",
- "value": "new_maintainability",
- },
- {
- "className": "projects-leak-sorting-option",
- "label": "projects.sorting.new_coverage",
- "value": "new_coverage",
- },
- {
- "className": "projects-leak-sorting-option",
- "label": "projects.sorting.new_duplications",
- "value": "new_duplications",
- },
- {
- "className": "projects-leak-sorting-option",
- "label": "projects.sorting.new_lines",
- "value": "new_lines",
- },
- ]
- }
- value={
- {
- "className": "projects-leak-sorting-option",
- "label": "projects.sorting.new_coverage",
- "value": "new_coverage",
- }
- }
- />
- <Tooltip
- mouseLeaveDelay={1}
- overlay="projects.sort_ascending"
- >
- <ButtonIcon
- aria-label="projects.sort_ascending"
- className="js-projects-sorting-invert spacer-left"
- color="#525252"
- innerRef={[Function]}
- onClick={[Function]}
- >
- <SortAscIcon />
- </ButtonIcon>
- </Tooltip>
-</div>
-`;
-
-exports[`should render correctly for overall view 1`] = `
-<div>
- <label
- id="aria-projects-sort"
- >
- projects.sort_by
- :
- </label>
- <Select
- aria-labelledby="aria-projects-sort"
- className="little-spacer-left input-medium it__projects-sort-select"
- components={
- {
- "Option": [Function],
- }
- }
- isClearable={false}
- isSearchable={false}
- onChange={[Function]}
- options={
- [
- {
- "className": undefined,
- "label": "projects.sorting.name",
- "value": "name",
- },
- {
- "className": undefined,
- "label": "projects.sorting.analysis_date",
- "value": "analysis_date",
- },
- {
- "className": undefined,
- "label": "projects.sorting.reliability",
- "value": "reliability",
- },
- {
- "className": undefined,
- "label": "projects.sorting.security",
- "value": "security",
- },
- {
- "className": undefined,
- "label": "projects.sorting.security_review",
- "value": "security_review",
- },
- {
- "className": undefined,
- "label": "projects.sorting.maintainability",
- "value": "maintainability",
- },
- {
- "className": undefined,
- "label": "projects.sorting.coverage",
- "value": "coverage",
- },
- {
- "className": undefined,
- "label": "projects.sorting.duplications",
- "value": "duplications",
- },
- {
- "className": undefined,
- "label": "projects.sorting.size",
- "value": "size",
- },
- ]
- }
- value={
- {
- "className": undefined,
- "label": "projects.sorting.name",
- "value": "name",
- }
- }
- />
- <Tooltip
- mouseLeaveDelay={1}
- overlay="projects.sort_ascending"
- >
- <ButtonIcon
- aria-label="projects.sort_ascending"
- className="js-projects-sorting-invert spacer-left"
- color="#525252"
- innerRef={[Function]}
- onClick={[Function]}
- >
- <SortAscIcon />
- </ButtonIcon>
- </Tooltip>
-</div>
-`;
-
-exports[`should render option correctly 1`] = `
-<Option
- className="it__project-sort-option-foo undefined"
- data={
- {
- "label": "foo",
- "short": "fo",
- "value": "foo",
- }
- }
->
- fo
-</Option>
-`;
-
-exports[`should render option correctly 2`] = `
-<Option
- className="it__project-sort-option-foo undefined"
- data={
- {
- "label": "foo",
- "value": "foo",
- }
- }
-/>
-`;
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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 SearchBox from '../../../components/controls/SearchBox';
-import { translate } from '../../../helpers/l10n';
-
-interface Props {
- query: { search?: string };
- onQueryChange: (change: { search?: string }) => void;
-}
-
-export default class SearchFilterContainer extends React.PureComponent<Props> {
- handleSearch = (search?: string) => {
- this.props.onQueryChange({ search });
- };
-
- render() {
- return (
- <div className="projects-topbar-item projects-topbar-item-search">
- <SearchBox
- minLength={2}
- onChange={this.handleSearch}
- placeholder={translate('projects.search')}
- value={this.props.query.search ?? ''}
- />
- </div>
- );
- }
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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 SearchFilterContainer from '../SearchFilterContainer';
-
-it('searches', () => {
- const onQueryChange = jest.fn();
- const wrapper = shallow(<SearchFilterContainer onQueryChange={onQueryChange} query={{}} />);
- expect(wrapper).toMatchSnapshot();
- wrapper.find('SearchBox').prop<Function>('onChange')('foo');
- expect(onQueryChange).toHaveBeenCalledWith({ search: 'foo' });
-});
+++ /dev/null
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`searches 1`] = `
-<div
- className="projects-topbar-item projects-topbar-item-search"
->
- <SearchBox
- minLength={2}
- onChange={[Function]}
- placeholder="projects.search"
- value=""
- />
-</div>
-`;
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import classNames from 'classnames';
+import { LightLabel, RequiredIcon } from 'design-system';
import * as React from 'react';
import { FormattedMessage } from 'react-intl';
import { translate } from '../../helpers/l10n';
export default function MandatoryFieldsExplanation({ className }: MandatoryFieldsExplanationProps) {
return (
- <div aria-hidden className={classNames('text-muted', className)}>
+ <LightLabel aria-hidden className={className}>
<FormattedMessage
id="fields_marked_with_x_required"
defaultMessage={translate('fields_marked_with_x_required')}
- values={{ star: <em className="mandatory">*</em> }}
+ values={{ star: <RequiredIcon className="sw-m-0" /> }}
/>
- </div>
+ </LightLabel>
);
}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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 MandatoryFieldsExplanation, {
- MandatoryFieldsExplanationProps,
-} from '../MandatoryFieldsExplanation';
-
-it('should render correctly', () => {
- expect(shallowRender()).toMatchSnapshot('default');
- expect(shallowRender({ className: 'foo-bar' })).toMatchSnapshot('with className');
-});
-
-function shallowRender(props: Partial<MandatoryFieldsExplanationProps> = {}) {
- return shallow<MandatoryFieldsExplanationProps>(<MandatoryFieldsExplanation {...props} />);
-}
+++ /dev/null
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly: default 1`] = `
-<div
- aria-hidden={true}
- className="text-muted"
->
- <FormattedMessage
- defaultMessage="fields_marked_with_x_required"
- id="fields_marked_with_x_required"
- values={
- {
- "star": <em
- className="mandatory"
- >
- *
- </em>,
- }
- }
- />
-</div>
-`;
-
-exports[`should render correctly: with className 1`] = `
-<div
- aria-hidden={true}
- className="text-muted foo-bar"
->
- <FormattedMessage
- defaultMessage="fields_marked_with_x_required"
- id="fields_marked_with_x_required"
- values={
- {
- "star": <em
- className="mandatory"
- >
- *
- </em>,
- }
- }
- />
-</div>
-`;
},
zIndex: {
normal: '2',
+ 'project-list-header': '30',
filterbar: '50',
'content-popup': '52',
'filterbar-header': '55',