aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web
diff options
context:
space:
mode:
Diffstat (limited to 'server/sonar-web')
-rw-r--r--server/sonar-web/design-system/src/components/Spinner.tsx7
-rw-r--r--server/sonar-web/src/main/js/app/components/AlmSynchronisationWarning.tsx29
-rw-r--r--server/sonar-web/src/main/js/app/components/global-search/GlobalSearchShowMore.tsx11
-rw-r--r--server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/CrossComponentSourceViewer.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx37
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/authentication/ConfigurationForm.tsx49
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}
/>
)}
</>