Browse Source

SONAR-21298 Showcase Echoes' Spinner component in a few places

tags/10.5.0.89998
David Cho-Lerat 2 months ago
parent
commit
0c7599320b

+ 7
- 0
server/sonar-web/design-system/src/components/Spinner.tsx View File

@@ -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 {

+ 20
- 9
server/sonar-web/src/main/js/app/components/AlmSynchronisationWarning.tsx View File

@@ -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>
)}

+ 7
- 4
server/sonar-web/src/main/js/app/components/global-search/GlobalSearchShowMore.tsx View File

@@ -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>
);
}

+ 3
- 1
server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/CrossComponentSourceViewer.tsx View File

@@ -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';

+ 21
- 16
server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx View File

@@ -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);
}
},

+ 28
- 21
server/sonar-web/src/main/js/apps/settings/components/authentication/ConfigurationForm.tsx View File

@@ -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}
/>
)}
</>

Loading…
Cancel
Save