diff options
Diffstat (limited to 'server/sonar-web')
6 files changed, 86 insertions, 51 deletions
diff --git a/server/sonar-web/design-system/src/components/Spinner.tsx b/server/sonar-web/design-system/src/components/Spinner.tsx index d76bbfef775..f8215fc5739 100644 --- a/server/sonar-web/design-system/src/components/Spinner.tsx +++ b/server/sonar-web/design-system/src/components/Spinner.tsx @@ -33,6 +33,13 @@ interface Props { placeholder?: boolean; } +/** @deprecated Use Spinner from Echoes instead. + * + * Some of the props have changed or been renamed: + * - ~`customSpinner`~ has been removed + * - `loading` is now `isLoading` + * - `placeholder` is now `hasPlaceholder` + */ export function Spinner(props: React.PropsWithChildren<Props>) { const intl = useIntl(); const { diff --git a/server/sonar-web/src/main/js/app/components/AlmSynchronisationWarning.tsx b/server/sonar-web/src/main/js/app/components/AlmSynchronisationWarning.tsx index ffc9f55bdaf..be2ee41fbd7 100644 --- a/server/sonar-web/src/main/js/app/components/AlmSynchronisationWarning.tsx +++ b/server/sonar-web/src/main/js/app/components/AlmSynchronisationWarning.tsx @@ -17,9 +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 styled from '@emotion/styled'; +import { Link, Spinner } from '@sonarsource/echoes-react'; import { formatDistance } from 'date-fns'; -import { CheckIcon, FlagMessage, FlagWarningIcon, Link, Spinner, themeColor } from 'design-system'; +import { CheckIcon, FlagMessage, FlagWarningIcon, themeColor } from 'design-system'; import * as React from 'react'; import { FormattedMessage } from 'react-intl'; import { translate, translateWithParameters } from '../../helpers/l10n'; @@ -27,19 +29,20 @@ import { AlmSyncStatus } from '../../types/provisioning'; import { TaskStatuses } from '../../types/tasks'; interface SynchronisationWarningProps { - short?: boolean; data: AlmSyncStatus; + short?: boolean; } interface LastSyncProps { - short?: boolean; info: AlmSyncStatus['lastSync']; + short?: boolean; } function LastSyncAlert({ info, short }: Readonly<LastSyncProps>) { if (info === undefined) { return null; } + const { finishedAt, errorMessage, status, summary, warningMessage } = info; const formattedDate = finishedAt ? formatDistance(new Date(finishedAt), new Date()) : ''; @@ -54,13 +57,14 @@ function LastSyncAlert({ info, short }: Readonly<LastSyncProps>) { <CheckIcon width={32} height={32} className="sw-mr-2" /> )} </IconWrapper> + <i> {warningMessage ? ( <FormattedMessage - id="settings.authentication.github.synchronization_successful.with_warning" defaultMessage={translate( 'settings.authentication.github.synchronization_successful.with_warning', )} + id="settings.authentication.github.synchronization_successful.with_warning" values={{ date: formattedDate, details: ( @@ -82,10 +86,10 @@ function LastSyncAlert({ info, short }: Readonly<LastSyncProps>) { <FlagMessage variant="error"> <div> <FormattedMessage - id="settings.authentication.github.synchronization_failed_short" defaultMessage={translate( 'settings.authentication.github.synchronization_failed_short', )} + id="settings.authentication.github.synchronization_failed_short" values={{ details: ( <Link className="sw-ml-2" to="/admin/settings?category=authentication&tab=github"> @@ -102,9 +106,9 @@ function LastSyncAlert({ info, short }: Readonly<LastSyncProps>) { return ( <> <FlagMessage - variant={status === TaskStatuses.Success ? 'success' : 'error'} - role="alert" aria-live="assertive" + role="alert" + variant={status === TaskStatuses.Success ? 'success' : 'error'} > <div> {status === TaskStatuses.Success ? ( @@ -113,7 +117,9 @@ function LastSyncAlert({ info, short }: Readonly<LastSyncProps>) { 'settings.authentication.github.synchronization_successful', formattedDate, )} + <br /> + {summary ?? ''} </> ) : ( @@ -124,12 +130,15 @@ function LastSyncAlert({ info, short }: Readonly<LastSyncProps>) { formattedDate, )} </div> + <br /> + {errorMessage ?? ''} </React.Fragment> )} </div> </FlagMessage> + <FlagMessage variant="warning" role="alert" aria-live="assertive"> {warningMessage} </FlagMessage> @@ -138,8 +147,8 @@ function LastSyncAlert({ info, short }: Readonly<LastSyncProps>) { } export default function AlmSynchronisationWarning({ - short, data, + short, }: Readonly<SynchronisationWarningProps>) { const loadingLabel = data.nextSync && @@ -148,11 +157,13 @@ export default function AlmSynchronisationWarning({ ? 'settings.authentication.github.synchronization_pending' : 'settings.authentication.github.synchronization_in_progress', ); + return ( <> {!short && ( <div className={data.nextSync ? 'sw-flex sw-gap-2 sw-mb-4' : ''}> - <Spinner loading={!!data.nextSync} ariaLabel={loadingLabel} /> + <Spinner ariaLabel={loadingLabel} isLoading={!!data.nextSync} /> + <div>{data.nextSync && loadingLabel}</div> </div> )} diff --git a/server/sonar-web/src/main/js/app/components/global-search/GlobalSearchShowMore.tsx b/server/sonar-web/src/main/js/app/components/global-search/GlobalSearchShowMore.tsx index d4a4abc4487..42417a21f78 100644 --- a/server/sonar-web/src/main/js/app/components/global-search/GlobalSearchShowMore.tsx +++ b/server/sonar-web/src/main/js/app/components/global-search/GlobalSearchShowMore.tsx @@ -17,8 +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 { Spinner } from '@sonarsource/echoes-react'; import classNames from 'classnames'; -import { ItemButton, Spinner } from 'design-system'; +import { ItemButton } from 'design-system'; import * as React from 'react'; import { translate } from '../../../helpers/l10n'; @@ -36,13 +38,14 @@ export default class GlobalSearchShowMore extends React.PureComponent<Props> { event.preventDefault(); event.stopPropagation(); event.currentTarget.blur(); - if (qualifier) { + + if (qualifier !== '') { this.props.onMoreClick(qualifier); } }; handleMouseEnter = (qualifier: string) => { - if (qualifier) { + if (qualifier !== '') { this.props.onSelect(`qualifier###${qualifier}`); } }; @@ -61,7 +64,7 @@ export default class GlobalSearchShowMore extends React.PureComponent<Props> { this.handleMouseEnter(qualifier); }} > - <Spinner loading={loadingMore === qualifier}>{translate('show_more')}</Spinner> + <Spinner isLoading={loadingMore === qualifier}>{translate('show_more')}</Spinner> </ItemButton> ); } diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/CrossComponentSourceViewer.tsx b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/CrossComponentSourceViewer.tsx index ff0e0c20fd1..d8cd4a408f1 100644 --- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/CrossComponentSourceViewer.tsx +++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/CrossComponentSourceViewer.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 { FlagMessage, Spinner } from 'design-system'; + +import { Spinner } from '@sonarsource/echoes-react'; +import { FlagMessage } from 'design-system'; import { findLastIndex, keyBy } from 'lodash'; import * as React from 'react'; import { getComponentForSourceViewer, getDuplications, getSources } from '../../../api/components'; diff --git a/server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx b/server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx index de49740d9ad..51005341c29 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx @@ -17,12 +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 styled from '@emotion/styled'; +import { Spinner } from '@sonarsource/echoes-react'; import { LAYOUT_FOOTER_HEIGHT, LargeCenteredLayout, PageContentFontWrapper, - Spinner, themeBorder, themeColor, } from 'design-system'; @@ -42,8 +43,10 @@ import handleRequiredAuthentication from '../../../helpers/handleRequiredAuthent import { translate } from '../../../helpers/l10n'; import { addSideBarClass, removeSideBarClass } from '../../../helpers/pages'; import { get, save } from '../../../helpers/storage'; +import { isDefined } from '../../../helpers/types'; import { AppState } from '../../../types/appstate'; import { ComponentQualifier } from '../../../types/component'; +import { MetricKey } from '../../../types/metrics'; import { RawQuery } from '../../../types/types'; import { CurrentUser, isLoggedIn } from '../../../types/users'; import { Query, hasFilterParams, parseUrlQuery } from '../query'; @@ -55,10 +58,10 @@ import PageSidebar from './PageSidebar'; import ProjectsList from './ProjectsList'; interface Props { + appState: AppState; currentUser: CurrentUser; isFavorite: boolean; location: Location; - appState: AppState; router: Router; } @@ -152,19 +155,20 @@ export class AllProjects extends React.PureComponent<Props, State> { handlePerspectiveChange = ({ view }: { view?: string }) => { const query: { view: string | undefined; - sort?: string | undefined; + sort?: string; } = { view: view === 'overall' ? undefined : view, }; if (this.state.query.view === 'leak' || view === 'leak') { - if (this.state.query.sort) { + if (isDefined(this.state.query.sort)) { const sort = parseSorting(this.state.query.sort); - if (SORTING_SWITCH[sort.sortValue]) { + if (isDefined(SORTING_SWITCH[sort.sortValue])) { query.sort = (sort.sortDesc ? '-' : '') + SORTING_SWITCH[sort.sortValue]; } } + this.props.router.push({ pathname: this.props.location.pathname, query }); } else { this.updateLocationQuery(query); @@ -214,6 +218,7 @@ export class AllProjects extends React.PureComponent<Props, State> { return searchProjects(data).then(({ facets }) => { const values = facets.find((facet) => facet.property === property)?.values ?? []; + return mapValues(keyBy(values, 'val'), 'count'); }); }; @@ -292,10 +297,10 @@ export class AllProjects extends React.PureComponent<Props, State> { handleFavorite={this.handleFavorite} isFavorite={this.props.isFavorite} isFiltered={hasFilterParams(this.state.query)} + loading={this.state.loading} + loadMore={this.fetchMoreProjects} projects={this.state.projects} query={this.state.query} - loadMore={this.fetchMoreProjects} - loading={this.state.loading} total={this.state.total} /> )} @@ -306,7 +311,7 @@ export class AllProjects extends React.PureComponent<Props, State> { render() { return ( <StyledWrapper id="projects-page"> - <Suggestions suggestions="projects" /> + <Suggestions suggestions={MetricKey.projects} /> <Helmet defer={false} title={translate('projects.page')} /> <h1 className="sw-sr-only">{translate('projects.page')}</h1> @@ -338,27 +343,27 @@ function getStorageOptions() { view?: string; } = {}; - if (get(LS_PROJECTS_SORT)) { - options.sort = get(LS_PROJECTS_SORT) || undefined; + if (get(LS_PROJECTS_SORT) !== null) { + options.sort = get(LS_PROJECTS_SORT) ?? undefined; } - if (get(LS_PROJECTS_VIEW)) { - options.view = get(LS_PROJECTS_VIEW) || undefined; + if (get(LS_PROJECTS_VIEW) !== null) { + options.view = get(LS_PROJECTS_VIEW) ?? undefined; } return options; } -function SetSearchParamsWrapper(props: Props) { +function SetSearchParamsWrapper(props: Readonly<Props>) { const [searchParams, setSearchParams] = useSearchParams(); const savedOptions = getStorageOptions(); React.useEffect( () => { - const hasViewParams = searchParams.get('sort') || searchParams.get('view'); - const hasSavedOptions = savedOptions.sort || savedOptions.view; + const hasViewParams = searchParams.get('sort') ?? searchParams.get('view'); + const hasSavedOptions = savedOptions.sort ?? savedOptions.view; - if (!hasViewParams && hasSavedOptions) { + if (!isDefined(hasViewParams) && isDefined(hasSavedOptions)) { setSearchParams(savedOptions); } }, diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/ConfigurationForm.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/ConfigurationForm.tsx index 9a7d6b545f8..2cab4f65783 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/authentication/ConfigurationForm.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/ConfigurationForm.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 { ButtonPrimary, FlagMessage, Modal, Spinner } from 'design-system'; + +import { Spinner } from '@sonarsource/echoes-react'; +import { ButtonPrimary, FlagMessage, Modal } from 'design-system'; import { keyBy } from 'lodash'; import * as React from 'react'; import { FormattedMessage } from 'react-intl'; @@ -34,16 +36,16 @@ import { SettingValue } from './hook/useConfiguration'; import { isAllowToSignUpEnabled, isOrganizationListEmpty } from './hook/useGithubConfiguration'; interface Props { - create: boolean; - loading: boolean; - values: Dict<SettingValue>; - setNewValue: (key: string, value: string | boolean) => void; canBeSave: boolean; - onClose: () => void; - tab: AuthenticationTabs; + create: boolean; excludedField: string[]; hasLegacyConfiguration?: boolean; + loading: boolean; + onClose: () => void; provisioningStatus?: ProvisioningType; + setNewValue: (key: string, value: string | boolean) => void; + tab: AuthenticationTabs; + values: Dict<SettingValue>; } interface ErrorValue { @@ -53,15 +55,15 @@ interface ErrorValue { export default function ConfigurationForm(props: Readonly<Props>) { const { - create, - loading, - values, - setNewValue, canBeSave, - tab, + create, excludedField, hasLegacyConfiguration, + loading, provisioningStatus, + setNewValue, + tab, + values, } = props; const [errors, setErrors] = React.useState<Dict<ErrorValue>>({}); const [showConfirmModal, setShowConfirmModal] = React.useState(false); @@ -87,12 +89,14 @@ export default function ConfigurationForm(props: Readonly<Props>) { const errors = Object.values(values) .filter((v) => v.newValue === undefined && v.value === undefined && v.mandatory) .map((v) => ({ key: v.key, message: translate('field_required') })); + setErrors(keyBy(errors, 'key')); } }; const onSave = async () => { const data = await changeConfig(Object.values(values)); + const errors = data .filter(({ success }) => !success) .map(({ key }) => ({ key, message: translate('default_save_field_error_message') })); @@ -110,15 +114,15 @@ export default function ConfigurationForm(props: Readonly<Props>) { const formBody = ( <form id={FORM_ID} onSubmit={handleSubmit}> - <Spinner loading={loading} ariaLabel={translate('settings.authentication.form.loading')}> + <Spinner ariaLabel={translate('settings.authentication.form.loading')} isLoading={loading}> <FlagMessage className="sw-w-full sw-mb-8" variant={hasLegacyConfiguration ? 'warning' : 'info'} > <span> <FormattedMessage - id={`settings.authentication.${helpMessage}`} defaultMessage={translate(`settings.authentication.${helpMessage}`)} + id={`settings.authentication.${helpMessage}`} values={{ link: ( <DocumentationLink @@ -131,21 +135,23 @@ export default function ConfigurationForm(props: Readonly<Props>) { /> </span> </FlagMessage> + {Object.values(values).map((val) => { if (excludedField.includes(val.key)) { return null; } const isSet = hasLegacyConfiguration ? false : !val.isNotSet; + return ( <div key={val.key} className="sw-mb-8"> <AuthenticationFormField - settingValue={values[val.key]?.newValue ?? values[val.key]?.value} definition={val.definition} + error={errors[val.key]?.message} + isNotSet={!isSet} mandatory={val.mandatory} onFieldChange={setNewValue} - isNotSet={!isSet} - error={errors[val.key]?.message} + settingValue={values[val.key]?.newValue ?? values[val.key]?.value} /> </div> ); @@ -157,23 +163,24 @@ export default function ConfigurationForm(props: Readonly<Props>) { return ( <> <Modal + body={formBody} headerTitle={header} isScrollable onClose={props.onClose} - body={formBody} primaryButton={ <ButtonPrimary form={FORM_ID} type="submit" autoFocus disabled={!canBeSave}> {translate('settings.almintegration.form.save')} - <Spinner className="sw-ml-2" loading={loading} /> + + <Spinner className="sw-ml-2" isLoading={loading} /> </ButtonPrimary> } /> {showConfirmModal && ( <GitHubConfirmModal - onConfirm={onSave} onClose={() => setShowConfirmModal(false)} - values={values} + onConfirm={onSave} provisioningStatus={provisioningStatus ?? ProvisioningType.jit} + values={values} /> )} </> |