'/projects',
'/project/information',
'/web_api_v2',
+ '/quality_gates',
];
const TEMP_PAGELIST_WITH_NEW_BACKGROUND_WHITE = ['/tutorials', '/projects/create'];
<div className="global-container">
<div
className={classNames('page-wrapper', {
- 'new-background': TEMP_PAGELIST_WITH_NEW_BACKGROUND.includes(location.pathname),
+ 'new-background': TEMP_PAGELIST_WITH_NEW_BACKGROUND.some((element) =>
+ location.pathname.startsWith(element)
+ ),
'white-background': TEMP_PAGELIST_WITH_NEW_BACKGROUND_WHITE.includes(
location.pathname,
),
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { withTheme } from '@emotion/react';
+import styled from '@emotion/styled';
+import {
+ LAYOUT_FOOTER_HEIGHT,
+ LAYOUT_GLOBAL_NAV_HEIGHT,
+ LargeCenteredLayout,
+ themeBorder,
+ themeColor,
+} from 'design-system';
import * as React from 'react';
import { Helmet } from 'react-helmet-async';
import { NavigateFunction, useNavigate, useParams } from 'react-router-dom';
import { fetchQualityGates } from '../../../api/quality-gates';
-import ScreenPositionHelper from '../../../components/common/ScreenPositionHelper';
import Suggestions from '../../../components/embed-docs-modal/Suggestions';
import '../../../components/search-navigator.css';
import Spinner from '../../../components/ui/Spinner';
const { canCreate, qualityGates } = this.state;
return (
- <>
+ <LargeCenteredLayout id="quality-gates-page">
<Helmet
defer={false}
titleTemplate={translateWithParameters(
translate('quality_gates.page'),
)}
/>
- <div className="layout-page" id="quality-gates-page">
+ <div className="sw-grid sw-gap-x-12 sw-gap-y-6 sw-grid-cols-12 sw-w-full">
<Suggestions suggestions="quality_gates" />
- <ScreenPositionHelper className="layout-page-side-outer">
- {({ top }) => (
- <nav className="layout-page-side" style={{ top }}>
- <div className="layout-page-side-inner">
- <div className="layout-page-filters">
- <ListHeader
- canCreate={canCreate}
- refreshQualityGates={this.fetchQualityGates}
- />
- <Spinner loading={this.state.loading}>
- <List qualityGates={qualityGates} currentQualityGate={name} />
- </Spinner>
- </div>
- </div>
- </nav>
- )}
- </ScreenPositionHelper>
+ <ContentWrapper className="sw-col-span-3 sw-px-4 sw-py-6">
+ <ListHeader canCreate={canCreate} refreshQualityGates={this.fetchQualityGates} />
+ <Spinner loading={this.state.loading}>
+ <List qualityGates={qualityGates} currentQualityGate={name} />
+ </Spinner>
+ </ContentWrapper>
{name !== undefined && (
- <Details
- qualityGateName={name}
- onSetDefault={this.handleSetDefault}
- qualityGates={this.state.qualityGates}
- refreshQualityGates={this.fetchQualityGates}
- />
+ <ContentWrapper className="sw-col-span-9 sw-overflow-y-auto">
+ <Details
+ qualityGateName={name}
+ onSetDefault={this.handleSetDefault}
+ qualityGates={this.state.qualityGates}
+ refreshQualityGates={this.fetchQualityGates}
+ />
+ </ContentWrapper>
)}
</div>
- </>
+ </LargeCenteredLayout>
);
}
}
+function ContentWrapper({ className, children }: React.PropsWithChildren<{ className: string }>) {
+ return (
+ <StyledContentWrapper
+ className={className}
+ style={{
+ height: `calc(100vh - ${LAYOUT_GLOBAL_NAV_HEIGHT + LAYOUT_FOOTER_HEIGHT}px)`,
+ }}
+ >
+ {children}
+ </StyledContentWrapper>
+ );
+}
+
export default function AppWrapper() {
const params = useParams();
const navigate = useNavigate();
return <App name={params['name']} navigate={navigate} />;
}
+
+const StyledContentWrapper = withTheme(styled.div`
+ box-sizing: border-box;
+
+ background-color: ${themeColor('filterbar')};
+ border-right: ${themeBorder('default', 'filterbarBorder')};
+ overflow-x: hidden;
+`);
* 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, Modal } from 'design-system';
import * as React from 'react';
import { createQualityGate } from '../../../api/quality-gates';
-import ConfirmModal from '../../../components/controls/ConfirmModal';
import { Router, withRouter } from '../../../components/hoc/withRouter';
-import MandatoryFieldMarker from '../../../components/ui/MandatoryFieldMarker';
import MandatoryFieldsExplanation from '../../../components/ui/MandatoryFieldsExplanation';
import { translate } from '../../../helpers/l10n';
import { getQualityGateUrl } from '../../../helpers/urls';
this.setState({ name: event.currentTarget.value });
};
+ handleFormSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => {
+ event.preventDefault();
+ this.handleCreate();
+ };
+
handleCreate = async () => {
const { name } = this.state;
- if (name) {
+ if (name !== undefined) {
const qualityGate = await createQualityGate({ name });
-
await this.props.onCreate();
-
+ this.props.onClose();
this.props.router.push(getQualityGateUrl(qualityGate.name));
}
};
render() {
const { name } = this.state;
- return (
- <ConfirmModal
- confirmButtonText={translate('save')}
- confirmDisable={!name}
- header={translate('quality_gates.create')}
- onClose={this.props.onClose}
- onConfirm={this.handleCreate}
- size="small"
- >
+
+ const body = (
+ <form onSubmit={this.handleFormSubmit}>
<MandatoryFieldsExplanation className="modal-field" />
- <div className="modal-field">
- <label htmlFor="quality-gate-form-name">
- {translate('name')}
- <MandatoryFieldMarker />
- </label>
- <input
- autoFocus
+ <FormField
+ htmlFor="quality-gate-form-name"
+ label={translate('name')}
+ required
+ requiredAriaLabel={translate('field_required')}
+ >
+ <InputField
+ autoComplete="off"
id="quality-gate-form-name"
- maxLength={100}
+ maxLength={256}
+ name="key"
onChange={this.handleNameChange}
- required
- size={50}
type="text"
+ size="full"
value={name}
/>
- </div>
- </ConfirmModal>
+ </FormField>
+ </form>
+ );
+
+ return (
+ <Modal
+ onClose={this.props.onClose}
+ headerTitle={translate('quality_gates.create')}
+ isScrollable
+ body={body}
+ primaryButton={
+ <ButtonSecondary
+ disabled={name === null || name === ''}
+ form="create-application-form"
+ type="submit"
+ onClick={this.handleCreate}
+ >
+ {translate('quality_gate.create')}
+ </ButtonSecondary>
+ }
+ secondaryButtonLabel={translate('cancel')}
+ />
);
}
}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import {
+ Badge,
+ BareButton,
+ FlagWarningIcon,
+ SubnavigationGroup,
+ SubnavigationItem,
+} from 'design-system';
import * as React from 'react';
-import { NavLink } from 'react-router-dom';
+import { useNavigate } from 'react-router-dom';
import Tooltip from '../../../components/controls/Tooltip';
-import AlertWarnIcon from '../../../components/icons/AlertWarnIcon';
+
import { translate } from '../../../helpers/l10n';
import { getQualityGateUrl } from '../../../helpers/urls';
import { CaycStatus, QualityGate } from '../../../types/types';
}
export default function List({ qualityGates, currentQualityGate }: Props) {
+ const navigateTo = useNavigate();
+
+ function redirectQualityGate(qualityGateName: string) {
+ navigateTo(getQualityGateUrl(qualityGateName));
+ }
+
return (
- <div className="list-group">
- {qualityGates.map((qualityGate) => (
- <NavLink
- className="list-group-item display-flex-center"
- aria-current={currentQualityGate === qualityGate.name && 'page'}
- key={qualityGate.name}
- to={getQualityGateUrl(qualityGate.name)}
- >
- <span className="flex-1 text-ellipsis" title={qualityGate.name}>
- {qualityGate.name}
- </span>
- {qualityGate.isDefault && (
- <span className="badge little-spacer-left">{translate('default')}</span>
- )}
- {qualityGate.isBuiltIn && <BuiltInQualityGateBadge className="little-spacer-left" />}
+ <SubnavigationGroup>
+ {qualityGates.map((qualityGate) => {
+ const isDefaultTitle = qualityGate.isDefault ? ` ${translate('default')}` : '';
+ const isBuiltInTitle = qualityGate.isBuiltIn
+ ? ` ${translate('quality_gates.built_in')}`
+ : '';
+
+ return (
+ <SubnavigationItem
+ className="it__list-group-item"
+ active={currentQualityGate === qualityGate.name}
+ key={qualityGate.name}
+ onClick={() => {
+ redirectQualityGate(qualityGate.name);
+ }}
+ >
+ <div className="sw-flex sw-flex-col">
+ <BareButton
+ aria-current={currentQualityGate === qualityGate.name && 'page'}
+ title={`${qualityGate.name}${isDefaultTitle}${isBuiltInTitle}`}
+ className="sw-flex-1 sw-text-ellipsis sw-overflow-hidden sw-max-w-abs-250 sw-whitespace-nowrap"
+ >
+ {qualityGate.name}
+ </BareButton>
- {qualityGate.caycStatus === CaycStatus.NonCompliant &&
- qualityGate.actions?.manageConditions && (
- <Tooltip overlay={translate('quality_gates.cayc.tooltip.message')}>
- <AlertWarnIcon
- className="spacer-left"
- description={translate('quality_gates.cayc.tooltip.message')}
- />
- </Tooltip>
- )}
- </NavLink>
- ))}
- </div>
+ {(qualityGate.isDefault || qualityGate.isBuiltIn) && (
+ <div className="sw-mt-2">
+ {qualityGate.isDefault && <Badge>{translate('default')}</Badge>}
+ {qualityGate.isBuiltIn && <BuiltInQualityGateBadge />}
+ </div>
+ )}
+ </div>
+ {qualityGate.caycStatus === CaycStatus.NonCompliant &&
+ qualityGate.actions?.manageConditions && (
+ <Tooltip overlay={translate('quality_gates.cayc.tooltip.message')}>
+ <FlagWarningIcon description={translate('quality_gates.cayc.tooltip.message')} />
+ </Tooltip>
+ )}
+ </SubnavigationItem>
+ );
+ })}
+ </SubnavigationGroup>
);
}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { ButtonPrimary, HelperHintIcon, Note } from 'design-system';
import * as React from 'react';
import DocumentationTooltip from '../../../components/common/DocumentationTooltip';
-import { Button } from '../../../components/controls/buttons';
import ModalButton from '../../../components/controls/ModalButton';
import { translate } from '../../../helpers/l10n';
import CreateQualityGateForm from './CreateQualityGateForm';
refreshQualityGates: () => Promise<void>;
}
-export default function ListHeader({ canCreate, refreshQualityGates }: Props) {
+function CreateQualityGateModal({ refreshQualityGates }: Pick<Props, 'refreshQualityGates'>) {
return (
- <div className="page-header">
- {canCreate && (
- <div className="page-actions">
- <ModalButton
- modal={({ onClose }) => (
- <CreateQualityGateForm onClose={onClose} onCreate={refreshQualityGates} />
- )}
- >
- {({ onClick }) => (
- <Button data-test="quality-gates__add" onClick={onClick}>
- {translate('create')}
- </Button>
- )}
- </ModalButton>
- </div>
- )}
+ <div>
+ <ModalButton
+ modal={({ onClose }) => (
+ <CreateQualityGateForm onClose={onClose} onCreate={refreshQualityGates} />
+ )}
+ >
+ {({ onClick }) => (
+ <ButtonPrimary data-test="quality-gates__add" onClick={onClick}>
+ {translate('create')}
+ </ButtonPrimary>
+ )}
+ </ModalButton>
+ </div>
+ );
+}
- <div className="display-flex-center">
- <h1 className="page-title">{translate('quality_gates.page')}</h1>
+export default function ListHeader({ canCreate, refreshQualityGates }: Props) {
+ return (
+ <div className="sw-flex sw-justify-between sw-pb-4">
+ <div className="sw-flex sw-justify-between">
+ <Note as="h1" className="sw-flex sw-items-center sw-body-md-highlight">
+ {translate('quality_gates.page')}
+ </Note>
<DocumentationTooltip
className="spacer-left"
content={translate('quality_gates.help')}
label: translate('learn_more'),
},
]}
- />
+ >
+ <HelperHintIcon />
+ </DocumentationTooltip>
</div>
+ {canCreate && <CreateQualityGateModal refreshQualityGates={refreshQualityGates} />}
</div>
);
}
const defaultQualityGate = handler.getDefaultQualityGate();
expect(
- await screen.findByRole('link', {
+ await screen.findByRole('button', {
current: 'page',
name: `${defaultQualityGate.name} default`,
})
renderQualityGateApp();
expect(
- await screen.findByRole('link', {
+ await screen.findByRole('button', {
name: `${handler.getDefaultQualityGate().name} default`,
})
).toBeInTheDocument();
expect(
- screen.getByRole('link', {
+ screen.getByRole('button', {
name: `${handler.getBuiltInQualityGate().name} quality_gates.built_in`,
})
).toBeInTheDocument();
await user.click(createButton);
await act(async () => {
await user.click(screen.getByRole('textbox', { name: /name.*/ }));
- await user.keyboard('testone{Enter}');
+ await user.keyboard('testone');
+ await user.click(screen.getByRole('button', { name: 'quality_gate.create' }));
});
- expect(await screen.findByRole('link', { name: 'testone' })).toBeInTheDocument();
+ expect(await screen.findByRole('button', { name: 'testone' })).toBeInTheDocument();
// Using modal button
createButton = await screen.findByRole('button', { name: 'create' });
await user.click(createButton);
- const saveButton = screen.getByRole('button', { name: 'save' });
+ const saveButton = screen.getByRole('button', { name: 'quality_gate.create' });
expect(saveButton).toBeDisabled();
const nameInput = screen.getByRole('textbox', { name: /name.*/ });
await user.click(saveButton);
});
- const newQG = await screen.findByRole('link', { name: 'testtwo' });
+ const newQG = await screen.findByRole('button', { name: 'testtwo' });
expect(newQG).toBeInTheDocument();
await user.click(dialogDeleteButton);
await waitFor(() => {
- expect(screen.queryByRole('link', { name: 'testtwo' })).not.toBeInTheDocument();
+ expect(screen.queryByRole('button', { name: 'testtwo' })).not.toBeInTheDocument();
});
});
await user.click(nameInput);
await user.keyboard(' bis{Enter}');
});
- expect(await screen.findByRole('link', { name: /.* bis/ })).toBeInTheDocument();
+ expect(await screen.findByRole('button', { name: /.* bis/ })).toBeInTheDocument();
});
it('should not be able to copy a quality gate which is not CAYC compliant', async () => {
await user.click(nameInput);
await user.keyboard('{Control>}a{/Control}New Name{Enter}');
- expect(await screen.findByRole('link', { name: /New Name.*/ })).toBeInTheDocument();
+ expect(await screen.findByRole('button', { name: /New Name.*/ })).toBeInTheDocument();
});
it('should not be able to set as default a quality gate which is not CAYC compliant', async () => {
handler.setIsAdmin(true);
renderQualityGateApp();
- const notDefaultQualityGate = await screen.findByRole('link', { name: /Sonar way/ });
+ const notDefaultQualityGate = await screen.findByRole('button', { name: /Sonar way/ });
await user.click(notDefaultQualityGate);
const setAsDefaultButton = screen.getByRole('button', { name: 'set_as_default' });
await user.click(setAsDefaultButton);
- expect(screen.getByRole('link', { name: /Sonar way/ })).toHaveTextContent('default');
+ expect(screen.getByRole('button', { name: /Sonar way default/ })).toBeInTheDocument();
});
it('should be able to add a condition', async () => {
await user.click(
// make it a regexp to ignore badges:
- await screen.findByRole('link', { name: new RegExp(handler.getCorruptedQualityGateName()) })
+ await screen.findByRole('button', { name: new RegExp(handler.getCorruptedQualityGateName()) })
);
expect(await screen.findByText('quality_gates.duplicated_conditions')).toBeInTheDocument();
const user = userEvent.setup();
renderQualityGateApp();
- const nonCompliantQualityGate = await screen.findByRole('link', { name: 'Non Cayc QG' });
+ const nonCompliantQualityGate = await screen.findByRole('button', { name: 'Non Cayc QG' });
await user.click(nonCompliantQualityGate);
handler.setIsAdmin(true);
renderQualityGateApp();
- const nonCompliantQualityGate = await screen.findByRole('link', { name: /Non Cayc QG/ });
+ const nonCompliantQualityGate = await screen.findByRole('button', { name: /Non Cayc QG/ });
await user.click(nonCompliantQualityGate);
// await just to make sure we've loaded the page
expect(
- await screen.findByRole('link', {
+ await screen.findByRole('button', {
name: `${handler.getDefaultQualityGate().name} default`,
})
).toBeInTheDocument();
#
#------------------------------------------------------------------------------
+quality_gate.create=Create
quality_gates.create=Create Quality Gate
quality_gates.rename=Rename Quality Gate
quality_gates.delete=Delete Quality Gate