--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * 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 classNames from 'classnames';
+import { ReactNode } from 'react';
+import tw from 'twin.macro';
+import { themeBorder, themeColor } from '../helpers/theme';
+import { BareButton } from './buttons';
+
+interface Props {
+ className?: string;
+ description: ReactNode;
+ image: ReactNode;
+ onClick: () => void;
+ selected: boolean;
+}
+
+export function IllustratedSelectionCard(props: Props) {
+ const { className, description, image, onClick, selected } = props;
+
+ return (
+ <StyledSelectionCard className={classNames(className, { selected })} onClick={onClick}>
+ <ImageContainer>{image}</ImageContainer>
+ <DescriptionContainer>
+ <Note>{description}</Note>
+ </DescriptionContainer>
+ </StyledSelectionCard>
+ );
+}
+
+const Note = styled.span`
+ color: ${themeColor('pageContentLight')};
+
+ ${tw`sw-body-sm`}
+`;
+
+const ImageContainer = styled.div`
+ min-height: 116px;
+ flex: 1;
+ background: ${themeColor('backgroundPrimary')};
+ ${tw`sw-flex`}
+ ${tw`sw-justify-center sw-items-center`}
+ ${tw`sw-rounded-t-1`}
+`;
+
+const DescriptionContainer = styled.div`
+ background: ${themeColor('backgroundSecondary')};
+ border-top: ${themeBorder()};
+ ${tw`sw-rounded-b-1`}
+ ${tw`sw-p-4`}
+`;
+
+export const StyledSelectionCard = styled(BareButton)`
+ ${tw`sw-flex`}
+ ${tw`sw-flex-col`}
+ ${tw`sw-rounded-1`};
+
+ min-width: 146px;
+ border: ${themeBorder('default')};
+ transition: border 0.3s ease;
+
+ &:hover,
+ &:focus,
+ &:active {
+ border: ${themeBorder('default', 'primary')};
+ }
+
+ &.selected {
+ border: ${themeBorder('default', 'primary')};
+ }
+`;
'aria-label' | 'autoFocus' | 'id' | 'name' | 'style' | 'title' | 'type'
>;
-interface Props extends AllowedRadioButtonAttributes {
+interface PropsBase extends AllowedRadioButtonAttributes {
checked: boolean;
children?: React.ReactNode;
className?: string;
disabled?: boolean;
- onCheck: (value: string) => void;
- value: string;
}
+type Props =
+ | ({ onCheck: (value: string) => void; value: string } & PropsBase)
+ | ({ onCheck: () => void; value: never } & PropsBase);
+
export function RadioButton({
checked,
children,
disabled?: boolean;
label?: string;
onChange: (value: T) => void;
- options: Array<ToggleButtonsOption<T>>;
+ options: ReadonlyArray<ToggleButtonsOption<T>>;
role?: 'radiogroup' | 'tablist';
value?: T;
}
export { Histogram } from './Histogram';
export { HotspotRating } from './HotspotRating';
export * from './HtmlFormatter';
+export { IllustratedSelectionCard } from './IlllustredSelectionCard';
export * from './InputField';
export * from './InputMultiSelect';
export { InputSearch } from './InputSearch';
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { throwGlobalError } from '../helpers/error';
-import { getJSON, postJSON } from '../helpers/request';
+import { getJSON, post } from '../helpers/request';
export function getProjectBadgesToken(project: string) {
return getJSON('/api/project_badges/token', { project })
}
export function renewProjectBadgesToken(project: string) {
- return postJSON('/api/project_badges/renew_token', { project }).catch(throwGlobalError);
+ return post('/api/project_badges/renew_token', { project }).catch(throwGlobalError);
}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import classNames from 'classnames';
-import * as React from 'react';
-import { Button } from '../../../components/controls/buttons';
-import { translate } from '../../../helpers/l10n';
-import { BadgeType } from './utils';
-
-interface Props {
- onClick: (type: BadgeType) => void;
- selected: boolean;
- type: BadgeType;
- url: string;
-}
-
-export default class BadgeButton extends React.PureComponent<Props> {
- handleClick = () => {
- this.props.onClick(this.props.type);
- };
-
- render() {
- const { selected, type, url } = this.props;
- const width = type !== BadgeType.measure ? '128px' : undefined;
- return (
- <Button className={classNames('badge-button', { selected })} onClick={this.handleClick}>
- <img alt={translate('overview.badges', type, 'alt')} src={url} width={width} />
- </Button>
- );
- }
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-import classNames from 'classnames';
-import * as React from 'react';
-import { fetchWebApi } from '../../../api/web-api';
-import withMetricsContext from '../../../app/components/metrics/withMetricsContext';
-import Select from '../../../components/controls/Select';
-import { getLocalizedMetricName, translate } from '../../../helpers/l10n';
-import { Dict, Metric } from '../../../types/types';
-import { BadgeFormats, BadgeOptions, BadgeType } from './utils';
-
-interface Props {
- className?: string;
- metrics: Dict<Metric>;
- options: BadgeOptions;
- type: BadgeType;
- updateOptions: (options: Partial<BadgeOptions>) => void;
-}
-
-interface State {
- badgeMetrics: string[];
-}
-
-export class BadgeParams extends React.PureComponent<Props> {
- mounted = false;
-
- state: State = { badgeMetrics: [] };
-
- componentDidMount() {
- this.mounted = true;
- this.fetchBadgeMetrics();
- }
-
- componentWillUnmount() {
- this.mounted = false;
- }
-
- fetchBadgeMetrics() {
- fetchWebApi(false).then(
- (webservices) => {
- if (this.mounted) {
- const domain = webservices.find((d) => d.path === 'api/project_badges');
- const ws = domain && domain.actions.find((w) => w.key === 'measure');
- const param = ws && ws.params && ws.params.find((p) => p.key === 'metric');
- if (param && param.possibleValues) {
- this.setState({ badgeMetrics: param.possibleValues });
- }
- }
- },
- () => {}
- );
- }
-
- getColorOptions = () => {
- return ['white', 'black', 'orange'].map((color) => ({
- label: translate('overview.badges.options.colors', color),
- value: color,
- }));
- };
-
- getFormatOptions = () => {
- return ['md', 'url'].map((format) => ({
- label: translate('overview.badges.options.formats', format),
- value: format as BadgeFormats,
- }));
- };
-
- getMetricOptions = () => {
- return this.state.badgeMetrics.map((key) => {
- const metric = this.props.metrics[key];
- return {
- value: key,
- label: metric ? getLocalizedMetricName(metric) : key,
- };
- });
- };
-
- handleFormatChange = ({ value }: { value: BadgeFormats }) => {
- this.props.updateOptions({ format: value });
- };
-
- handleMetricChange = ({ value }: { value: string }) => {
- this.props.updateOptions({ metric: value });
- };
-
- renderBadgeType = (type: BadgeType, options: BadgeOptions) => {
- if (type === BadgeType.measure) {
- const metricOptions = this.getMetricOptions();
- return (
- <>
- <label className="spacer-right" htmlFor="badge-metric">
- {translate('overview.badges.metric')}:
- </label>
- <Select
- className="input-medium it__metric-badge-select"
- inputId="badge-metric"
- isSearchable={false}
- onChange={this.handleMetricChange}
- options={metricOptions}
- value={metricOptions.find((o) => o.value === options.metric)}
- />
- </>
- );
- }
- return null;
- };
-
- render() {
- const { className, options, type } = this.props;
- const formatOptions = this.getFormatOptions();
- return (
- <div className={className}>
- {this.renderBadgeType(type, options)}
-
- <label
- className={classNames('spacer-right', {
- 'spacer-top': type !== BadgeType.qualityGate,
- })}
- htmlFor="badge-format"
- >
- {translate('format')}:
- </label>
- <Select
- className="input-medium"
- inputId="badge-format"
- isSearchable={false}
- onChange={this.handleFormatChange}
- options={formatOptions}
- value={formatOptions.find((o) => o.value === options.format)}
- defaultValue={formatOptions.find((o) => o.value === 'md')}
- />
- </div>
- );
- }
-}
-
-export default withMetricsContext(BadgeParams);
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import {
+ BasicSeparator,
+ ButtonSecondary,
+ CodeSnippet,
+ DeferredSpinner,
+ FlagMessage,
+ FormField,
+ IllustratedSelectionCard,
+ InputSelect,
+ SubTitle,
+ ToggleButton,
+} from 'design-system';
+import { isEmpty } from 'lodash';
import * as React from 'react';
-import { getProjectBadgesToken, renewProjectBadgesToken } from '../../../api/project-badges';
-import CodeSnippet from '../../../components/common/CodeSnippet';
-import { Button } from '../../../components/controls/buttons';
-import { Alert } from '../../../components/ui/Alert';
-import DeferredSpinner from '../../../components/ui/DeferredSpinner';
+import { useState } from 'react';
import { getBranchLikeQuery } from '../../../helpers/branch-like';
import { translate } from '../../../helpers/l10n';
import { BranchLike } from '../../../types/branch-like';
import { MetricKey } from '../../../types/metrics';
import { Component } from '../../../types/types';
-import BadgeButton from './BadgeButton';
-import BadgeParams from './BadgeParams';
-import './styles.css';
-import { BadgeOptions, BadgeType, getBadgeSnippet, getBadgeUrl } from './utils';
+import {
+ useBadgeMetricsQuery,
+ useBadgeTokenQuery,
+ useRenewBagdeTokenMutation,
+} from '../query/badges';
+import { BadgeFormats, BadgeOptions, BadgeType, getBadgeSnippet, getBadgeUrl } from './utils';
-interface Props {
+export interface ProjectBadgesProps {
branchLike?: BranchLike;
component: Component;
}
-interface State {
- isRenewing: boolean;
- token: string;
- selectedType: BadgeType;
- badgeOptions: BadgeOptions;
-}
+export default function ProjectBadges(props: ProjectBadgesProps) {
+ const {
+ branchLike,
+ component: { key: project, qualifier, configuration },
+ } = props;
+ const [selectedType, setSelectedType] = useState(BadgeType.measure);
+ const [metricOptions, setMetricOptions] = useState(MetricKey.alert_status);
+ const [formatOption, setFormatOption] = useState<BadgeFormats>('md');
+ const {
+ data: token,
+ isLoading: isLoadingToken,
+ isFetching: isFetchingToken,
+ } = useBadgeTokenQuery(project);
+ const { data: metricsOptions, isLoading: isLoadingMetrics } = useBadgeMetricsQuery();
+ const { mutate: renewToken, isLoading: isRenewing } = useRenewBagdeTokenMutation();
+ const isLoading = isLoadingMetrics || isLoadingToken || isRenewing;
-export default class ProjectBadges extends React.PureComponent<Props, State> {
- mounted = false;
- headingNodeRef = React.createRef<HTMLHeadingElement>();
- state: State = {
- isRenewing: false,
- token: '',
- selectedType: BadgeType.measure,
- badgeOptions: { metric: MetricKey.alert_status },
+ const handleSelectBadge = (selectedType: BadgeType) => {
+ setSelectedType(selectedType);
};
- componentDidMount() {
- this.mounted = true;
- this.fetchToken();
- if (this.headingNodeRef.current) {
- this.headingNodeRef.current.focus();
- }
- }
-
- componentWillUnmount() {
- this.mounted = false;
- }
-
- async fetchToken() {
- const {
- component: { key },
- } = this.props;
- const token = await getProjectBadgesToken(key).catch(() => '');
- if (this.mounted) {
- this.setState({ token });
- }
- }
+ const formatOptions = [
+ {
+ label: translate('overview.badges.options.formats.md'),
+ value: 'md',
+ },
+ {
+ label: translate('overview.badges.options.formats.url'),
+ value: 'url',
+ },
+ ] as const;
- handleSelectBadge = (selectedType: BadgeType) => {
- this.setState({ selectedType });
+ const fullBadgeOptions: BadgeOptions = {
+ project,
+ metric: metricOptions,
+ format: formatOption,
+ ...getBranchLikeQuery(branchLike),
};
+ const canRenew = configuration?.showSettings;
- handleUpdateOptions = (options: Partial<BadgeOptions>) => {
- this.setState((state) => ({
- badgeOptions: { ...state.badgeOptions, ...options },
- }));
- };
+ return (
+ <div>
+ <SubTitle>{translate('overview.badges.get_badge')}</SubTitle>
+ <p className="big-spacer-bottom">{translate('overview.badges.description', qualifier)}</p>
- handleRenew = async () => {
- const {
- component: { key },
- } = this.props;
+ <DeferredSpinner loading={isLoading || isEmpty(token)}>
+ <div className="sw-flex sw-space-x-4 sw-mb-4">
+ <IllustratedSelectionCard
+ className="sw-w-abs-300 it__badge-button"
+ onClick={() => handleSelectBadge(BadgeType.measure)}
+ selected={BadgeType.measure === selectedType}
+ image={
+ <img
+ alt={translate('overview.badges', BadgeType.measure, 'alt')}
+ src={getBadgeUrl(BadgeType.measure, fullBadgeOptions, token)}
+ />
+ }
+ description={translate('overview.badges', BadgeType.measure, 'description', qualifier)}
+ />
+ <IllustratedSelectionCard
+ className="sw-w-abs-300 it__badge-button"
+ onClick={() => handleSelectBadge(BadgeType.qualityGate)}
+ selected={BadgeType.qualityGate === selectedType}
+ image={
+ <img
+ alt={translate('overview.badges', BadgeType.qualityGate, 'alt')}
+ src={getBadgeUrl(BadgeType.qualityGate, fullBadgeOptions, token)}
+ width="128px"
+ />
+ }
+ description={translate(
+ 'overview.badges',
+ BadgeType.qualityGate,
+ 'description',
+ qualifier
+ )}
+ />
+ </div>
+ </DeferredSpinner>
- this.setState({ isRenewing: true });
- await renewProjectBadgesToken(key).catch(() => {});
- await this.fetchToken();
- if (this.mounted) {
- this.setState({ isRenewing: false });
- }
- };
+ {BadgeType.measure === selectedType && (
+ <FormField htmlFor="badge-param-customize" label={translate('overview.badges.metric')}>
+ <InputSelect
+ className="sw-w-abs-300"
+ inputId="badge-param-customize"
+ options={metricsOptions}
+ onChange={(value) => {
+ if (value) {
+ setMetricOptions(value.value);
+ }
+ }}
+ value={metricsOptions.find((m) => m.value === metricOptions)}
+ />
+ </FormField>
+ )}
- render() {
- const {
- branchLike,
- component: { key: project, qualifier, configuration },
- } = this.props;
- const { isRenewing, selectedType, badgeOptions, token } = this.state;
- const fullBadgeOptions = {
- project,
- ...badgeOptions,
- ...getBranchLikeQuery(branchLike),
- };
- const canRenew = configuration?.showSettings;
+ <BasicSeparator className="sw-mb-4" />
- return (
- <div className="display-flex-column">
- <h3 tabIndex={-1} ref={this.headingNodeRef}>
- {translate('overview.badges.get_badge', qualifier)}
- </h3>
- <p className="big-spacer-bottom">{translate('overview.badges.description', qualifier)}</p>
- <BadgeButton
- onClick={this.handleSelectBadge}
- selected={BadgeType.measure === selectedType}
- type={BadgeType.measure}
- url={getBadgeUrl(BadgeType.measure, fullBadgeOptions, token)}
- />
- <p className="huge-spacer-bottom spacer-top">
- {translate('overview.badges', BadgeType.measure, 'description', qualifier)}
- </p>
- <BadgeButton
- onClick={this.handleSelectBadge}
- selected={BadgeType.qualityGate === selectedType}
- type={BadgeType.qualityGate}
- url={getBadgeUrl(BadgeType.qualityGate, fullBadgeOptions, token)}
- />
- <p className="huge-spacer-bottom spacer-top">
- {translate('overview.badges', BadgeType.qualityGate, 'description', qualifier)}
- </p>
- <BadgeParams
- className="big-spacer-bottom display-flex-column"
- options={badgeOptions}
- type={selectedType}
- updateOptions={this.handleUpdateOptions}
- />
- {isRenewing ? (
- <div className="spacer-top spacer-bottom display-flex-row display-flex-justify-center">
- <DeferredSpinner className="spacer-top spacer-bottom" loading={isRenewing} />
- </div>
- ) : (
- <CodeSnippet isOneLine snippet={getBadgeSnippet(selectedType, fullBadgeOptions, token)} />
+ <FormField label={translate('overview.badges.format')}>
+ <div className="sw-flex ">
+ <ToggleButton
+ label={translate('overview.badges.format')}
+ options={formatOptions}
+ onChange={(value: BadgeFormats) => {
+ if (value) {
+ setFormatOption(value);
+ }
+ }}
+ value={formatOption}
+ />
+ </div>
+ </FormField>
+
+ <DeferredSpinner className="spacer-top spacer-bottom" loading={isFetchingToken || isRenewing}>
+ {!isLoading && (
+ <CodeSnippet
+ wrap
+ className="sw-p-6 it__code-snippet"
+ language="plaintext"
+ snippet={getBadgeSnippet(selectedType, fullBadgeOptions, token)}
+ />
)}
+ </DeferredSpinner>
- <Alert variant="warning">
- <p>
- {translate('overview.badges.leak_warning')}{' '}
- {canRenew && translate('overview.badges.renew.description')}
- </p>
+ <FlagMessage variant="warning">
+ <p>
+ {translate('overview.badges.leak_warning')}
{canRenew && (
- <Button
- disabled={isRenewing}
- className="spacer-top it__project-info-renew-badge"
- onClick={this.handleRenew}
- >
- {translate('overview.badges.renew')}
- </Button>
+ <div className="sw-flex sw-flex-col">
+ {translate('overview.badges.renew.description')}{' '}
+ <ButtonSecondary
+ disabled={isLoading}
+ className="spacer-top it__project-info-renew-badge sw-mr-auto"
+ onClick={() => {
+ renewToken(project);
+ }}
+ >
+ {translate('overview.badges.renew')}
+ </ButtonSecondary>
+ </div>
)}
- </Alert>
- </div>
- );
- }
+ </p>
+ </FlagMessage>
+ </div>
+ );
}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { screen } from '@testing-library/react';
+import { screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import * as React from 'react';
import selectEvent from 'react-select-event';
import { Location } from '../../../../helpers/urls';
import { ComponentQualifier } from '../../../../types/component';
import { MetricKey } from '../../../../types/metrics';
-import ProjectBadges from '../ProjectBadges';
+import ProjectBadges, { ProjectBadgesProps } from '../ProjectBadges';
import { BadgeType } from '../utils';
jest.mock('../../../../helpers/urls', () => ({
component: mockComponent({ configuration: { showSettings: true } }),
});
- expect(
- await screen.findByText(`overview.badges.get_badge.${ComponentQualifier.Project}`)
- ).toHaveFocus();
-
- expect(screen.getByAltText(`overview.badges.${BadgeType.qualityGate}.alt`)).toHaveAttribute(
- 'src',
- 'host/api/project_badges/quality_gate?branch=branch-6.7&project=my-project&token=foo'
+ await waitFor(() =>
+ expect(screen.getByAltText(`overview.badges.${BadgeType.qualityGate}.alt`)).toHaveAttribute(
+ 'src',
+ 'host/api/project_badges/quality_gate?branch=branch-6.7&project=my-project&token=foo'
+ )
);
expect(screen.getByAltText(`overview.badges.${BadgeType.measure}.alt`)).toHaveAttribute(
)
).toBeInTheDocument();
- await selectEvent.select(screen.getByLabelText('format:'), [
+ await selectEvent.select(screen.getByLabelText('overview.badges.format'), [
'overview.badges.options.formats.url',
]);
)
).toBeInTheDocument();
- await selectEvent.select(screen.getByLabelText('overview.badges.metric:'), MetricKey.coverage);
+ await selectEvent.select(screen.getByLabelText('overview.badges.metric'), MetricKey.coverage);
expect(
screen.getByText(
).toBeInTheDocument();
});
-function renderProjectBadges(props: Partial<ProjectBadges['props']> = {}) {
+function renderProjectBadges(props: Partial<ProjectBadgesProps> = {}) {
return renderComponent(
<ProjectBadges
branchLike={mockBranch()}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-.badges-list {
- display: flex;
- justify-content: space-around;
- justify-content: space-evenly;
- flex-wrap: nowrap;
-}
-
-.button.badge-button {
- display: flex;
- justify-content: center;
- padding: var(--gridSize);
- min-width: 146px;
- height: 116px;
- background-color: var(--barBackgroundColor);
- border: solid 1px var(--barBorderColor);
- border-radius: 3px;
- transition: all 0.3s ease;
-}
-
-.button.badge-button:hover,
-.button.badge-button:focus,
-.button.badge-button:active {
- background-color: var(--barBackgroundColor);
- border-color: var(--blue);
-}
-
-.button.badge-button.selected {
- background-color: var(--lightBlue);
- border-color: var(--darkBlue);
-}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
+import { useContext } from 'react';
+import { getProjectBadgesToken, renewProjectBadgesToken } from '../../../api/project-badges';
+import { fetchWebApi } from '../../../api/web-api';
+import { MetricsContext } from '../../../app/components/metrics/MetricsContext';
+import { getLocalizedMetricName } from '../../../helpers/l10n';
+import { MetricKey } from '../../../types/metrics';
+
+export function useFetchWebApiQuery() {
+ return useQuery(['web-api'], () => fetchWebApi(false));
+}
+
+export function useRenewBagdeTokenMutation() {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: async (key: string) => {
+ await renewProjectBadgesToken(key);
+ },
+ onSuccess: (_, key) => {
+ queryClient.invalidateQueries({ queryKey: ['badges-token', key], refetchType: 'all' });
+ },
+ });
+}
+
+export function useBadgeMetricsQuery() {
+ const metrics = useContext(MetricsContext);
+ const { data: webservices = [], ...rest } = useFetchWebApiQuery();
+ const domain = webservices.find((d) => d.path === 'api/project_badges');
+ const ws = domain?.actions.find((w) => w.key === 'measure');
+ const param = ws?.params?.find((p) => p.key === 'metric');
+ if (param?.possibleValues) {
+ return {
+ ...rest,
+ data: param.possibleValues.map((key) => {
+ const metric = metrics[key];
+ return {
+ value: key as MetricKey,
+ label: metric ? getLocalizedMetricName(metric) : key,
+ };
+ }),
+ };
+ }
+ return { ...rest, data: [] };
+}
+
+export function useBadgeTokenQuery(componentKey: string) {
+ return useQuery(['badges-token', componentKey] as const, ({ queryKey: [_, key] }) =>
+ getProjectBadgesToken(key)
+ );
+}
overview.deleted_profile={0} has been deleted since the last analysis.
overview.link_to_x_profile_y=Go to {0} profile "{1}"
-overview.badges.get_badge.TRK=Get project badges
-overview.badges.get_badge.VW=Get portfolio badges
-overview.badges.get_badge.APP=Get application badges
+overview.badges.get_badge=Badges
overview.badges.title=Get project badges
overview.badges.description.TRK=Show the status of your project metrics on your README or website. Pick your style:
overview.badges.description.VW=Show the status of your portfolio metrics on your README or website. Pick your style:
overview.badges.description.APP=Show the status of your application metrics on your README or website. Pick your style:
-overview.badges.metric=Metric
+overview.badges.metric=Customize badge
+overview.badges.format=Code format
overview.badges.options.colors.white=White
overview.badges.options.colors.black=Black
overview.badges.options.colors.orange=Orange