diff options
author | Jeremy Davis <jeremy.davis@sonarsource.com> | 2023-12-01 17:47:29 +0100 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2023-12-04 20:03:22 +0000 |
commit | cdda3dbcdd4e4b5f4a2e7471a37fa66877e26aad (patch) | |
tree | 9586bf172e5b2dc763fda6c5a90774790a98e95b | |
parent | e347b7fd97c898781bd3eefe606155c12d22abe9 (diff) | |
download | sonarqube-cdda3dbcdd4e4b5f4a2e7471a37fa66877e26aad.tar.gz sonarqube-cdda3dbcdd4e4b5f4a2e7471a37fa66877e26aad.zip |
SONAR-21069 Background tasks adopt the new UI
32 files changed, 646 insertions, 693 deletions
diff --git a/server/sonar-web/src/main/js/app/components/GlobalContainer.tsx b/server/sonar-web/src/main/js/app/components/GlobalContainer.tsx index 7fbe8586840..da3dd405fb4 100644 --- a/server/sonar-web/src/main/js/app/components/GlobalContainer.tsx +++ b/server/sonar-web/src/main/js/app/components/GlobalContainer.tsx @@ -70,6 +70,8 @@ const TEMP_PAGELIST_WITH_NEW_BACKGROUND_WHITE = [ '/project_roles', '/admin/permissions', '/admin/permission_templates', + '/project/background_tasks', + '/admin/background_tasks', ]; export default function GlobalContainer() { diff --git a/server/sonar-web/src/main/js/apps/background-tasks/__tests__/BackgroundTasks-it.tsx b/server/sonar-web/src/main/js/apps/background-tasks/__tests__/BackgroundTasks-it.tsx index cdc0ed40273..8844b0974b9 100644 --- a/server/sonar-web/src/main/js/apps/background-tasks/__tests__/BackgroundTasks-it.tsx +++ b/server/sonar-web/src/main/js/apps/background-tasks/__tests__/BackgroundTasks-it.tsx @@ -19,15 +19,18 @@ */ import { screen, waitFor, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; +import selectEvent from 'react-select-event'; import ComputeEngineServiceMock from '../../../api/mocks/ComputeEngineServiceMock'; import { parseDate } from '../../../helpers/dates'; import { mockAppState } from '../../../helpers/testMocks'; +import { RenderContext, renderAppWithAdminContext } from '../../../helpers/testReactTestingUtils'; import { - RenderContext, - dateInputEvent, - renderAppWithAdminContext, -} from '../../../helpers/testReactTestingUtils'; -import { byLabelText, byPlaceholderText, byRole, byText } from '../../../helpers/testSelector'; + byLabelText, + byPlaceholderText, + byRole, + byTestId, + byText, +} from '../../../helpers/testSelector'; import { EditionKey } from '../../../types/editions'; import { TaskStatuses, TaskTypes } from '../../../types/tasks'; import routes from '../routes'; @@ -47,7 +50,7 @@ describe('The Global background task page', () => { renderGlobalBackgroundTasksApp(); await ui.appLoaded(); - expect(ui.numberOfWorkers(2).get()).toBeInTheDocument(); + expect(ui.numberOfWorkers().get()).toHaveTextContent('2'); const editWorkersButton = screen.getByRole('button', { name: 'background_tasks.change_number_of_workers', @@ -72,7 +75,7 @@ describe('The Global background task page', () => { await user.click(within(modal).getByRole('button', { name: 'save' })); - expect(ui.numberOfWorkers(4).get()).toBeInTheDocument(); + expect(ui.numberOfWorkers().get()).toHaveTextContent('4'); }); it('should display the list of tasks', async () => { @@ -311,18 +314,22 @@ function getPageObject() { const selectors = { loading: byLabelText('loading'), pageHeading: byRole('heading', { name: 'background_tasks.page' }), - numberOfWorkers: (count: number) => - byText('background_tasks.number_of_workers').byText(`${count}`), - onlyLatestAnalysis: byRole('checkbox', { name: 'yes' }), + numberOfWorkers: () => byLabelText(`background_tasks.number_of_workers`), + onlyLatestAnalysis: byRole('switch', { + name: 'background_tasks.currents_filter.ALL', + }), search: byPlaceholderText('background_tasks.search_by_task_or_component'), - fromDateInput: byRole('textbox', { name: 'start_date' }), - toDateInput: byRole('textbox', { name: 'end_date' }), + fromDateInput: byLabelText('start_date'), + toDateInput: byLabelText('end_date'), resetFilters: byRole('button', { name: 'reset_verb' }), showMoreButton: byRole('button', { name: 'show_more' }), reloadButton: byRole('button', { name: 'reload' }), cancelAllButton: byRole('button', { description: 'background_tasks.cancel_all_tasks' }), cancelAllButtonConfirm: byText('background_tasks.cancel_all_tasks.submit'), row: byRole('row'), + startDateInput: byPlaceholderText('start_date'), + monthSelector: byTestId('month-select'), + yearSelector: byTestId('year-select'), }; const ui = { @@ -339,26 +346,64 @@ function getPageObject() { }, async changeTaskFilter(fieldLabel: string, value: string) { - await user.click(screen.getByLabelText(fieldLabel, { selector: 'input' })); - await user.click(screen.getByText(value)); + await selectEvent.select(screen.getByRole('combobox', { name: fieldLabel }), [value]); + expect(await screen.findByRole('button', { name: 'reload' })).toBeEnabled(); }, async setDateRange(from?: string, to?: string) { - const dateInput = dateInputEvent(user); if (from) { - await dateInput.pickDate(ui.fromDateInput.get(), parseDate(from)); + await this.selectDate(from, ui.fromDateInput.get()); } if (to) { - await dateInput.pickDate(ui.toDateInput.get(), parseDate(to)); + await this.selectDate(to, ui.toDateInput.get()); } + expect(await screen.findByRole('button', { name: 'reload' })).toBeEnabled(); + }, + + async selectDate(date: string, datePickerSelector: HTMLElement) { + const monthMap = [ + 'Jan', + 'Feb', + 'Mar', + 'Apr', + 'May', + 'Jun', + 'Jul', + 'Aug', + 'Sep', + 'Oct', + 'Nov', + 'Dec', + ]; + const parsedDate = parseDate(date); + await user.click(datePickerSelector); + const monthSelector = within(ui.monthSelector.get()).getByRole('combobox'); + + await user.click(monthSelector); + const selectedMonthElements = within(ui.monthSelector.get()).getAllByText( + monthMap[parseDate(parsedDate).getMonth()], + ); + await user.click(selectedMonthElements[selectedMonthElements.length - 1]); + + const yearSelector = within(ui.yearSelector.get()).getByRole('combobox'); + + await user.click(yearSelector); + const selectedYearElements = within(ui.yearSelector.get()).getAllByText( + parseDate(parsedDate).getFullYear(), + ); + await user.click(selectedYearElements[selectedYearElements.length - 1]); + + await user.click( + screen.getByText(parseDate(parsedDate).getDate().toString(), { selector: 'button' }), + ); }, async clickOnTaskAction(rowIndex: number, label: string) { const row = ui.getAllRows()[rowIndex]; expect(row).toBeVisible(); await user.click(within(row).getByRole('button', { name: 'background_tasks.show_actions' })); - await user.click(within(row).getByRole('button', { name: label })); + await user.click(within(row).getByRole('menuitem', { name: label })); }, }; diff --git a/server/sonar-web/src/main/js/apps/background-tasks/background-tasks.css b/server/sonar-web/src/main/js/apps/background-tasks/background-tasks.css deleted file mode 100644 index d88eee5073c..00000000000 --- a/server/sonar-web/src/main/js/apps/background-tasks/background-tasks.css +++ /dev/null @@ -1,44 +0,0 @@ -/* - * 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. - */ -.bt-search-form { - display: flex; - align-items: flex-end; -} - -.bt-search-form > li + li { - margin-left: 16px; -} - -.bt-search-form-field { - padding: 4px 0; -} - -.bt-search-form-large { - flex: 1; -} - -.bt-workers-warning-icon { - margin-top: 5px; -} - -.emphasised-measure { - font-size: var(--hugeFontSize); - font-weight: 300; -} diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/BackgroundTasksApp.tsx b/server/sonar-web/src/main/js/apps/background-tasks/components/BackgroundTasksApp.tsx index 70b0e5291af..ad0705a64e9 100644 --- a/server/sonar-web/src/main/js/apps/background-tasks/components/BackgroundTasksApp.tsx +++ b/server/sonar-web/src/main/js/apps/background-tasks/components/BackgroundTasksApp.tsx @@ -17,6 +17,7 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { LargeCenteredLayout, PageContentFontWrapper } from 'design-system'; import { debounce } from 'lodash'; import * as React from 'react'; import { Helmet } from 'react-helmet-async'; @@ -37,7 +38,6 @@ import { translate } from '../../../helpers/l10n'; import { parseAsDate } from '../../../helpers/query'; import { Task, TaskStatuses } from '../../../types/tasks'; import { Component, Paging, RawQuery } from '../../../types/types'; -import '../background-tasks.css'; import { CURRENTS, DEBOUNCE_DELAY, DEFAULT_FILTERS, PAGE_SIZE } from '../constants'; import { Query, mapFiltersToParameters, updateTask } from '../utils'; import Header from './Header'; @@ -192,7 +192,9 @@ export class BackgroundTasksApp extends React.PureComponent<Props, State> { this.handleFilterUpdate({ query: task.componentKey }); }; - handleShowFailing = () => { + handleShowFailing = (e: React.SyntheticEvent<HTMLAnchorElement>) => { + e.preventDefault(); + this.handleFilterUpdate({ ...DEFAULT_FILTERS, status: TaskStatuses.Failed, @@ -211,7 +213,7 @@ export class BackgroundTasksApp extends React.PureComponent<Props, State> { }; render() { - const { component } = this.props; + const { component, location } = this.props; const { loading, pagination, types, tasks } = this.state; if (!types) { @@ -223,58 +225,60 @@ export class BackgroundTasksApp extends React.PureComponent<Props, State> { ); } - const status = this.props.location.query.status || DEFAULT_FILTERS.status; - const taskType = this.props.location.query.taskType || DEFAULT_FILTERS.taskType; - const currents = this.props.location.query.currents || DEFAULT_FILTERS.currents; - const minSubmittedAt = parseAsDate(this.props.location.query.minSubmittedAt); - const maxExecutedAt = parseAsDate(this.props.location.query.maxExecutedAt); - const query = this.props.location.query.query || ''; + const status = location.query.status || DEFAULT_FILTERS.status; + const taskType = location.query.taskType || DEFAULT_FILTERS.taskType; + const currents = location.query.currents || DEFAULT_FILTERS.currents; + const minSubmittedAt = parseAsDate(location.query.minSubmittedAt); + const maxExecutedAt = parseAsDate(location.query.maxExecutedAt); + const query = location.query.query ?? ''; return ( - <main className="page page-limited"> - <Suggestions suggestions="background_tasks" /> - <Helmet defer={false} title={translate('background_tasks.page')} /> - <Header component={component} /> - - <Stats - component={component} - failingCount={this.state.failingCount} - onCancelAllPending={this.handleCancelAllPending} - onShowFailing={this.handleShowFailing} - pendingCount={this.state.pendingCount} - pendingTime={this.state.pendingTime} - /> - - <Search - component={component} - currents={currents} - loading={loading} - maxExecutedAt={maxExecutedAt} - minSubmittedAt={minSubmittedAt} - onFilterUpdate={this.handleFilterUpdate} - onReload={this.loadTasksDebounced} - query={query} - status={status} - taskType={taskType} - types={types} - /> - - <Tasks - component={component} - loading={loading} - onCancelTask={this.handleCancelTask} - onFilterTask={this.handleFilterTask} - tasks={tasks} - /> - - <ListFooter - count={tasks.length} - loadMore={this.loadMoreTasks} - loading={loading} - pageSize={pagination.pageSize} - total={pagination.total} - /> - </main> + <LargeCenteredLayout id="background-tasks"> + <PageContentFontWrapper className="sw-my-8 sw-body-sm"> + <Suggestions suggestions="background_tasks" /> + <Helmet defer={false} title={translate('background_tasks.page')} /> + <Header component={component} /> + + <Stats + component={component} + failingCount={this.state.failingCount} + onCancelAllPending={this.handleCancelAllPending} + onShowFailing={this.handleShowFailing} + pendingCount={this.state.pendingCount} + pendingTime={this.state.pendingTime} + /> + + <Search + component={component} + currents={currents} + loading={loading} + maxExecutedAt={maxExecutedAt} + minSubmittedAt={minSubmittedAt} + onFilterUpdate={this.handleFilterUpdate} + onReload={this.loadTasksDebounced} + query={query} + status={status} + taskType={taskType} + types={types} + /> + + <Tasks + component={component} + onCancelTask={this.handleCancelTask} + onFilterTask={this.handleFilterTask} + tasks={tasks} + /> + + <ListFooter + count={tasks.length} + loadMore={this.loadMoreTasks} + loading={loading} + pageSize={pagination.pageSize} + total={pagination.total} + useMIUIButtons + /> + </PageContentFontWrapper> + </LargeCenteredLayout> ); } } diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/CurrentsFilter.tsx b/server/sonar-web/src/main/js/apps/background-tasks/components/CurrentsFilter.tsx index f4f5c9dd87b..816b86685f1 100644 --- a/server/sonar-web/src/main/js/apps/background-tasks/components/CurrentsFilter.tsx +++ b/server/sonar-web/src/main/js/apps/background-tasks/components/CurrentsFilter.tsx @@ -17,19 +17,18 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { Switch } from 'design-system'; import * as React from 'react'; -import Checkbox from '../../../components/controls/Checkbox'; import { translate } from '../../../helpers/l10n'; import { CURRENTS } from '../constants'; interface CurrentsFilterProps { value?: string; - id: string; onChange: (value: string) => void; } -export default function CurrentsFilter(props: CurrentsFilterProps) { - const { id, value, onChange } = props; +export default function CurrentsFilter(props: Readonly<CurrentsFilterProps>) { + const { value, onChange } = props; const checked = value === CURRENTS.ONLY_CURRENTS; const handleChange = React.useCallback( @@ -41,10 +40,13 @@ export default function CurrentsFilter(props: CurrentsFilterProps) { ); return ( - <div className="bt-search-form-field"> - <Checkbox id={id} checked={checked} onCheck={handleChange}> - <span className="little-spacer-left">{translate('yes')}</span> - </Checkbox> - </div> + <Switch + value={checked} + onChange={handleChange} + labels={{ + on: translate('background_tasks.currents_filter.ONLY_CURRENTS'), + off: translate('background_tasks.currents_filter.ALL'), + }} + /> ); } diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/DateFilter.tsx b/server/sonar-web/src/main/js/apps/background-tasks/components/DateFilter.tsx index b89b722d23d..ae6113d98be 100644 --- a/server/sonar-web/src/main/js/apps/background-tasks/components/DateFilter.tsx +++ b/server/sonar-web/src/main/js/apps/background-tasks/components/DateFilter.tsx @@ -17,8 +17,9 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { DateRangePicker } from 'design-system'; import * as React from 'react'; -import DageRangeInput from '../../../components/controls/DateRangeInput'; +import { translate } from '../../../helpers/l10n'; interface Props { maxExecutedAt: Date | undefined; @@ -34,9 +35,14 @@ export default class DateFilter extends React.PureComponent<Props> { render() { const dateRange = { from: this.props.minSubmittedAt, to: this.props.maxExecutedAt }; return ( - <div className="nowrap"> - <DageRangeInput onChange={this.handleDateRangeChange} value={dateRange} /> - </div> + <DateRangePicker + clearButtonLabel={translate('clear')} + fromLabel={translate('start_date')} + toLabel={translate('end_date')} + onChange={this.handleDateRangeChange} + inputSize="small" + value={dateRange} + /> ); } } diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/Header.tsx b/server/sonar-web/src/main/js/apps/background-tasks/components/Header.tsx index 0fc59090648..87ee9e72e16 100644 --- a/server/sonar-web/src/main/js/apps/background-tasks/components/Header.tsx +++ b/server/sonar-web/src/main/js/apps/background-tasks/components/Header.tsx @@ -17,30 +17,34 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { Title } from 'design-system'; import * as React from 'react'; -import DocLink from '../../../components/common/DocLink'; +import DocumentationLink from '../../../components/common/DocumentationLink'; import { translate } from '../../../helpers/l10n'; +import { Component } from '../../../types/types'; import Workers from './Workers'; interface Props { - component?: any; + component?: Component; } -export default function Header(props: Props) { +export default function Header(props: Readonly<Props>) { return ( - <header className="page-header"> - <h1 className="page-title">{translate('background_tasks.page')}</h1> + <header className="sw-mb-12 sw-flex sw-justify-between"> + <div className="sw-flex-1"> + <Title className="sw-mb-4">{translate('background_tasks.page')}</Title> + <p className="sw-max-w-3/4"> + {translate('background_tasks.page.description')} + <DocumentationLink className="spacer-left" to="/analyzing-source-code/background-tasks/"> + {translate('learn_more')} + </DocumentationLink> + </p> + </div> {!props.component && ( - <div className="page-actions"> + <div> <Workers /> </div> )} - <p className="page-description"> - {translate('background_tasks.page.description')} - <DocLink className="spacer-left" to="/analyzing-source-code/background-tasks/"> - {translate('learn_more')} - </DocLink> - </p> </header> ); } diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/NoWorkersSupportPopup.tsx b/server/sonar-web/src/main/js/apps/background-tasks/components/NoWorkersSupportPopup.tsx index 08cd492f109..0806ceceb66 100644 --- a/server/sonar-web/src/main/js/apps/background-tasks/components/NoWorkersSupportPopup.tsx +++ b/server/sonar-web/src/main/js/apps/background-tasks/components/NoWorkersSupportPopup.tsx @@ -17,24 +17,19 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { Link } from 'design-system'; import * as React from 'react'; -import Link from '../../../components/common/Link'; import { translate } from '../../../helpers/l10n'; export default function NoWorkersSupportPopup() { return ( <> - <p className="spacer-bottom"> + <p className="sw-mb-2"> <strong>{translate('background_tasks.add_more_workers')}</strong> </p> - <p className="big-spacer-bottom markdown"> - {translate('background_tasks.add_more_workers.text')} - </p> + <p className="sw-mb-4 markdown">{translate('background_tasks.add_more_workers.text')}</p> <p> - <Link - to="https://www.sonarsource.com/plans-and-pricing/enterprise/?referrer=sonarqube-background-tasks" - target="_blank" - > + <Link to="https://www.sonarsource.com/plans-and-pricing/enterprise/?referrer=sonarqube-background-tasks"> {translate('learn_more')} </Link> </p> diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/ScannerContext.tsx b/server/sonar-web/src/main/js/apps/background-tasks/components/ScannerContext.tsx index a63cd07032e..da971eac10b 100644 --- a/server/sonar-web/src/main/js/apps/background-tasks/components/ScannerContext.tsx +++ b/server/sonar-web/src/main/js/apps/background-tasks/components/ScannerContext.tsx @@ -17,10 +17,11 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { Modal, Spinner } from 'design-system'; +import { noop } from 'lodash'; import * as React from 'react'; +import { FormattedMessage } from 'react-intl'; import { getTask } from '../../../api/ce'; -import Modal from '../../../components/controls/Modal'; -import { ResetButtonLink } from '../../../components/controls/buttons'; import { translate } from '../../../helpers/l10n'; import { Task } from '../../../types/tasks'; @@ -51,7 +52,7 @@ export default class ScannerContext extends React.PureComponent<Props, State> { if (this.mounted) { this.setState({ scannerContext: task.scannerContext }); } - }); + }, noop); } render() { @@ -59,30 +60,26 @@ export default class ScannerContext extends React.PureComponent<Props, State> { const { scannerContext } = this.state; return ( - <Modal contentLabel="scanner context" onRequestClose={this.props.onClose} size="large"> - <div className="modal-head"> - <h2> - {translate('background_tasks.scanner_context')} - {': '} - {task.componentName} - {' ['} - {translate('background_task.type', task.type)} - {']'} - </h2> - </div> - - <div className="modal-body modal-container"> - {scannerContext != null ? ( + <Modal + onClose={this.props.onClose} + isLarge + isScrollable + headerTitle={ + <FormattedMessage + id="background_tasks.error_stacktrace.title" + values={{ + project: task.componentName, + type: translate('background_task.type', task.type), + }} + /> + } + body={ + <Spinner loading={scannerContext == null}> <pre className="js-task-scanner-context">{scannerContext}</pre> - ) : ( - <i className="spinner" /> - )} - </div> - - <div className="modal-foot"> - <ResetButtonLink onClick={this.props.onClose}>{translate('close')}</ResetButtonLink> - </div> - </Modal> + </Spinner> + } + secondaryButtonLabel={translate('close')} + /> ); } } diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/Search.tsx b/server/sonar-web/src/main/js/apps/background-tasks/components/Search.tsx index 0164436a3f6..358a48179b9 100644 --- a/server/sonar-web/src/main/js/apps/background-tasks/components/Search.tsx +++ b/server/sonar-web/src/main/js/apps/background-tasks/components/Search.tsx @@ -17,9 +17,8 @@ * 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, InputSearch } from 'design-system'; import * as React from 'react'; -import { Button } from '../../../components/controls/buttons'; -import SearchBox from '../../../components/controls/SearchBox'; import { translate } from '../../../helpers/l10n'; import { DEFAULT_FILTERS } from '../constants'; import { Query } from '../utils'; @@ -67,78 +66,61 @@ export default class Search extends React.PureComponent<Props> { this.props.onFilterUpdate(DEFAULT_FILTERS); }; - renderSearchBox() { - const { component, query } = this.props; - - if (component) { - // do not render search form on the project-level page - return null; - } - - return ( - <li className="bt-search-form-large"> - <SearchBox - onChange={this.handleQueryChange} - placeholder={translate('background_tasks.search_by_task_or_component')} - value={query} - /> - </li> - ); - } - render() { - const { loading, component, types, status, taskType, currents, minSubmittedAt, maxExecutedAt } = - this.props; + const { + loading, + component, + query, + types, + status, + taskType, + currents, + minSubmittedAt, + maxExecutedAt, + } = this.props; return ( - <section className="big-spacer-top big-spacer-bottom"> - <ul className="bt-search-form"> + <section className="sw-my-4"> + <ul className="sw-flex sw-items-center sw-flex-wrap sw-gap-4"> <li> - <div className="display-flex-column"> - <label - id="background-task-status-filter-label" - className="text-bold little-spacer-bottom" - htmlFor="status-filter" - > - {translate('status')} - </label> - <StatusFilter id="status-filter" onChange={this.handleStatusChange} value={status} /> - </div> + <label + id="background-task-status-filter-label" + className="sw-body-sm-highlight sw-mr-2" + htmlFor="status-filter" + > + {translate('status')} + </label> + <StatusFilter id="status-filter" onChange={this.handleStatusChange} value={status} /> </li> {types.length > 1 && ( <li> - <div className="display-flex-column"> - <label - id="background-task-type-filter-label" - className="text-bold little-spacer-bottom" - htmlFor="types-filter" - > - {translate('type')} - </label> - <TypesFilter - id="types-filter" - onChange={this.handleTypeChange} - types={types} - value={taskType} - /> - </div> + <label + id="background-task-type-filter-label" + className="sw-body-sm-highlight sw-mr-2" + htmlFor="types-filter" + > + {translate('type')} + </label> + <TypesFilter + id="types-filter" + onChange={this.handleTypeChange} + types={types} + value={taskType} + /> </li> )} {!component && ( - <li> - <div className="display-flex-column"> - <label className="text-bold little-spacer-bottom" htmlFor="currents-filter"> - {translate('background_tasks.currents_filter.ONLY_CURRENTS')} - </label> - <CurrentsFilter - id="currents-filter" - onChange={this.handleCurrentsChange} - value={currents} - /> - </div> + <li className="sw-flex sw-items-center"> + <label className="sw-body-sm-highlight sw-mr-2"> + {translate('background_tasks.currents_filter.ONLY_CURRENTS')} + </label> + <CurrentsFilter onChange={this.handleCurrentsChange} value={currents} /> </li> )} - <li> + <li className="sw-flex sw-items-center"> + <label className="sw-body-sm-highlight sw-mr-2"> + {translate('background_tasks.date_filter')} + </label> <DateFilter maxExecutedAt={maxExecutedAt} minSubmittedAt={minSubmittedAt} @@ -146,15 +128,27 @@ export default class Search extends React.PureComponent<Props> { /> </li> - {this.renderSearchBox()} + {!component && ( + <li> + <InputSearch + onChange={this.handleQueryChange} + placeholder={translate('background_tasks.search_by_task_or_component')} + value={query} + /> + </li> + )} - <li className="nowrap"> - <Button className="js-reload" disabled={loading} onClick={this.props.onReload}> + <li> + <ButtonSecondary + className="js-reload sw-mr-2" + disabled={loading} + onClick={this.props.onReload} + > {translate('reload')} - </Button>{' '} - <Button disabled={loading} onClick={this.handleReset}> + </ButtonSecondary> + <ButtonSecondary disabled={loading} onClick={this.handleReset}> {translate('reset_verb')} - </Button> + </ButtonSecondary> </li> </ul> </section> diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/Stacktrace.tsx b/server/sonar-web/src/main/js/apps/background-tasks/components/Stacktrace.tsx index 92938ac6c89..c0e8b6912e5 100644 --- a/server/sonar-web/src/main/js/apps/background-tasks/components/Stacktrace.tsx +++ b/server/sonar-web/src/main/js/apps/background-tasks/components/Stacktrace.tsx @@ -17,10 +17,10 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { Modal, Spinner } from 'design-system'; import * as React from 'react'; +import { FormattedMessage } from 'react-intl'; import { getTask } from '../../../api/ce'; -import Modal from '../../../components/controls/Modal'; -import { ResetButtonLink } from '../../../components/controls/buttons'; import { translate } from '../../../helpers/l10n'; import { Task } from '../../../types/tasks'; @@ -67,40 +67,36 @@ export default class Stacktrace extends React.PureComponent<Props, State> { const { loading, stacktrace } = this.state; return ( - <Modal contentLabel="stacktrace" onRequestClose={this.props.onClose} size="large"> - <div className="modal-head"> - <h2> - {translate('background_tasks.error_stacktrace')} - {': '} - {task.componentName} - {' ['} - {translate('background_task.type', task.type)} - {']'} - </h2> - </div> - - <div className="modal-body modal-container"> - {loading ? ( - <i className="spinner" /> - ) : stacktrace ? ( - <div> - <h4 className="spacer-bottom">{translate('background_tasks.error_stacktrace')}</h4> - <pre className="js-task-stacktrace">{stacktrace}</pre> - </div> - ) : ( - <div> - <h4 className="spacer-bottom">{translate('background_tasks.error_message')}</h4> - <pre className="js-task-error-message">{task.errorMessage}</pre> - </div> - )} - </div> - - <div className="modal-foot"> - <ResetButtonLink className="js-modal-close" onClick={this.props.onClose}> - {translate('close')} - </ResetButtonLink> - </div> - </Modal> + <Modal + onClose={this.props.onClose} + isLarge + isScrollable + headerTitle={ + <FormattedMessage + id="background_tasks.error_stacktrace.title" + values={{ + project: task.componentName, + type: translate('background_task.type', task.type), + }} + /> + } + body={ + <Spinner loading={loading}> + {stacktrace ? ( + <div> + <h4 className="sw-mb-2">{translate('background_tasks.error_stacktrace')}</h4> + <pre className="js-task-stacktrace">{stacktrace}</pre> + </div> + ) : ( + <div> + <h4 className="sw-mb-2">{translate('background_tasks.error_message')}</h4> + <pre className="js-task-error-message">{task.errorMessage}</pre> + </div> + )} + </Spinner> + } + secondaryButtonLabel={translate('close')} + /> ); } } diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/StatPendingCount.tsx b/server/sonar-web/src/main/js/apps/background-tasks/components/StatPendingCount.tsx index 48be363a391..4abe690af8d 100644 --- a/server/sonar-web/src/main/js/apps/background-tasks/components/StatPendingCount.tsx +++ b/server/sonar-web/src/main/js/apps/background-tasks/components/StatPendingCount.tsx @@ -17,12 +17,11 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { DestructiveIcon, TrashIcon } from 'design-system'; import * as React from 'react'; import withAppStateContext from '../../../app/components/app-state/withAppStateContext'; -import { colors } from '../../../app/theme'; import ConfirmButton from '../../../components/controls/ConfirmButton'; import Tooltip from '../../../components/controls/Tooltip'; -import { ClearButton } from '../../../components/controls/buttons'; import { translate } from '../../../helpers/l10n'; import { AppState } from '../../../types/appstate'; @@ -32,34 +31,37 @@ export interface Props { pendingCount?: number; } -function StatPendingCount({ appState, onCancelAllPending, pendingCount }: Props) { +function StatPendingCount({ appState, onCancelAllPending, pendingCount }: Readonly<Props>) { if (pendingCount === undefined) { return null; } return ( - <span> - <span className="emphasised-measure">{pendingCount}</span> - <span className="little-spacer-left display-inline-flex-center"> - {translate('background_tasks.pending')} - {appState.canAdmin && pendingCount > 0 && ( - <ConfirmButton - cancelButtonText={translate('close')} - confirmButtonText={translate('background_tasks.cancel_all_tasks.submit')} - isDestructive - modalBody={translate('background_tasks.cancel_all_tasks.text')} - modalHeader={translate('background_tasks.cancel_all_tasks')} - onConfirm={onCancelAllPending} - > - {({ onClick }) => ( - <Tooltip overlay={translate('background_tasks.cancel_all_tasks')}> - <ClearButton className="little-spacer-left" color={colors.red} onClick={onClick} /> - </Tooltip> - )} - </ConfirmButton> - )} - </span> - </span> + <div className="sw-flex sw-items-center"> + <span className="sw-body-md-highlight sw-mr-1">{pendingCount}</span> + {translate('background_tasks.pending')} + {appState.canAdmin && pendingCount > 0 && ( + <ConfirmButton + cancelButtonText={translate('close')} + confirmButtonText={translate('background_tasks.cancel_all_tasks.submit')} + isDestructive + modalBody={translate('background_tasks.cancel_all_tasks.text')} + modalHeader={translate('background_tasks.cancel_all_tasks')} + onConfirm={onCancelAllPending} + > + {({ onClick }) => ( + <Tooltip overlay={translate('background_tasks.cancel_all_tasks')}> + <DestructiveIcon + aria-label={translate('background_tasks.cancel_all_tasks')} + className="sw-ml-1" + Icon={TrashIcon} + onClick={onClick} + /> + </Tooltip> + )} + </ConfirmButton> + )} + </div> ); } diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/StatPendingTime.tsx b/server/sonar-web/src/main/js/apps/background-tasks/components/StatPendingTime.tsx index c8a7d86f0c0..21533525b97 100644 --- a/server/sonar-web/src/main/js/apps/background-tasks/components/StatPendingTime.tsx +++ b/server/sonar-web/src/main/js/apps/background-tasks/components/StatPendingTime.tsx @@ -17,6 +17,7 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { HelperHintIcon } from 'design-system'; import * as React from 'react'; import HelpTooltip from '../../../components/controls/HelpTooltip'; import { translate } from '../../../helpers/l10n'; @@ -26,23 +27,24 @@ import { formatMeasure } from '../../../helpers/measures'; const MIN_PENDING_TIME_THRESHOLD = 1000; export interface Props { - className?: string; pendingCount?: number; pendingTime?: number; } -export default function StatPendingTime({ className, pendingCount, pendingTime }: Props) { +export default function StatPendingTime({ pendingCount, pendingTime }: Readonly<Props>) { if (!pendingTime || !pendingCount || pendingTime < MIN_PENDING_TIME_THRESHOLD) { return null; } return ( - <span className={className}> - <span className="emphasised-measure">{formatMeasure(pendingTime, 'MILLISEC')}</span> - <span className="little-spacer-left">{translate('background_tasks.pending_time')}</span> + <div className="sw-flex sw-items-center"> + <span className="sw-body-md-highlight sw-mr-1">{formatMeasure(pendingTime, 'MILLISEC')}</span> + {translate('background_tasks.pending_time')} <HelpTooltip - className="little-spacer-left" + className="sw-ml-1" overlay={translate('background_tasks.pending_time.description')} - /> - </span> + > + <HelperHintIcon /> + </HelpTooltip> + </div> ); } diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/StatStillFailing.tsx b/server/sonar-web/src/main/js/apps/background-tasks/components/StatStillFailing.tsx index 407050341ab..36369fbdddc 100644 --- a/server/sonar-web/src/main/js/apps/background-tasks/components/StatStillFailing.tsx +++ b/server/sonar-web/src/main/js/apps/background-tasks/components/StatStillFailing.tsx @@ -17,36 +17,38 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { HelperHintIcon, StandoutLink } from 'design-system'; import * as React from 'react'; -import { ButtonLink } from '../../../components/controls/buttons'; import HelpTooltip from '../../../components/controls/HelpTooltip'; import { translate } from '../../../helpers/l10n'; export interface Props { - className?: string; failingCount?: number; - onShowFailing: () => void; + onShowFailing: (e: React.SyntheticEvent<HTMLAnchorElement>) => void; } -export default function StatStillFailing({ className, failingCount, onShowFailing }: Props) { +export default function StatStillFailing({ failingCount, onShowFailing }: Readonly<Props>) { if (failingCount === undefined) { return null; } return ( - <span className={className}> + <div className="sw-flex sw-items-center "> {failingCount > 0 ? ( - <ButtonLink className="emphasised-measure text-baseline" onClick={onShowFailing}> + <StandoutLink + className="sw-body-md-highlight sw-align-baseline" + to="#" + onClick={onShowFailing} + > {failingCount} - </ButtonLink> + </StandoutLink> ) : ( - <span className="emphasised-measure">{failingCount}</span> + <span className="sw-body-md-highlight">{failingCount}</span> )} - <span className="little-spacer-left">{translate('background_tasks.failures')}</span> - <HelpTooltip - className="little-spacer-left" - overlay={translate('background_tasks.failing_count')} - /> - </span> + <span className="sw-ml-1">{translate('background_tasks.failures')}</span> + <HelpTooltip className="sw-ml-1" overlay={translate('background_tasks.failing_count')}> + <HelperHintIcon /> + </HelpTooltip> + </div> ); } diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/Stats.tsx b/server/sonar-web/src/main/js/apps/background-tasks/components/Stats.tsx index b5cf7b32fc1..0f7a011d752 100644 --- a/server/sonar-web/src/main/js/apps/background-tasks/components/Stats.tsx +++ b/server/sonar-web/src/main/js/apps/background-tasks/components/Stats.tsx @@ -27,28 +27,20 @@ export interface Props { component?: Pick<Component, 'key'>; failingCount?: number; onCancelAllPending: () => void; - onShowFailing: () => void; + onShowFailing: (e: React.SyntheticEvent<HTMLAnchorElement>) => void; pendingCount?: number; pendingTime?: number; } -export default function Stats({ component, pendingCount, pendingTime, ...props }: Props) { +export default function Stats({ component, pendingCount, pendingTime, ...props }: Readonly<Props>) { return ( - <section className="big-spacer-top big-spacer-bottom"> + <section className="sw-flex sw-items-center sw-my-4 sw-gap-8 sw-body-md"> <StatPendingCount onCancelAllPending={props.onCancelAllPending} pendingCount={pendingCount} /> {!component && ( - <StatPendingTime - className="huge-spacer-left" - pendingCount={pendingCount} - pendingTime={pendingTime} - /> - )} - {!component && ( - <StatStillFailing - className="huge-spacer-left" - failingCount={props.failingCount} - onShowFailing={props.onShowFailing} - /> + <> + <StatPendingTime pendingCount={pendingCount} pendingTime={pendingTime} /> + <StatStillFailing failingCount={props.failingCount} onShowFailing={props.onShowFailing} /> + </> )} </section> ); diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/StatusFilter.tsx b/server/sonar-web/src/main/js/apps/background-tasks/components/StatusFilter.tsx index cf09c8f331f..5a7c6c9a24f 100644 --- a/server/sonar-web/src/main/js/apps/background-tasks/components/StatusFilter.tsx +++ b/server/sonar-web/src/main/js/apps/background-tasks/components/StatusFilter.tsx @@ -17,8 +17,8 @@ * 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, LabelValueSelectOption } from 'design-system'; import * as React from 'react'; -import Select, { LabelValueSelectOption } from '../../../components/controls/Select'; import { translate } from '../../../helpers/l10n'; import { TaskStatuses } from '../../../types/tasks'; import { STATUSES } from '../constants'; @@ -29,10 +29,10 @@ interface StatusFilterProps { onChange: (value?: string) => void; } -export default function StatusFilter(props: StatusFilterProps) { +export default function StatusFilter(props: Readonly<StatusFilterProps>) { const { id, value, onChange } = props; - const options: LabelValueSelectOption[] = [ + const options: LabelValueSelectOption<string>[] = [ { value: STATUSES.ALL, label: translate('background_task.status.ALL') }, { value: STATUSES.ALL_EXCEPT_PENDING, @@ -46,21 +46,21 @@ export default function StatusFilter(props: StatusFilterProps) { ]; const handleChange = React.useCallback( - ({ value }: LabelValueSelectOption) => { + ({ value }: LabelValueSelectOption<string>) => { onChange(value); }, [onChange], ); return ( - <Select + <InputSelect aria-labelledby="background-task-status-filter-label" - className="input-medium" + className="sw-w-abs-200" id={id} onChange={handleChange} options={options} + size="medium" value={options.find((o) => o.value === value)} - isSearchable={false} /> ); } diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/Task.tsx b/server/sonar-web/src/main/js/apps/background-tasks/components/Task.tsx index 27c538d38bb..c6a2bdad84e 100644 --- a/server/sonar-web/src/main/js/apps/background-tasks/components/Task.tsx +++ b/server/sonar-web/src/main/js/apps/background-tasks/components/Task.tsx @@ -17,16 +17,15 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { TableRow } from 'design-system'; import * as React from 'react'; -import { AppState } from '../../../types/appstate'; +import { AppStateContext } from '../../../app/components/app-state/AppStateContext'; import { EditionKey } from '../../../types/editions'; import { Task as ITask } from '../../../types/tasks'; import TaskActions from './TaskActions'; import TaskComponent from './TaskComponent'; import TaskDate from './TaskDate'; -import TaskDay from './TaskDay'; import TaskExecutionTime from './TaskExecutionTime'; -import TaskId from './TaskId'; import TaskNodeName from './TaskNodeName'; import TaskStatus from './TaskStatus'; import TaskSubmitter from './TaskSubmitter'; @@ -36,25 +35,20 @@ interface Props { onCancelTask: (task: ITask) => Promise<void>; onFilterTask: (task: ITask) => void; task: ITask; - previousTask?: ITask; - appState: AppState; } -export default function Task(props: Props) { - const { task, component, onCancelTask, onFilterTask, previousTask, appState } = props; +export default function Task(props: Readonly<Props>) { + const { task, component, onCancelTask, onFilterTask } = props; + + const appState = React.useContext(AppStateContext); + const isDataCenter = appState.edition === EditionKey.datacenter; return ( - <tr> + <TableRow> <TaskStatus status={task.status} /> <TaskComponent task={task} /> - <TaskId id={task.id} /> - <TaskSubmitter submitter={task.submitterLogin} /> - {appState?.edition === EditionKey.datacenter && <TaskNodeName nodeName={task.nodeName} />} - <TaskDay - prevSubmittedAt={previousTask && previousTask.submittedAt} - submittedAt={task.submittedAt} - /> - <TaskDate date={task.submittedAt} /> + {isDataCenter && <TaskNodeName nodeName={task.nodeName} />} + <TaskSubmitter submittedAt={task.submittedAt} submitter={task.submitterLogin} /> <TaskDate baseDate={task.submittedAt} date={task.startedAt} /> <TaskDate baseDate={task.submittedAt} date={task.executedAt} /> <TaskExecutionTime ms={task.executionTimeMs} /> @@ -64,6 +58,6 @@ export default function Task(props: Props) { onFilterTask={onFilterTask} task={task} /> - </tr> + </TableRow> ); } diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/TaskActions.tsx b/server/sonar-web/src/main/js/apps/background-tasks/components/TaskActions.tsx index 39b30634b87..ec6464f1df9 100644 --- a/server/sonar-web/src/main/js/apps/background-tasks/components/TaskActions.tsx +++ b/server/sonar-web/src/main/js/apps/background-tasks/components/TaskActions.tsx @@ -17,8 +17,8 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { ActionCell, ActionsDropdown, ItemButton, ItemDangerButton } from 'design-system'; import * as React from 'react'; -import ActionsDropdown, { ActionsDropdownItem } from '../../../components/controls/ActionsDropdown'; import ConfirmModal from '../../../components/controls/ConfirmModal'; import { translate, translateWithParameters } from '../../../helpers/l10n'; import { Task, TaskStatuses } from '../../../types/tasks'; @@ -103,51 +103,45 @@ export default class TaskActions extends React.PureComponent<Props, State> { } return ( - <td className="thin nowrap"> + <ActionCell> <ActionsDropdown - label={translate('background_tasks.show_actions')} + id={`task-${task.id}-actions`} + ariaLabel={translate('background_tasks.show_actions')} className="js-task-action" > {canFilter && task.componentName && ( - <ActionsDropdownItem className="js-task-filter" onClick={this.handleFilterClick}> + <ItemButton className="js-task-filter" onClick={this.handleFilterClick}> {translateWithParameters( 'background_tasks.filter_by_component_x', task.componentName, )} - </ActionsDropdownItem> + </ItemButton> )} {canCancel && ( - <ActionsDropdownItem - className="js-task-cancel" - destructive - onClick={this.handleCancelClick} - > + <ItemDangerButton className="js-task-cancel" onClick={this.handleCancelClick}> {translate('background_tasks.cancel_task')} - </ActionsDropdownItem> + </ItemDangerButton> )} {task.hasScannerContext && ( - <ActionsDropdownItem + <ItemButton className="js-task-show-scanner-context" onClick={this.handleShowScannerContextClick} > {translate('background_tasks.show_scanner_context')} - </ActionsDropdownItem> + </ItemButton> )} {canShowStacktrace && ( - <ActionsDropdownItem + <ItemButton className="js-task-show-stacktrace" onClick={this.handleShowStacktraceClick} > {translate('background_tasks.show_stacktrace')} - </ActionsDropdownItem> + </ItemButton> )} {canShowWarnings && ( - <ActionsDropdownItem - className="js-task-show-warnings" - onClick={this.handleShowWarningsClick} - > + <ItemButton className="js-task-show-warnings" onClick={this.handleShowWarningsClick}> {translate('background_tasks.show_warnings')} - </ActionsDropdownItem> + </ItemButton> )} </ActionsDropdown> @@ -177,7 +171,7 @@ export default class TaskActions extends React.PureComponent<Props, State> { taskId={task.id} /> )} - </td> + </ActionCell> ); } } diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/TaskComponent.tsx b/server/sonar-web/src/main/js/apps/background-tasks/components/TaskComponent.tsx index 6e248875d59..bdcf3d16be6 100644 --- a/server/sonar-web/src/main/js/apps/background-tasks/components/TaskComponent.tsx +++ b/server/sonar-web/src/main/js/apps/background-tasks/components/TaskComponent.tsx @@ -17,11 +17,17 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import styled from '@emotion/styled'; +import { + BranchIcon, + ContentCell, + Note, + PullRequestIcon, + QualifierIcon, + StandoutLink, +} from 'design-system'; import * as React from 'react'; -import Link from '../../../components/common/Link'; -import BranchIcon from '../../../components/icons/BranchIcon'; -import PullRequestIcon from '../../../components/icons/PullRequestIcon'; -import QualifierIcon from '../../../components/icons/QualifierIcon'; +import { translate } from '../../../helpers/l10n'; import { getBranchUrl, getPortfolioUrl, @@ -30,57 +36,50 @@ import { } from '../../../helpers/urls'; import { isPortfolioLike } from '../../../types/component'; import { Task } from '../../../types/tasks'; -import TaskType from './TaskType'; interface Props { task: Task; } -export default function TaskComponent({ task }: Props) { - if (!task.componentKey) { - return ( - <td> - <span className="note">{task.id}</span> - <TaskType type={task.type} /> - </td> - ); - } - +export default function TaskComponent({ task }: Readonly<Props>) { return ( - <td> - {task.branch !== undefined && <BranchIcon className="little-spacer-right" />} - {task.pullRequest !== undefined && <PullRequestIcon className="little-spacer-right" />} + <ContentCell> + <div> + <p> + {task.componentKey && ( + <span className="sw-mr-2"> + <TaskComponentIndicator task={task} /> - {!task.branch && !task.pullRequest && task.componentQualifier && ( - <span className="little-spacer-right"> - <QualifierIcon qualifier={task.componentQualifier} /> - </span> - )} + {task.componentName && ( + <StandoutLink className="sw-ml-2" to={getTaskComponentUrl(task.componentKey, task)}> + <StyledSpan title={task.componentName}>{task.componentName}</StyledSpan> - {task.componentName && ( - <Link className="spacer-right" to={getTaskComponentUrl(task.componentKey, task)}> - <span className="text-limited text-text-top" title={task.componentName}> - {task.componentName} - </span> + {task.branch && ( + <StyledSpan title={task.branch}> + <span className="sw-mx-1">/</span> + {task.branch} + </StyledSpan> + )} - {task.branch && ( - <span className="text-limited text-text-top" title={task.branch}> - <span style={{ marginLeft: 5, marginRight: 5 }}>/</span> - {task.branch} + {task.pullRequest && ( + <StyledSpan title={task.pullRequestTitle}> + <span className="sw-mx-1">/</span> + {task.pullRequest} + </StyledSpan> + )} + </StandoutLink> + )} </span> )} - {task.pullRequest && ( - <span className="text-limited text-text-top" title={task.pullRequestTitle}> - <span style={{ marginLeft: 5, marginRight: 5 }}>/</span> - {task.pullRequest} - </span> - )} - </Link> - )} + <span>{translate('background_task.type', task.type)}</span> + </p> - <TaskType type={task.type} /> - </td> + <Note as="div" className="sw-mt-2"> + {translate('background_tasks.table.id')}: {task.id} + </Note> + </div> + </ContentCell> ); } @@ -94,3 +93,28 @@ function getTaskComponentUrl(componentKey: string, task: Task) { } return getProjectUrl(componentKey); } + +const StyledSpan = styled.span` + display: inline-block; + max-width: 16vw; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + margin-bottom: -4px; /* compensate the inline-block effect on the wrapping link */ +`; + +function TaskComponentIndicator({ task }: Readonly<Props>) { + if (task.branch !== undefined) { + return <BranchIcon />; + } + + if (task.pullRequest !== undefined) { + return <PullRequestIcon />; + } + + if (task.componentQualifier) { + return <QualifierIcon qualifier={task.componentQualifier} />; + } + + return null; +} diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/TaskDate.tsx b/server/sonar-web/src/main/js/apps/background-tasks/components/TaskDate.tsx index 88ca7c10e58..f69a81a8468 100644 --- a/server/sonar-web/src/main/js/apps/background-tasks/components/TaskDate.tsx +++ b/server/sonar-web/src/main/js/apps/background-tasks/components/TaskDate.tsx @@ -17,7 +17,9 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import styled from '@emotion/styled'; import { differenceInDays } from 'date-fns'; +import { Note, NumericalCell, themeColor } from 'design-system'; import * as React from 'react'; import TimeFormatter from '../../../components/intl/TimeFormatter'; import { isValidDate, parseDate } from '../../../helpers/dates'; @@ -27,19 +29,29 @@ interface Props { baseDate?: string; } -export default function TaskDate({ date, baseDate }: Props) { - const parsedDate = date && parseDate(date); - const parsedBaseDate = baseDate && parseDate(baseDate); +export default function TaskDate({ date, baseDate }: Readonly<Props>) { + const parsedDate = date !== undefined && parseDate(date); + const parsedBaseDate = baseDate !== undefined && parseDate(baseDate); const diff = parsedDate && parsedBaseDate && isValidDate(parsedDate) && isValidDate(parsedBaseDate) ? differenceInDays(parsedDate, parsedBaseDate) : 0; return ( - <td className="thin nowrap text-right"> - {diff > 0 && <span className="text-warning little-spacer-right">{`(+${diff}d)`}</span>} + <NumericalCell className="sw-px-2"> + {diff > 0 && <StyledWarningText className="sw-mr-1">{`(+${diff}d)`}</StyledWarningText>} - {parsedDate && isValidDate(parsedDate) ? <TimeFormatter date={parsedDate} long /> : ''} - </td> + {parsedDate && isValidDate(parsedDate) ? ( + <span className="sw-whitespace-nowrap"> + <TimeFormatter date={parsedDate} long /> + </span> + ) : ( + '' + )} + </NumericalCell> ); } + +const StyledWarningText = styled(Note)` + color: ${themeColor('warningText')}; +`; diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/TaskDay.tsx b/server/sonar-web/src/main/js/apps/background-tasks/components/TaskDay.tsx deleted file mode 100644 index 1d076a32b96..00000000000 --- a/server/sonar-web/src/main/js/apps/background-tasks/components/TaskDay.tsx +++ /dev/null @@ -1,39 +0,0 @@ -/* - * 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 { isSameDay } from 'date-fns'; -import * as React from 'react'; -import DateFormatter from '../../../components/intl/DateFormatter'; -import { parseDate } from '../../../helpers/dates'; - -interface Props { - submittedAt: string; - prevSubmittedAt?: string; -} - -export default function TaskDay({ submittedAt, prevSubmittedAt }: Props) { - const shouldDisplay = - !prevSubmittedAt || !isSameDay(parseDate(submittedAt), parseDate(prevSubmittedAt)); - - return ( - <td className="thin nowrap text-right small"> - {shouldDisplay ? <DateFormatter date={submittedAt} long /> : ''} - </td> - ); -} diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/TaskExecutionTime.tsx b/server/sonar-web/src/main/js/apps/background-tasks/components/TaskExecutionTime.tsx index b7cddbf0e1b..b7a2733ebc2 100644 --- a/server/sonar-web/src/main/js/apps/background-tasks/components/TaskExecutionTime.tsx +++ b/server/sonar-web/src/main/js/apps/background-tasks/components/TaskExecutionTime.tsx @@ -17,6 +17,7 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { NumericalCell } from 'design-system/lib'; import * as React from 'react'; import { formatDuration } from '../utils'; @@ -24,6 +25,10 @@ interface Props { ms?: number; } -export default function TaskExecutionTime({ ms }: Props) { - return <td className="thin nowrap text-right">{ms && formatDuration(ms)}</td>; +export default function TaskExecutionTime({ ms }: Readonly<Props>) { + return ( + <NumericalCell className="sw-whitespace-nowrap"> + {ms !== undefined && formatDuration(ms)} + </NumericalCell> + ); } diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/TaskId.tsx b/server/sonar-web/src/main/js/apps/background-tasks/components/TaskId.tsx deleted file mode 100644 index b70dfd204ee..00000000000 --- a/server/sonar-web/src/main/js/apps/background-tasks/components/TaskId.tsx +++ /dev/null @@ -1,32 +0,0 @@ -/* - * 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'; - -interface Props { - id: string; -} - -export default function TaskId({ id }: Props) { - return ( - <td className="thin nowrap"> - <div className="note">{id}</div> - </td> - ); -} diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/TaskNodeName.tsx b/server/sonar-web/src/main/js/apps/background-tasks/components/TaskNodeName.tsx index a8fdfd7e2a7..3d616c93e36 100644 --- a/server/sonar-web/src/main/js/apps/background-tasks/components/TaskNodeName.tsx +++ b/server/sonar-web/src/main/js/apps/background-tasks/components/TaskNodeName.tsx @@ -17,16 +17,17 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { ContentCell, Note } from 'design-system'; import * as React from 'react'; interface Props { nodeName?: string; } -export default function TaskNodeName({ nodeName }: Props) { +export default function TaskNodeName({ nodeName }: Readonly<Props>) { return ( - <td className="thin"> - <div className="note">{nodeName}</div> - </td> + <ContentCell> + <Note>{nodeName}</Note> + </ContentCell> ); } diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/TaskStatus.tsx b/server/sonar-web/src/main/js/apps/background-tasks/components/TaskStatus.tsx index 3d2fe6a250f..5b2e6f3f489 100644 --- a/server/sonar-web/src/main/js/apps/background-tasks/components/TaskStatus.tsx +++ b/server/sonar-web/src/main/js/apps/background-tasks/components/TaskStatus.tsx @@ -17,8 +17,15 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { + ClockIcon, + ContentCell, + FlagErrorIcon, + FlagSuccessIcon, + FlagWarningIcon, + Spinner, +} from 'design-system'; import * as React from 'react'; -import PendingIcon from '../../../components/icons/PendingIcon'; import { translate } from '../../../helpers/l10n'; import { TaskStatuses } from '../../../types/tasks'; @@ -26,32 +33,51 @@ interface Props { status: string; } -export default function TaskStatus({ status }: Props) { - let inner; +interface StatusDataDictionnary { + [key: string]: StatusDataType; +} + +interface StatusDataType { + iconComponent: React.ReactElement; + textKey: string; +} + +const STATUS_ENUM: StatusDataDictionnary = { + [TaskStatuses.Pending]: { + iconComponent: <ClockIcon />, + textKey: 'background_task.status.PENDING', + }, + [TaskStatuses.InProgress]: { + iconComponent: <Spinner />, + textKey: 'background_task.status.IN_PROGRESS', + }, + [TaskStatuses.Success]: { + iconComponent: <FlagSuccessIcon />, + textKey: 'background_task.status.SUCCESS', + }, + [TaskStatuses.Failed]: { + iconComponent: <FlagErrorIcon />, + textKey: 'background_task.status.FAILED', + }, + [TaskStatuses.Canceled]: { + iconComponent: <FlagWarningIcon />, + textKey: 'background_task.status.CANCELED', + }, +}; + +export default function TaskStatus({ status }: Readonly<Props>) { + const statusData = STATUS_ENUM[status]; - switch (status) { - case TaskStatuses.Pending: - inner = <PendingIcon />; - break; - case TaskStatuses.InProgress: - inner = <i className="spinner" />; - break; - case TaskStatuses.Success: - inner = ( - <span className="badge badge-success">{translate('background_task.status.SUCCESS')}</span> - ); - break; - case TaskStatuses.Failed: - inner = ( - <span className="badge badge-error">{translate('background_task.status.FAILED')}</span> - ); - break; - case TaskStatuses.Canceled: - inner = <span className="badge">{translate('background_task.status.CANCELED')}</span>; - break; - default: - inner = ''; + if (!statusData) { + return <ContentCell />; } - return <td className="thin spacer-right">{inner}</td>; + return ( + <ContentCell> + <div className="sw-flex sw-gap-1 sw-items-center"> + {statusData.iconComponent} + {translate(statusData.textKey)} + </div> + </ContentCell> + ); } diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/TaskSubmitter.tsx b/server/sonar-web/src/main/js/apps/background-tasks/components/TaskSubmitter.tsx index 67c5c5f1b50..bfb5085fe4b 100644 --- a/server/sonar-web/src/main/js/apps/background-tasks/components/TaskSubmitter.tsx +++ b/server/sonar-web/src/main/js/apps/background-tasks/components/TaskSubmitter.tsx @@ -17,17 +17,42 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { ContentCell, Note } from 'design-system'; import * as React from 'react'; +import { FormattedMessage } from 'react-intl'; +import DateFormatter from '../../../components/intl/DateFormatter'; +import TimeFormatter from '../../../components/intl/TimeFormatter'; +import { isValidDate, parseDate } from '../../../helpers/dates'; import { translate } from '../../../helpers/l10n'; interface Props { + submittedAt: string; submitter?: string; } -export default function TaskSubmitter({ submitter }: Props) { +export default function TaskSubmitter(props: Readonly<Props>) { + const { submitter = translate('anonymous'), submittedAt } = props; + return ( - <td className="thin note"> - <span className="text-limited-small text-bottom">{submitter || translate('anonymous')}</span> - </td> + <ContentCell> + <div> + <div className="sw-whitespace-nowrap"> + {isValidDate(parseDate(submittedAt)) ? ( + <FormattedMessage + id="background_tasks.date_and_time" + values={{ + date: <DateFormatter date={submittedAt} long />, + time: <TimeFormatter date={submittedAt} long />, + }} + /> + ) : ( + <DateFormatter date={submittedAt} long /> + )} + </div> + <Note> + <FormattedMessage id="background_tasks.submitted_by_x" values={{ submitter }} /> + </Note> + </div> + </ContentCell> ); } diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/TaskType.tsx b/server/sonar-web/src/main/js/apps/background-tasks/components/TaskType.tsx deleted file mode 100644 index cf7a9763d89..00000000000 --- a/server/sonar-web/src/main/js/apps/background-tasks/components/TaskType.tsx +++ /dev/null @@ -1,35 +0,0 @@ -/* - * 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 { translate } from '../../../helpers/l10n'; - -interface Props { - type: string; -} - -export default function TaskType({ type }: Props) { - return ( - <span className="display-inline-block note"> - {'['} - {translate('background_task.type', type)} - {']'} - </span> - ); -} diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/Tasks.tsx b/server/sonar-web/src/main/js/apps/background-tasks/components/Tasks.tsx index 76114c6af1f..9099f90e0c1 100644 --- a/server/sonar-web/src/main/js/apps/background-tasks/components/Tasks.tsx +++ b/server/sonar-web/src/main/js/apps/background-tasks/components/Tasks.tsx @@ -17,11 +17,11 @@ * 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 { ContentCell, NumericalCell, Table, TableRow } from 'design-system'; import * as React from 'react'; +import { AppStateContext } from '../../../app/components/app-state/AppStateContext'; import withAppStateContext from '../../../app/components/app-state/withAppStateContext'; import { translate } from '../../../helpers/l10n'; -import { AppState } from '../../../types/appstate'; import { EditionKey } from '../../../types/editions'; import { Task as ITask } from '../../../types/tasks'; import Task from './Task'; @@ -29,52 +29,46 @@ import Task from './Task'; interface Props { tasks: ITask[]; component?: unknown; - loading: boolean; onCancelTask: (task: ITask) => Promise<void>; onFilterTask: (task: ITask) => void; - appState: AppState; } -export function Tasks({ tasks, component, loading, onCancelTask, onFilterTask, appState }: Props) { - const className = classNames('data zebra zebra-hover background-tasks', { - 'new-loading': loading, - }); +const COLUMN_WIDTHS = [0, 'auto', 'auto', 0, 0, 0, 0]; +const COLUMN_WIDTHS_WITH_NODES = [0, 'auto', 'auto', 0, 0, 0, 0, 0]; + +export function Tasks({ tasks, component, onCancelTask, onFilterTask }: Readonly<Props>) { + const appState = React.useContext(AppStateContext); + const isDataCenter = appState.edition === EditionKey.datacenter; return ( - <div className="boxed-group boxed-group-inner"> - <table className={className}> - <thead> - <tr> - <th>{translate('background_tasks.table.status')}</th> - <th>{translate('background_tasks.table.task')}</th> - <th>{translate('background_tasks.table.id')}</th> - <th>{translate('background_tasks.table.submitter')}</th> - {appState?.edition === EditionKey.datacenter && ( - <th>{translate('background_tasks.table.nodeName')}</th> - )} - <th> </th> - <th className="text-right">{translate('background_tasks.table.submitted')}</th> - <th className="text-right">{translate('background_tasks.table.started')}</th> - <th className="text-right">{translate('background_tasks.table.finished')}</th> - <th className="text-right">{translate('background_tasks.table.duration')}</th> - <th> </th> - </tr> - </thead> - <tbody> - {tasks.map((task, index, tasks) => ( - <Task - component={component} - key={task.id} - onCancelTask={onCancelTask} - onFilterTask={onFilterTask} - previousTask={index > 0 ? tasks[index - 1] : undefined} - task={task} - appState={appState} - /> - ))} - </tbody> - </table> - </div> + <Table + columnCount={isDataCenter ? COLUMN_WIDTHS_WITH_NODES.length : COLUMN_WIDTHS.length} + columnWidths={isDataCenter ? COLUMN_WIDTHS_WITH_NODES : COLUMN_WIDTHS} + header={ + <TableRow> + <ContentCell>{translate('background_tasks.table.status')}</ContentCell> + <ContentCell>{translate('background_tasks.table.task')}</ContentCell> + {isDataCenter && ( + <ContentCell>{translate('background_tasks.table.nodeName')}</ContentCell> + )} + <ContentCell>{translate('background_tasks.table.submitted')}</ContentCell> + <NumericalCell>{translate('background_tasks.table.started')}</NumericalCell> + <NumericalCell>{translate('background_tasks.table.finished')}</NumericalCell> + <NumericalCell>{translate('background_tasks.table.duration')}</NumericalCell> + <ContentCell /> + </TableRow> + } + > + {tasks.map((task) => ( + <Task + component={component} + key={task.id} + onCancelTask={onCancelTask} + onFilterTask={onFilterTask} + task={task} + /> + ))} + </Table> ); } diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/TypesFilter.tsx b/server/sonar-web/src/main/js/apps/background-tasks/components/TypesFilter.tsx index c5fe7304c50..40ad55b7a5e 100644 --- a/server/sonar-web/src/main/js/apps/background-tasks/components/TypesFilter.tsx +++ b/server/sonar-web/src/main/js/apps/background-tasks/components/TypesFilter.tsx @@ -17,8 +17,8 @@ * 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, LabelValueSelectOption } from 'design-system'; import * as React from 'react'; -import Select, { LabelValueSelectOption } from '../../../components/controls/Select'; import { translate } from '../../../helpers/l10n'; import { ALL_TYPES } from '../constants'; @@ -30,7 +30,7 @@ interface Props { } export default class TypesFilter extends React.PureComponent<Props> { - handleChange = ({ value }: LabelValueSelectOption) => { + handleChange = ({ value }: LabelValueSelectOption<string>) => { this.props.onChange(value); }; @@ -43,20 +43,20 @@ export default class TypesFilter extends React.PureComponent<Props> { }; }); - const allOptions: LabelValueSelectOption[] = [ + const allOptions: LabelValueSelectOption<string>[] = [ { value: ALL_TYPES, label: translate('background_task.type.ALL') }, ...options, ]; return ( - <Select + <InputSelect aria-labelledby="background-task-type-filter-label" - className="input-large" + className="sw-w-abs-200" id={id} isClearable={false} + size="medium" onChange={this.handleChange} options={allOptions} - isSearchable={false} value={allOptions.find((o) => o.value === value)} /> ); diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/Workers.tsx b/server/sonar-web/src/main/js/apps/background-tasks/components/Workers.tsx index cbde95ea0d1..88ba5ff3e78 100644 --- a/server/sonar-web/src/main/js/apps/background-tasks/components/Workers.tsx +++ b/server/sonar-web/src/main/js/apps/background-tasks/components/Workers.tsx @@ -17,14 +17,17 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { + FlagWarningIcon, + HelperHintIcon, + InteractiveIcon, + PencilIcon, + Spinner, +} from 'design-system'; import * as React from 'react'; import { getWorkers } from '../../../api/ce'; -import { colors } from '../../../app/theme'; -import { EditButton } from '../../../components/controls/buttons'; import HelpTooltip from '../../../components/controls/HelpTooltip'; import Tooltip from '../../../components/controls/Tooltip'; -import AlertWarnIcon from '../../../components/icons/AlertWarnIcon'; -import PlusCircleIcon from '../../../components/icons/PlusCircleIcon'; import { translate } from '../../../helpers/l10n'; import NoWorkersSupportPopup from './NoWorkersSupportPopup'; import WorkersForm from './WorkersForm'; @@ -33,7 +36,6 @@ interface State { canSetWorkerCount: boolean; formOpen: boolean; loading: boolean; - noSupportPopup: boolean; workerCount: number; } @@ -43,7 +45,6 @@ export default class Workers extends React.PureComponent<{}, State> { canSetWorkerCount: false, formOpen: false, loading: true, - noSupportPopup: false, workerCount: 1, }; @@ -85,57 +86,41 @@ export default class Workers extends React.PureComponent<{}, State> { this.setState({ formOpen: true }); }; - handleHelpClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => { - event.preventDefault(); - event.stopPropagation(); - this.toggleNoSupportPopup(); - }; - - toggleNoSupportPopup = (show?: boolean) => { - if (show !== undefined) { - this.setState({ noSupportPopup: show }); - } else { - this.setState((state) => ({ noSupportPopup: !state.noSupportPopup })); - } - }; - render() { const { canSetWorkerCount, formOpen, loading, workerCount } = this.state; return ( - <div className="display-flex-center"> + <div className="sw-flex sw-items-center"> {!loading && workerCount > 1 && ( <Tooltip overlay={translate('background_tasks.number_of_workers.warning')}> - <span className="display-inline-flex-center little-spacer-right"> - <AlertWarnIcon fill="#d3d3d3" /> - </span> + <div className="sw-py-1/2 sw-mr-1"> + <FlagWarningIcon /> + </div> </Tooltip> )} - <span className="text-middle"> - {translate('background_tasks.number_of_workers')} + <span id="ww">{translate('background_tasks.number_of_workers')}</span> - {loading ? ( - <i className="spinner little-spacer-left" /> - ) : ( - <strong className="little-spacer-left">{workerCount}</strong> - )} - </span> + <Spinner className="sw-ml-1" loading={loading}> + <strong aria-labelledby="ww" className="sw-ml-1"> + {workerCount} + </strong> + </Spinner> {!loading && canSetWorkerCount && ( <Tooltip overlay={translate('background_tasks.change_number_of_workers')}> - <EditButton + <InteractiveIcon + Icon={PencilIcon} aria-label={translate('background_tasks.change_number_of_workers')} - className="js-edit button-small spacer-left" + className="js-edit sw-ml-2" onClick={this.handleChangeClick} - title={translate('edit')} /> </Tooltip> )} {!loading && !canSetWorkerCount && ( - <HelpTooltip className="spacer-left" overlay={<NoWorkersSupportPopup />}> - <PlusCircleIcon fill={colors.blue} size={12} /> + <HelpTooltip className="sw-ml-2" overlay={<NoWorkersSupportPopup />}> + <HelperHintIcon /> </HelpTooltip> )} diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/WorkersForm.tsx b/server/sonar-web/src/main/js/apps/background-tasks/components/WorkersForm.tsx index 601b7d4b3a7..2c57c02b947 100644 --- a/server/sonar-web/src/main/js/apps/background-tasks/components/WorkersForm.tsx +++ b/server/sonar-web/src/main/js/apps/background-tasks/components/WorkersForm.tsx @@ -17,15 +17,13 @@ * 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, FlagMessage, InputSelect, Modal } from 'design-system'; import * as React from 'react'; import { setWorkerCount } from '../../../api/ce'; -import { ResetButtonLink, SubmitButton } from '../../../components/controls/buttons'; -import Modal from '../../../components/controls/Modal'; -import Select from '../../../components/controls/Select'; -import { Alert } from '../../../components/ui/Alert'; import { translate } from '../../../helpers/l10n'; const MAX_WORKERS = 10; +const WORKERS_FORM_ID = 'workers-form'; interface Props { onClose: (newWorkerCount?: number) => void; @@ -60,7 +58,7 @@ export default class WorkersForm extends React.PureComponent<Props, State> { this.props.onClose(); }; - handleWorkerCountChange = (option: { value: number }) => + handleWorkerCountChange = (option: { label: string; value: number }) => this.setState({ newWorkerCount: option.value }); handleSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => { @@ -82,6 +80,8 @@ export default class WorkersForm extends React.PureComponent<Props, State> { }; render() { + const { newWorkerCount, submitting } = this.state; + const options = []; for (let i = 1; i <= MAX_WORKERS; i++) { options.push({ label: String(i), value: i }); @@ -89,37 +89,33 @@ export default class WorkersForm extends React.PureComponent<Props, State> { return ( <Modal - contentLabel={translate('background_tasks.change_number_of_workers')} - onRequestClose={this.handleClose} - > - <header className="modal-head"> - <h2 id="background-task-workers-label"> - {translate('background_tasks.change_number_of_workers')} - </h2> - </header> - <form onSubmit={this.handleSubmit}> - <div className="modal-body"> - <Select - aria-labelledby="background-task-workers-label" - className="input-tiny spacer-top" + headerTitle={translate('background_tasks.change_number_of_workers')} + onClose={this.handleClose} + isOverflowVisible + body={ + <form id={WORKERS_FORM_ID} onSubmit={this.handleSubmit}> + <InputSelect + aria-label={translate('background_tasks.change_number_of_workers')} + className="sw-mt-2" isSearchable={false} onChange={this.handleWorkerCountChange} options={options} - value={options.find((o) => o.value === this.state.newWorkerCount)} + size="medium" + value={options.find((o) => o.value === newWorkerCount)} /> - <Alert className="big-spacer-top" variant="info"> + <FlagMessage className="sw-mt-4" variant="info"> {translate('background_tasks.change_number_of_workers.hint')} - </Alert> - </div> - <footer className="modal-foot"> - <div> - {this.state.submitting && <i className="spinner spacer-right" />} - <SubmitButton disabled={this.state.submitting}>{translate('save')}</SubmitButton> - <ResetButtonLink onClick={this.handleClose}>{translate('cancel')}</ResetButtonLink> - </div> - </footer> - </form> - </Modal> + </FlagMessage> + </form> + } + primaryButton={ + <ButtonPrimary disabled={submitting} type="submit" form={WORKERS_FORM_ID}> + {translate('save')} + </ButtonPrimary> + } + secondaryButtonLabel={translate('cancel')} + loading={submitting} + /> ); } } diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index b3d5b50819f..9fb52c27090 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -3646,6 +3646,7 @@ background_tasks.page.description=This page allows monitoring of the queue of ta background_tasks.currents_filter.ALL=All background_tasks.currents_filter.ONLY_CURRENTS=Only Latest Analysis +background_tasks.date_filter=Date background_tasks.date_filter.ALL=Any Date background_tasks.date_filter.TODAY=Today background_tasks.date_filter.CUSTOM=Custom @@ -3672,11 +3673,14 @@ background_tasks.show_actions=Show actions background_tasks.show_stacktrace=Show Error Details background_tasks.show_warnings=Show Warnings background_tasks.error_message=Error Message +background_tasks.error_stacktrace.title=Error Details: {project} [{type}] background_tasks.error_stacktrace=Error Details background_tasks.pending=pending background_tasks.pending_time=pending time background_tasks.pending_time.description=Pending time of the oldest background task waiting to be processed. background_tasks.failures=still failing +background_tasks.date_and_time={date} - {time} +background_tasks.submitted_by_x=By {submitter} background_tasks.number_of_workers=Number of Workers: background_tasks.number_of_workers.warning=Configuring additional workers without first vertically scaling your server could have negative performance impacts. |