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