]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-21482 My Projects adopts the new UI
authorJeremy Davis <jeremy.davis@sonarsource.com>
Wed, 24 Jan 2024 15:45:15 +0000 (16:45 +0100)
committersonartech <sonartech@sonarsource.com>
Mon, 29 Jan 2024 20:03:17 +0000 (20:03 +0000)
server/sonar-web/src/main/js/app/components/GlobalContainer.tsx
server/sonar-web/src/main/js/apps/account/account.css
server/sonar-web/src/main/js/apps/account/projects/ProjectCard.tsx
server/sonar-web/src/main/js/apps/account/projects/Projects.tsx
server/sonar-web/src/main/js/apps/account/projects/ProjectsContainer.tsx
server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaLinks.tsx
server/sonar-web/src/main/js/components/common/MetaLink.tsx
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index dc38bafbca807171b7f6c5b6f6da9359a8557520..55900e06cb4787d392f454c3a69e7d5073ecd9f8 100644 (file)
@@ -87,6 +87,7 @@ const TEMP_PAGELIST_WITH_NEW_BACKGROUND_WHITE = [
   '/admin/extension/license/support',
   '/admin/audit',
   '/admin/projects_management',
+  '/account/projects',
 ];
 
 export default function GlobalContainer() {
index 7138c62f5e28b682826e20e0093b0af4c352eb9e..0578071bf8f59a0016ff62eae5de2b19321f1ca9 100644 (file)
   border-top: 1px solid var(--barBorderColor);
 }
 
-.account-projects-list > li {
-  padding: 15px 20px;
-}
-
-.account-projects-list > li + li {
-  border-top: 1px solid var(--barBorderColor);
-}
-
-.account-project-side {
-  float: right;
-  margin-left: 10px;
-  text-align: right;
-}
-
-.account-project-analysis {
-  line-height: var(--controlHeight);
-  color: var(--secondFontColor);
-  font-size: var(--smallFontSize);
-}
-
-.account-project-card {
-  position: relative;
-  display: block;
-}
-
-.account-project-name {
-  display: inline-block;
-  vertical-align: top;
-  max-width: 300px;
-  overflow: hidden;
-  text-overflow: ellipsis;
-  white-space: nowrap;
-}
-
-.account-project-name > a {
-  border-bottom-color: #d0d0d0;
-  color: var(--baseFontColor);
-}
-
-.account-project-name > a:hover {
-  border-bottom-color: var(--lightBlue);
-  color: var(--blue);
-}
-
-.account-project-quality-gate {
-  display: inline-block;
-  vertical-align: top;
-  line-height: var(--controlHeight);
-  margin-left: 8px;
-}
-
-.account-project-description {
-  margin-top: 6px;
-  line-height: 1.5;
-}
-
-.account-project-links {
-  margin-top: 6px;
-}
-
-.account-project-key {
-  margin-top: 6px;
-  color: var(--secondFontColor);
-  font-size: var(--smallFontSize);
-}
-
-.my-activity-issues {
-  position: relative;
-  display: flex;
-  justify-content: center;
-  margin-bottom: 70px;
-}
-
-.my-activity-issues:after {
-  position: absolute;
-  z-index: 5;
-  top: -15px;
-  left: 50%;
-  width: 1px;
-  height: 100px;
-  background-color: var(--barBorderColor);
-  transform: rotate(30deg);
-  content: '';
-}
-
-.my-activity-issues > a {
-  display: block;
-  padding: 15px 20px;
-  border: none;
-  border-radius: 2px;
-  color: var(--baseFontColor);
-}
-
-.my-activity-issues > a:hover {
-  background-color: var(--barBackgroundColor);
-}
-
-.my-activity-recent-issues {
-  margin-right: 50px;
-  text-align: right;
-}
-
-.my-activity-recent-issues .my-activity-issues-note {
-  text-align: left;
-}
-
-.my-activity-all-issues {
-  margin-left: 50px;
-}
-
-.my-activity-issues-number {
-  display: inline-block;
-  vertical-align: middle;
-  line-height: 36px;
-  font-size: 36px;
-  font-weight: 300;
-}
-
-.my-activity-issues-note {
-  display: inline-block;
-  vertical-align: middle;
-  padding-left: 10px;
-  padding-top: 2px;
-  line-height: 16px;
-  font-size: var(--smallFontSize);
-}
-
-.my-activity-projects {
-  width: 360px;
-  margin: 0 auto;
-  padding: 40px 0;
-}
-
-.my-activity-projects-header {
-  line-height: var(--controlHeight);
-  margin-bottom: 15px;
-  padding: 0 10px;
-}
-
-.my-activity-projects > ul > li + li {
-  border-top: 1px solid var(--barBorderColor);
-}
-
-.my-activity-projects > ul > li > a {
-  display: block;
-  padding: 15px 10px;
-  border: none;
-}
-
-.my-activity-projects > ul > li > a:hover {
-  background-color: var(--barBackgroundColor);
-}
-
-.my-activity-projects .level {
-  width: 60px;
-}
-
-.my-activity-projects .more {
-  margin-top: 30px;
-  text-align: center;
-}
-
 .notifications-table {
   margin-top: calc(-2 * var(--gridSize));
 }
index 833425e19a66614c9076d21f1210d85a98fadc2e..ecb186bf184d8fe8f11d607d4351dee50b2dcf96 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 {
+  Card,
+  DiscreetLink,
+  LightPrimary,
+  Note,
+  QualityGateIndicator,
+  SubHeading,
+  UnorderedList,
+} from 'design-system';
 import * as React from 'react';
-import Link from '../../../components/common/Link';
 import MetaLink from '../../../components/common/MetaLink';
-import HelpTooltip from '../../../components/controls/HelpTooltip';
+import Tooltip from '../../../components/controls/Tooltip';
 import DateFromNow from '../../../components/intl/DateFromNow';
-import Level from '../../../components/ui/Level';
 import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { formatMeasure } from '../../../helpers/measures';
 import { orderLinks } from '../../../helpers/projectLinks';
 import { getProjectUrl } from '../../../helpers/urls';
-import { MyProject, ProjectLink } from '../../../types/types';
+import { MetricType } from '../../../types/metrics';
+import { MyProject, ProjectLink, Status } from '../../../types/types';
 
 interface Props {
   project: MyProject;
 }
 
-export default function ProjectCard({ project }: Props) {
+export default function ProjectCard({ project }: Readonly<Props>) {
   const { links } = project;
 
   const orderedLinks: ProjectLink[] = orderLinks(
@@ -49,53 +58,54 @@ export default function ProjectCard({ project }: Props) {
 
   const { lastAnalysisDate } = project;
 
+  const formatted = formatMeasure(project.qualityGate, MetricType.Level);
+  const qualityGateLabel = translateWithParameters('overview.quality_gate_x', formatted);
+
   return (
-    <div className="account-project-card clearfix">
-      <aside className="account-project-side">
+    <Card>
+      <aside className="sw-float-right sw-flex sw-flex-col sw-items-end sw-gap-2">
         {lastAnalysisDate !== undefined ? (
-          <div className="account-project-analysis">
+          <Note>
             <DateFromNow date={lastAnalysisDate}>
               {(fromNow) => translateWithParameters('my_account.projects.analyzed_x', fromNow)}
             </DateFromNow>
-          </div>
+          </Note>
         ) : (
-          <div className="account-project-analysis">
-            {translate('my_account.projects.never_analyzed')}
-          </div>
+          <Note>{translate('my_account.projects.never_analyzed')}</Note>
         )}
 
         {project.qualityGate !== undefined && (
-          <div className="account-project-quality-gate">
-            {project.qualityGate === 'WARN' && (
-              <HelpTooltip
-                className="little-spacer-right"
-                overlay={translate('quality_gates.conditions.warning.tooltip')}
-              />
-            )}
-            <Level aria-label={translate('quality_gates.status')} level={project.qualityGate} />
+          <div>
+            <Tooltip overlay={qualityGateLabel}>
+              <span className="sw-flex sw-items-center">
+                <QualityGateIndicator
+                  status={(project.qualityGate as Status) ?? 'NONE'}
+                  ariaLabel={qualityGateLabel}
+                />
+                <LightPrimary className="sw-ml-2 sw-body-sm-highlight">{formatted}</LightPrimary>
+              </span>
+            </Tooltip>
           </div>
         )}
       </aside>
 
-      <h3 className="account-project-name">
-        <Link to={getProjectUrl(project.key)}>{project.name}</Link>
-      </h3>
+      <SubHeading as="h3">
+        <DiscreetLink to={getProjectUrl(project.key)}>{project.name}</DiscreetLink>
+      </SubHeading>
+
+      <Note>{project.key}</Note>
+
+      {!!project.description && <div className="sw-mt-2">{project.description}</div>}
 
       {orderedLinks.length > 0 && (
-        <div className="account-project-links">
-          <ul className="list-inline">
+        <div className="sw-mt-2">
+          <UnorderedList className="sw-flex sw-gap-4">
             {orderedLinks.map((link) => (
-              <MetaLink iconOnly key={link.id} link={link} />
+              <MetaLink key={link.id} link={link} />
             ))}
-          </ul>
+          </UnorderedList>
         </div>
       )}
-
-      <div className="account-project-key">{project.key}</div>
-
-      {!!project.description && (
-        <div className="account-project-description">{project.description}</div>
-      )}
-    </div>
+    </Card>
   );
 }
index 45a717c29014e8656e809d08d0bfee211ce7099b..08c88a3cd67563d62794a4744346e70019e707f9 100644 (file)
@@ -30,35 +30,36 @@ interface Props {
   total?: number;
 }
 
-export default function Projects(props: Props) {
+export default function Projects(props: Readonly<Props>) {
   const { projects } = props;
 
   return (
     <div id="account-projects">
-      {projects.length === 0 ? (
-        <div className="js-no-results">{translate('my_account.projects.no_results')}</div>
-      ) : (
-        <p>{translate('my_account.projects.description')}</p>
-      )}
+      <div className="sw-mt-8">
+        {projects.length === 0
+          ? translate('my_account.projects.no_results')
+          : translate('my_account.projects.description')}
+      </div>
 
       {projects.length > 0 && (
-        <ul className="account-projects-list">
-          {projects.map((project) => (
-            <li key={project.key}>
-              <ProjectCard project={project} />
-            </li>
-          ))}
-        </ul>
-      )}
+        <>
+          <ul className="sw-mt-4 sw-flex sw-flex-col sw-gap-4">
+            {projects.map((project) => (
+              <li key={project.key}>
+                <ProjectCard project={project} />
+              </li>
+            ))}
+          </ul>
 
-      {projects.length > 0 && (
-        <ListFooter
-          count={projects.length}
-          loadMore={props.loadMore}
-          loading={props.loading}
-          ready={!props.loading}
-          total={props.total || 0}
-        />
+          <ListFooter
+            count={projects.length}
+            loadMore={props.loadMore}
+            loading={props.loading}
+            ready={!props.loading}
+            total={props.total ?? 0}
+            useMIUIButtons
+          />
+        </>
       )}
     </div>
   );
index dcc7cb1ef103a8577f9e4cf9f74ff06e6bf38ffb..e8b863d0274cc070dcfa01e865462363a6af79cc 100644 (file)
@@ -17,6 +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 { LargeCenteredLayout, PageContentFontWrapper, Spinner } from 'design-system';
 import * as React from 'react';
 import { Helmet } from 'react-helmet-async';
 import { getMyProjects } from '../../../api/components';
@@ -62,27 +63,22 @@ export default class ProjectsContainer extends React.PureComponent<{}, State> {
   };
 
   render() {
-    const helmet = <Helmet title={translate('my_account.projects')} />;
-
-    if (this.state.projects == null) {
-      return (
-        <div className="text-center">
-          {helmet}
-          <i className="spinner spacer" />
-        </div>
-      );
-    }
+    const { loading, projects = [], total } = this.state;
 
     return (
-      <div className="account-body account-container">
-        {helmet}
-        <Projects
-          loadMore={this.loadMore}
-          loading={this.state.loading}
-          projects={this.state.projects}
-          total={this.state.total}
-        />
-      </div>
+      <LargeCenteredLayout as="main">
+        <PageContentFontWrapper className="sw-body-sm sw-py-8">
+          <Helmet title={translate('my_account.projects')} />
+          <Spinner loading={loading && projects.length === 0}>
+            <Projects
+              loadMore={this.loadMore}
+              loading={loading}
+              projects={projects}
+              total={total}
+            />
+          </Spinner>
+        </PageContentFontWrapper>
+      </LargeCenteredLayout>
     );
   }
 }
index 27aa46835d3d26f14c93fd24c55be6a0e32ea91d..8891c5311cccd65c74aee8a2a188b548414dac92 100644 (file)
@@ -27,7 +27,7 @@ interface Props {
   links: ProjectLink[];
 }
 
-export default function MetaLinks({ links }: Props) {
+export default function MetaLinks({ links }: Readonly<Props>) {
   const orderedLinks = orderLinks(links);
 
   return (
@@ -35,7 +35,7 @@ export default function MetaLinks({ links }: Props) {
       <h3 id="external-links">{translate('overview.external_links')}</h3>
       <ul className="project-info-list" aria-labelledby="external-links">
         {orderedLinks.map((link) => (
-          <MetaLink miui key={link.id} link={link} />
+          <MetaLink key={link.id} link={link} />
         ))}
       </ul>
     </>
index 26dd69e2e07696ce006d290932e7e49b9c722310..b4df193db662e276e3a0312adda65b8e77752653 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 styled from '@emotion/styled';
 import { CloseIcon, InputField, InteractiveIcon, Link } from 'design-system';
 import React, { useState } from 'react';
 import isValidUri from '../../app/utils/isValidUri';
 import { translate } from '../../helpers/l10n';
 import { getLinkName } from '../../helpers/projectLinks';
 import { ProjectLink } from '../../types/types';
-import { ClearButton } from '../controls/buttons';
 import ProjectLinkIcon from '../icons/ProjectLinkIcon';
 
 interface Props {
   iconOnly?: boolean;
   link: ProjectLink;
-  // TODO Remove this prop when all links are migrated to the new design
-  miui?: boolean;
 }
 
-export default function MetaLink({ iconOnly, link, miui }: Props) {
+export default function MetaLink({ iconOnly, link }: Readonly<Props>) {
   const [expanded, setExpanded] = useState(false);
 
   const handleClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
@@ -51,29 +49,20 @@ export default function MetaLink({ iconOnly, link, miui }: Props) {
 
   const linkTitle = getLinkName(link);
   const isValid = isValidUri(link.url);
-  return miui ? (
+  return (
     <li>
-      <Link
-        isExternal
+      <StyledLink
         to={link.url}
         preventDefault={!isValid}
         onClick={isValid ? undefined : handleClick}
-        rel="nofollow noreferrer noopener"
-        target="_blank"
-        icon={<ProjectLinkIcon miui className="little-spacer-right" type={link.type} />}
+        icon={<ProjectLinkIcon miui type={link.type} />}
       >
         {!iconOnly && linkTitle}
-      </Link>
+      </StyledLink>
 
       {expanded && (
-        <div className="little-spacer-top display-flex-center">
-          <InputField
-            className="overview-key width-80"
-            onClick={handleSelect}
-            readOnly
-            type="text"
-            value={link.url}
-          />
+        <div className="sw-mt-1 sw-flex sw-items-center">
+          <InputField onClick={handleSelect} readOnly type="text" value={link.url} size="large" />
           <InteractiveIcon
             Icon={CloseIcon}
             aria-label={translate('hide')}
@@ -83,31 +72,21 @@ export default function MetaLink({ iconOnly, link, miui }: Props) {
         </div>
       )}
     </li>
-  ) : (
-    <li>
-      <a
-        className="link-no-underline"
-        href={isValid ? link.url : undefined}
-        onClick={isValid ? undefined : handleClick}
-        rel="nofollow noreferrer noopener"
-        target="_blank"
-        title={linkTitle}
-      >
-        <ProjectLinkIcon className="little-spacer-right" type={link.type} />
-        {!iconOnly && linkTitle}
-      </a>
-      {expanded && (
-        <div className="little-spacer-top display-flex-center">
-          <input
-            className="overview-key width-80"
-            onClick={handleSelect}
-            readOnly
-            type="text"
-            value={link.url}
-          />
-          <ClearButton className="little-spacer-left" onClick={handleCollapse} />
-        </div>
-      )}
-    </li>
   );
 }
+
+/*
+ * Override the spacing to make it smaller
+ * 1rem = 16px for the icon width
+ * +
+ * 0.5 rem = margin '2'
+ */
+const StyledLink = styled(Link)`
+  margin-left: 1.5rem;
+
+  & > svg,
+  & > img {
+    margin-right: 0.5rem;
+    margin-left: -1.5rem;
+  }
+`;
index 406b403c26a9008851918cb195e476083e29ae94..80754c1d07518d3bc16c2a02c4b15dcd177205e9 100644 (file)
@@ -2252,8 +2252,6 @@ quality_gates.conditions.overall_code.description=Conditions on overall code app
 quality_gates.conditions.overall_code_1=1 condition failed on overall code
 quality_gates.conditions.overall_code_x={0} conditions failed on overall code
 quality_gates.conditions.operator=Operator
-quality_gates.conditions.warning=Warning
-quality_gates.conditions.warning.tooltip=Warning status is deprecated and will disappear with the next update of the Quality Gate.
 quality_gates.conditions.value=Value
 quality_gates.conditions.where=Where?
 quality_gates.duplicated_conditions=This quality gate has duplicated conditions:
@@ -2678,8 +2676,8 @@ my_account.tokens_last_usage=Last use
 my_account.tokens.expiration=Expiration
 my_account.tokens.expired=Token is expired
 my_account.projects=Projects
-my_account.projects.description=Those projects are the ones you are administering.
-my_account.projects.no_results=You are not administering any project yet.
+my_account.projects.description=You have admin privileges over the following projects.
+my_account.projects.no_results=You have no project admin privileges.
 my_account.projects.analyzed_x=Analyzed {0}
 my_account.projects.never_analyzed=Never analyzed
 my_account.search_project=Search Project