]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-19613 Migrate project badge to new UI
authorMathieu Suen <mathieu.suen@sonarsource.com>
Fri, 23 Jun 2023 09:45:36 +0000 (11:45 +0200)
committersonartech <sonartech@sonarsource.com>
Mon, 26 Jun 2023 20:03:55 +0000 (20:03 +0000)
12 files changed:
server/sonar-web/design-system/src/components/IlllustredSelectionCard.tsx [new file with mode: 0644]
server/sonar-web/design-system/src/components/RadioButton.tsx
server/sonar-web/design-system/src/components/ToggleButton.tsx
server/sonar-web/design-system/src/components/index.ts
server/sonar-web/src/main/js/api/project-badges.ts
server/sonar-web/src/main/js/apps/projectInformation/badges/BadgeButton.tsx [deleted file]
server/sonar-web/src/main/js/apps/projectInformation/badges/BadgeParams.tsx [deleted file]
server/sonar-web/src/main/js/apps/projectInformation/badges/ProjectBadges.tsx
server/sonar-web/src/main/js/apps/projectInformation/badges/__tests__/ProjectBadges-test.tsx
server/sonar-web/src/main/js/apps/projectInformation/badges/styles.css [deleted file]
server/sonar-web/src/main/js/apps/projectInformation/query/badges.ts [new file with mode: 0644]
sonar-core/src/main/resources/org/sonar/l10n/core.properties

diff --git a/server/sonar-web/design-system/src/components/IlllustredSelectionCard.tsx b/server/sonar-web/design-system/src/components/IlllustredSelectionCard.tsx
new file mode 100644 (file)
index 0000000..bdc2e6d
--- /dev/null
@@ -0,0 +1,88 @@
+/*
+ * 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')};
+  }
+`;
index f374db14dc75a342da0023422dfe47d98cf2e2fe..069f6d2c78f3bb91fc6fdbc14f3077dfc87bc01a 100644 (file)
@@ -28,15 +28,17 @@ type AllowedRadioButtonAttributes = Pick<
   '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,
index 6291bdd2102967ad7cdff4412750bc026f21a12c..edd5c17b99b02b925d7b602d43749ccffaec39fd 100644 (file)
@@ -38,7 +38,7 @@ export interface ButtonToggleProps<T extends ToggleButtonValueType> {
   disabled?: boolean;
   label?: string;
   onChange: (value: T) => void;
-  options: Array<ToggleButtonsOption<T>>;
+  options: ReadonlyArray<ToggleButtonsOption<T>>;
   role?: 'radiogroup' | 'tablist';
   value?: T;
 }
index ba3cb3bc20a66f187f89fe5fe8b8d5e241823224..89f9f5024d39eb3086f7980c8dc3674db351b7d5 100644 (file)
@@ -49,6 +49,7 @@ export * from './HighlightedSection';
 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';
index 259df5a75d12ebafd04200261c770e7f48a5a0f1..5db6e5fe0cf3e5ca6dd1f3d888d571fa76b7b51c 100644 (file)
@@ -18,7 +18,7 @@
  * 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 })
@@ -27,5 +27,5 @@ export function getProjectBadgesToken(project: string) {
 }
 
 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);
 }
diff --git a/server/sonar-web/src/main/js/apps/projectInformation/badges/BadgeButton.tsx b/server/sonar-web/src/main/js/apps/projectInformation/badges/BadgeButton.tsx
deleted file mode 100644 (file)
index daea2e4..0000000
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * 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>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/projectInformation/badges/BadgeParams.tsx b/server/sonar-web/src/main/js/apps/projectInformation/badges/BadgeParams.tsx
deleted file mode 100644 (file)
index c8789b0..0000000
+++ /dev/null
@@ -1,154 +0,0 @@
-/*
- * 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);
index 4b4a8ff6815f4be7607c69d5e2a74e24cd04db08..f87f66623518e3291c61826e7c1d632aa74d0349 100644 (file)
  * 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>
+  );
 }
index 28e3bfb173b609fd52f4ed62b26d5292536af82a..19f5b52275f5c0a4f9233720743aef2ddb037ba1 100644 (file)
@@ -17,7 +17,7 @@
  * 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';
@@ -28,7 +28,7 @@ import { renderComponent } from '../../../../helpers/testReactTestingUtils';
 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', () => ({
@@ -65,13 +65,11 @@ it('should renew token', async () => {
     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(
@@ -105,7 +103,7 @@ it('should update params', async () => {
     )
   ).toBeInTheDocument();
 
-  await selectEvent.select(screen.getByLabelText('format:'), [
+  await selectEvent.select(screen.getByLabelText('overview.badges.format'), [
     'overview.badges.options.formats.url',
   ]);
 
@@ -115,7 +113,7 @@ it('should update params', async () => {
     )
   ).toBeInTheDocument();
 
-  await selectEvent.select(screen.getByLabelText('overview.badges.metric:'), MetricKey.coverage);
+  await selectEvent.select(screen.getByLabelText('overview.badges.metric'), MetricKey.coverage);
 
   expect(
     screen.getByText(
@@ -124,7 +122,7 @@ it('should update params', async () => {
   ).toBeInTheDocument();
 });
 
-function renderProjectBadges(props: Partial<ProjectBadges['props']> = {}) {
+function renderProjectBadges(props: Partial<ProjectBadgesProps> = {}) {
   return renderComponent(
     <ProjectBadges
       branchLike={mockBranch()}
diff --git a/server/sonar-web/src/main/js/apps/projectInformation/badges/styles.css b/server/sonar-web/src/main/js/apps/projectInformation/badges/styles.css
deleted file mode 100644 (file)
index 8dca6fd..0000000
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * 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);
-}
diff --git a/server/sonar-web/src/main/js/apps/projectInformation/query/badges.ts b/server/sonar-web/src/main/js/apps/projectInformation/query/badges.ts
new file mode 100644 (file)
index 0000000..204030a
--- /dev/null
@@ -0,0 +1,70 @@
+/*
+ * 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)
+  );
+}
index 7fd7a3df05c449740b8f98905916f3e07bb6cb15..26cd64a379cce2eede53e6535234c99c747e3e05 100644 (file)
@@ -3601,14 +3601,13 @@ overview.deprecated_profile=This Quality Profile uses {0} deprecated rules and s
 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