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

placeholder?: boolean; 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>) { export function Spinner(props: React.PropsWithChildren<Props>) {
const intl = useIntl(); const intl = useIntl();
const { const {

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

* along with this program; if not, write to the Free Software Foundation, * along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/ */

import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { Link, Spinner } from '@sonarsource/echoes-react';
import { formatDistance } from 'date-fns'; 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 * as React from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { translate, translateWithParameters } from '../../helpers/l10n'; import { translate, translateWithParameters } from '../../helpers/l10n';
import { TaskStatuses } from '../../types/tasks'; import { TaskStatuses } from '../../types/tasks';


interface SynchronisationWarningProps { interface SynchronisationWarningProps {
short?: boolean;
data: AlmSyncStatus; data: AlmSyncStatus;
short?: boolean;
} }


interface LastSyncProps { interface LastSyncProps {
short?: boolean;
info: AlmSyncStatus['lastSync']; info: AlmSyncStatus['lastSync'];
short?: boolean;
} }


function LastSyncAlert({ info, short }: Readonly<LastSyncProps>) { function LastSyncAlert({ info, short }: Readonly<LastSyncProps>) {
if (info === undefined) { if (info === undefined) {
return null; return null;
} }

const { finishedAt, errorMessage, status, summary, warningMessage } = info; const { finishedAt, errorMessage, status, summary, warningMessage } = info;


const formattedDate = finishedAt ? formatDistance(new Date(finishedAt), new Date()) : ''; const formattedDate = finishedAt ? formatDistance(new Date(finishedAt), new Date()) : '';
<CheckIcon width={32} height={32} className="sw-mr-2" /> <CheckIcon width={32} height={32} className="sw-mr-2" />
)} )}
</IconWrapper> </IconWrapper>

<i> <i>
{warningMessage ? ( {warningMessage ? (
<FormattedMessage <FormattedMessage
id="settings.authentication.github.synchronization_successful.with_warning"
defaultMessage={translate( defaultMessage={translate(
'settings.authentication.github.synchronization_successful.with_warning', 'settings.authentication.github.synchronization_successful.with_warning',
)} )}
id="settings.authentication.github.synchronization_successful.with_warning"
values={{ values={{
date: formattedDate, date: formattedDate,
details: ( details: (
<FlagMessage variant="error"> <FlagMessage variant="error">
<div> <div>
<FormattedMessage <FormattedMessage
id="settings.authentication.github.synchronization_failed_short"
defaultMessage={translate( defaultMessage={translate(
'settings.authentication.github.synchronization_failed_short', 'settings.authentication.github.synchronization_failed_short',
)} )}
id="settings.authentication.github.synchronization_failed_short"
values={{ values={{
details: ( details: (
<Link className="sw-ml-2" to="/admin/settings?category=authentication&tab=github"> <Link className="sw-ml-2" to="/admin/settings?category=authentication&tab=github">
return ( return (
<> <>
<FlagMessage <FlagMessage
variant={status === TaskStatuses.Success ? 'success' : 'error'}
role="alert"
aria-live="assertive" aria-live="assertive"
role="alert"
variant={status === TaskStatuses.Success ? 'success' : 'error'}
> >
<div> <div>
{status === TaskStatuses.Success ? ( {status === TaskStatuses.Success ? (
'settings.authentication.github.synchronization_successful', 'settings.authentication.github.synchronization_successful',
formattedDate, formattedDate,
)} )}

<br /> <br />

{summary ?? ''} {summary ?? ''}
</> </>
) : ( ) : (
formattedDate, formattedDate,
)} )}
</div> </div>

<br /> <br />

{errorMessage ?? ''} {errorMessage ?? ''}
</React.Fragment> </React.Fragment>
)} )}
</div> </div>
</FlagMessage> </FlagMessage>

<FlagMessage variant="warning" role="alert" aria-live="assertive"> <FlagMessage variant="warning" role="alert" aria-live="assertive">
{warningMessage} {warningMessage}
</FlagMessage> </FlagMessage>
} }


export default function AlmSynchronisationWarning({ export default function AlmSynchronisationWarning({
short,
data, data,
short,
}: Readonly<SynchronisationWarningProps>) { }: Readonly<SynchronisationWarningProps>) {
const loadingLabel = const loadingLabel =
data.nextSync && data.nextSync &&
? 'settings.authentication.github.synchronization_pending' ? 'settings.authentication.github.synchronization_pending'
: 'settings.authentication.github.synchronization_in_progress', : 'settings.authentication.github.synchronization_in_progress',
); );

return ( return (
<> <>
{!short && ( {!short && (
<div className={data.nextSync ? 'sw-flex sw-gap-2 sw-mb-4' : ''}> <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>{data.nextSync && loadingLabel}</div>
</div> </div>
)} )}

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

* along with this program; if not, write to the Free Software Foundation, * along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/ */

import { Spinner } from '@sonarsource/echoes-react';
import classNames from 'classnames'; import classNames from 'classnames';
import { ItemButton, Spinner } from 'design-system';
import { ItemButton } from 'design-system';
import * as React from 'react'; import * as React from 'react';
import { translate } from '../../../helpers/l10n'; import { translate } from '../../../helpers/l10n';


event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
event.currentTarget.blur(); event.currentTarget.blur();
if (qualifier) {

if (qualifier !== '') {
this.props.onMoreClick(qualifier); this.props.onMoreClick(qualifier);
} }
}; };


handleMouseEnter = (qualifier: string) => { handleMouseEnter = (qualifier: string) => {
if (qualifier) {
if (qualifier !== '') {
this.props.onSelect(`qualifier###${qualifier}`); this.props.onSelect(`qualifier###${qualifier}`);
} }
}; };
this.handleMouseEnter(qualifier); this.handleMouseEnter(qualifier);
}} }}
> >
<Spinner loading={loadingMore === qualifier}>{translate('show_more')}</Spinner>
<Spinner isLoading={loadingMore === qualifier}>{translate('show_more')}</Spinner>
</ItemButton> </ItemButton>
); );
} }

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

* along with this program; if not, write to the Free Software Foundation, * along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * 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 { findLastIndex, keyBy } from 'lodash';
import * as React from 'react'; import * as React from 'react';
import { getComponentForSourceViewer, getDuplications, getSources } from '../../../api/components'; import { getComponentForSourceViewer, getDuplications, getSources } from '../../../api/components';

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

* along with this program; if not, write to the Free Software Foundation, * along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/ */

import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { Spinner } from '@sonarsource/echoes-react';
import { import {
LAYOUT_FOOTER_HEIGHT, LAYOUT_FOOTER_HEIGHT,
LargeCenteredLayout, LargeCenteredLayout,
PageContentFontWrapper, PageContentFontWrapper,
Spinner,
themeBorder, themeBorder,
themeColor, themeColor,
} from 'design-system'; } from 'design-system';
import { translate } from '../../../helpers/l10n'; import { translate } from '../../../helpers/l10n';
import { addSideBarClass, removeSideBarClass } from '../../../helpers/pages'; import { addSideBarClass, removeSideBarClass } from '../../../helpers/pages';
import { get, save } from '../../../helpers/storage'; import { get, save } from '../../../helpers/storage';
import { isDefined } from '../../../helpers/types';
import { AppState } from '../../../types/appstate'; import { AppState } from '../../../types/appstate';
import { ComponentQualifier } from '../../../types/component'; import { ComponentQualifier } from '../../../types/component';
import { MetricKey } from '../../../types/metrics';
import { RawQuery } from '../../../types/types'; import { RawQuery } from '../../../types/types';
import { CurrentUser, isLoggedIn } from '../../../types/users'; import { CurrentUser, isLoggedIn } from '../../../types/users';
import { Query, hasFilterParams, parseUrlQuery } from '../query'; import { Query, hasFilterParams, parseUrlQuery } from '../query';
import ProjectsList from './ProjectsList'; import ProjectsList from './ProjectsList';


interface Props { interface Props {
appState: AppState;
currentUser: CurrentUser; currentUser: CurrentUser;
isFavorite: boolean; isFavorite: boolean;
location: Location; location: Location;
appState: AppState;
router: Router; router: Router;
} }


handlePerspectiveChange = ({ view }: { view?: string }) => { handlePerspectiveChange = ({ view }: { view?: string }) => {
const query: { const query: {
view: string | undefined; view: string | undefined;
sort?: string | undefined;
sort?: string;
} = { } = {
view: view === 'overall' ? undefined : view, view: view === 'overall' ? undefined : view,
}; };


if (this.state.query.view === 'leak' || view === 'leak') { 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); 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]; query.sort = (sort.sortDesc ? '-' : '') + SORTING_SWITCH[sort.sortValue];
} }
} }

this.props.router.push({ pathname: this.props.location.pathname, query }); this.props.router.push({ pathname: this.props.location.pathname, query });
} else { } else {
this.updateLocationQuery(query); this.updateLocationQuery(query);


return searchProjects(data).then(({ facets }) => { return searchProjects(data).then(({ facets }) => {
const values = facets.find((facet) => facet.property === property)?.values ?? []; const values = facets.find((facet) => facet.property === property)?.values ?? [];

return mapValues(keyBy(values, 'val'), 'count'); return mapValues(keyBy(values, 'val'), 'count');
}); });
}; };
handleFavorite={this.handleFavorite} handleFavorite={this.handleFavorite}
isFavorite={this.props.isFavorite} isFavorite={this.props.isFavorite}
isFiltered={hasFilterParams(this.state.query)} isFiltered={hasFilterParams(this.state.query)}
loading={this.state.loading}
loadMore={this.fetchMoreProjects}
projects={this.state.projects} projects={this.state.projects}
query={this.state.query} query={this.state.query}
loadMore={this.fetchMoreProjects}
loading={this.state.loading}
total={this.state.total} total={this.state.total}
/> />
)} )}
render() { render() {
return ( return (
<StyledWrapper id="projects-page"> <StyledWrapper id="projects-page">
<Suggestions suggestions="projects" />
<Suggestions suggestions={MetricKey.projects} />
<Helmet defer={false} title={translate('projects.page')} /> <Helmet defer={false} title={translate('projects.page')} />


<h1 className="sw-sr-only">{translate('projects.page')}</h1> <h1 className="sw-sr-only">{translate('projects.page')}</h1>
view?: string; 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; return options;
} }


function SetSearchParamsWrapper(props: Props) {
function SetSearchParamsWrapper(props: Readonly<Props>) {
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
const savedOptions = getStorageOptions(); const savedOptions = getStorageOptions();


React.useEffect( 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); setSearchParams(savedOptions);
} }
}, },

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

* along with this program; if not, write to the Free Software Foundation, * along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * 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 { keyBy } from 'lodash';
import * as React from 'react'; import * as React from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { isAllowToSignUpEnabled, isOrganizationListEmpty } from './hook/useGithubConfiguration'; import { isAllowToSignUpEnabled, isOrganizationListEmpty } from './hook/useGithubConfiguration';


interface Props { interface Props {
create: boolean;
loading: boolean;
values: Dict<SettingValue>;
setNewValue: (key: string, value: string | boolean) => void;
canBeSave: boolean; canBeSave: boolean;
onClose: () => void;
tab: AuthenticationTabs;
create: boolean;
excludedField: string[]; excludedField: string[];
hasLegacyConfiguration?: boolean; hasLegacyConfiguration?: boolean;
loading: boolean;
onClose: () => void;
provisioningStatus?: ProvisioningType; provisioningStatus?: ProvisioningType;
setNewValue: (key: string, value: string | boolean) => void;
tab: AuthenticationTabs;
values: Dict<SettingValue>;
} }


interface ErrorValue { interface ErrorValue {


export default function ConfigurationForm(props: Readonly<Props>) { export default function ConfigurationForm(props: Readonly<Props>) {
const { const {
create,
loading,
values,
setNewValue,
canBeSave, canBeSave,
tab,
create,
excludedField, excludedField,
hasLegacyConfiguration, hasLegacyConfiguration,
loading,
provisioningStatus, provisioningStatus,
setNewValue,
tab,
values,
} = props; } = props;
const [errors, setErrors] = React.useState<Dict<ErrorValue>>({}); const [errors, setErrors] = React.useState<Dict<ErrorValue>>({});
const [showConfirmModal, setShowConfirmModal] = React.useState(false); const [showConfirmModal, setShowConfirmModal] = React.useState(false);
const errors = Object.values(values) const errors = Object.values(values)
.filter((v) => v.newValue === undefined && v.value === undefined && v.mandatory) .filter((v) => v.newValue === undefined && v.value === undefined && v.mandatory)
.map((v) => ({ key: v.key, message: translate('field_required') })); .map((v) => ({ key: v.key, message: translate('field_required') }));

setErrors(keyBy(errors, 'key')); setErrors(keyBy(errors, 'key'));
} }
}; };


const onSave = async () => { const onSave = async () => {
const data = await changeConfig(Object.values(values)); const data = await changeConfig(Object.values(values));

const errors = data const errors = data
.filter(({ success }) => !success) .filter(({ success }) => !success)
.map(({ key }) => ({ key, message: translate('default_save_field_error_message') })); .map(({ key }) => ({ key, message: translate('default_save_field_error_message') }));


const formBody = ( const formBody = (
<form id={FORM_ID} onSubmit={handleSubmit}> <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 <FlagMessage
className="sw-w-full sw-mb-8" className="sw-w-full sw-mb-8"
variant={hasLegacyConfiguration ? 'warning' : 'info'} variant={hasLegacyConfiguration ? 'warning' : 'info'}
> >
<span> <span>
<FormattedMessage <FormattedMessage
id={`settings.authentication.${helpMessage}`}
defaultMessage={translate(`settings.authentication.${helpMessage}`)} defaultMessage={translate(`settings.authentication.${helpMessage}`)}
id={`settings.authentication.${helpMessage}`}
values={{ values={{
link: ( link: (
<DocumentationLink <DocumentationLink
/> />
</span> </span>
</FlagMessage> </FlagMessage>

{Object.values(values).map((val) => { {Object.values(values).map((val) => {
if (excludedField.includes(val.key)) { if (excludedField.includes(val.key)) {
return null; return null;
} }


const isSet = hasLegacyConfiguration ? false : !val.isNotSet; const isSet = hasLegacyConfiguration ? false : !val.isNotSet;

return ( return (
<div key={val.key} className="sw-mb-8"> <div key={val.key} className="sw-mb-8">
<AuthenticationFormField <AuthenticationFormField
settingValue={values[val.key]?.newValue ?? values[val.key]?.value}
definition={val.definition} definition={val.definition}
error={errors[val.key]?.message}
isNotSet={!isSet}
mandatory={val.mandatory} mandatory={val.mandatory}
onFieldChange={setNewValue} onFieldChange={setNewValue}
isNotSet={!isSet}
error={errors[val.key]?.message}
settingValue={values[val.key]?.newValue ?? values[val.key]?.value}
/> />
</div> </div>
); );
return ( return (
<> <>
<Modal <Modal
body={formBody}
headerTitle={header} headerTitle={header}
isScrollable isScrollable
onClose={props.onClose} onClose={props.onClose}
body={formBody}
primaryButton={ primaryButton={
<ButtonPrimary form={FORM_ID} type="submit" autoFocus disabled={!canBeSave}> <ButtonPrimary form={FORM_ID} type="submit" autoFocus disabled={!canBeSave}>
{translate('settings.almintegration.form.save')} {translate('settings.almintegration.form.save')}
<Spinner className="sw-ml-2" loading={loading} />

<Spinner className="sw-ml-2" isLoading={loading} />
</ButtonPrimary> </ButtonPrimary>
} }
/> />
{showConfirmModal && ( {showConfirmModal && (
<GitHubConfirmModal <GitHubConfirmModal
onConfirm={onSave}
onClose={() => setShowConfirmModal(false)} onClose={() => setShowConfirmModal(false)}
values={values}
onConfirm={onSave}
provisioningStatus={provisioningStatus ?? ProvisioningType.jit} provisioningStatus={provisioningStatus ?? ProvisioningType.jit}
values={values}
/> />
)} )}
</> </>

Loading…
Cancel
Save