aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorWouter Admiraal <wouter.admiraal@sonarsource.com>2022-12-29 08:05:41 +0100
committersonartech <sonartech@sonarsource.com>2023-01-03 20:03:03 +0000
commitb6982a7a1a7f67d24a0c4bef4e5937d0a5abe821 (patch)
tree4216857e1d677a64e305d9b2fcbd3ea4879e6049
parent8937c9305a7d8693b5e3771439404238672913b1 (diff)
downloadsonarqube-b6982a7a1a7f67d24a0c4bef4e5937d0a5abe821.tar.gz
sonarqube-b6982a7a1a7f67d24a0c4bef4e5937d0a5abe821.zip
SONAR-10740 Display each project's last analysis date in Portfolio breakdown
-rw-r--r--server/sonar-web/src/main/js/apps/code/__tests__/Code-it.ts14
-rw-r--r--server/sonar-web/src/main/js/apps/code/code.css54
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/CodeApp.tsx5
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/Component.tsx53
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/Components.tsx65
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/ComponentsEmpty.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/ComponentsHeader.tsx38
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/SourceViewerWrapper.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/__tests__/SourceViewerWrapper-test.tsx76
-rw-r--r--server/sonar-web/src/main/js/apps/code/utils.ts42
-rw-r--r--server/sonar-web/src/main/js/components/intl/__mocks__/DateFromNow.tsx2
-rw-r--r--server/sonar-web/src/main/js/types/types.ts1
-rw-r--r--sonar-core/src/main/resources/org/sonar/l10n/core.properties3
13 files changed, 237 insertions, 121 deletions
diff --git a/server/sonar-web/src/main/js/apps/code/__tests__/Code-it.ts b/server/sonar-web/src/main/js/apps/code/__tests__/Code-it.ts
index f495b4b5da9..88011dfa728 100644
--- a/server/sonar-web/src/main/js/apps/code/__tests__/Code-it.ts
+++ b/server/sonar-web/src/main/js/apps/code/__tests__/Code-it.ts
@@ -41,6 +41,8 @@ jest.mock('../../../api/issues');
jest.mock('../../../api/rules');
jest.mock('../../../api/users');
+jest.mock('../../../components/intl/DateFromNow');
+
jest.mock('../../../components/SourceViewer/helpers/lines', () => {
const lines = jest.requireActual('../../../components/SourceViewer/helpers/lines');
return {
@@ -246,7 +248,7 @@ it('should correctly show measures for a project', async () => {
['coverage', '2.0%'],
['duplicated_lines_density', '2.0%'],
].forEach(([domain, value]) => {
- expect(ui.measureValueCell(folderRow, domain, value, 1)).toBeInTheDocument();
+ expect(ui.measureValueCell(folderRow, domain, value)).toBeInTheDocument();
});
// index.tsx
@@ -260,7 +262,7 @@ it('should correctly show measures for a project', async () => {
['coverage', '—'],
['duplicated_lines_density', '—'],
].forEach(([domain, value]) => {
- expect(ui.measureValueCell(fileRow, domain, value, 1)).toBeInTheDocument();
+ expect(ui.measureValueCell(fileRow, domain, value)).toBeInTheDocument();
});
});
@@ -278,6 +280,7 @@ it('should correctly show new VS overall measures for Portfolios', async () => {
children: [
{
component: mockComponent({
+ analysisDate: '2022-02-01',
key: 'child1',
name: 'Child 1',
}),
@@ -314,6 +317,7 @@ it('should correctly show new VS overall measures for Portfolios', async () => {
['security_hotspots', 'C'],
['Maintainability', 'C'],
['ncloc', '3'],
+ ['last_analysis_date', '2022-02-01'],
].forEach(([domain, value]) => {
expect(ui.measureValueCell(child1Row, domain, value)).toBeInTheDocument();
});
@@ -327,6 +331,7 @@ it('should correctly show new VS overall measures for Portfolios', async () => {
['security_hotspots', '—'],
['Maintainability', '—'],
['ncloc', '—'],
+ ['last_analysis_date', '—'],
].forEach(([domain, value]) => {
expect(ui.measureValueCell(child2Row, domain, value)).toBeInTheDocument();
});
@@ -375,7 +380,7 @@ function getPageObject(user: UserEvent) {
newCodeBtn: byRole('button', { name: 'projects.view.new_code' }),
overallCodeBtn: byRole('button', { name: 'projects.view.overall_code' }),
measureRow: (name: string | RegExp) => byRole('row', { name, exact: false }),
- measureValueCell: (row: HTMLElement, name: string, value: string, offset = 0) => {
+ measureValueCell: (row: HTMLElement, name: string, value: string) => {
const i = Array.from(screen.getAllByRole('columnheader')).findIndex((c) =>
c.textContent?.includes(name)
);
@@ -386,7 +391,8 @@ function getPageObject(user: UserEvent) {
}
const { getAllByRole } = within(row);
- const cell = getAllByRole('cell').at(i + offset);
+ const cell = getAllByRole('cell').at(i);
+
if (cell?.textContent === value) {
return cell;
}
diff --git a/server/sonar-web/src/main/js/apps/code/code.css b/server/sonar-web/src/main/js/apps/code/code.css
index 285d20c9ab9..4a74cd5c19b 100644
--- a/server/sonar-web/src/main/js/apps/code/code.css
+++ b/server/sonar-web/src/main/js/apps/code/code.css
@@ -29,6 +29,47 @@
margin-top: -50px;
}
+.code-components .table-wrapper {
+ margin: 0 20px;
+}
+
+.code-components table.data {
+ table-layout: fixed;
+}
+
+.code-components table.data td {
+ padding: 8px 6px;
+ vertical-align: middle;
+}
+
+.code-components table.data th {
+ padding-top: 24px;
+}
+
+.code-components table.data th,
+.code-components table.data td:not(.thin) {
+ width: 84px;
+}
+
+.code-components table.data td.code-name-cell,
+.code-components table.data th.code-name-cell {
+ width: auto;
+}
+
+.code-components table.data th.thin,
+.code-components table.data td.thin {
+ width: 10px !important;
+}
+
+.code-components table.data tr.current-folder {
+ border-bottom: 1px solid var(--barBorderColor);
+}
+
+.code-components table.data tr.current-folder td {
+ padding-bottom: 16px !important;
+ padding-top: 10px !important;
+}
+
.code-breadcrumbs {
display: flex;
flex-wrap: wrap;
@@ -56,19 +97,6 @@
display: none;
}
-.code-components-cell {
- padding-left: calc(2 * var(--gridSize)) !important;
- box-sizing: border-box;
-}
-
-.code-components-rating-cell {
- width: 110px;
-}
-
-.code-name-cell {
- max-width: 0;
-}
-
@media (max-width: 1200px) {
.code-name-cell .badge {
display: none;
diff --git a/server/sonar-web/src/main/js/apps/code/components/CodeApp.tsx b/server/sonar-web/src/main/js/apps/code/components/CodeApp.tsx
index 1bb29530c1c..53bc79a18e1 100644
--- a/server/sonar-web/src/main/js/apps/code/components/CodeApp.tsx
+++ b/server/sonar-web/src/main/js/apps/code/components/CodeApp.tsx
@@ -292,10 +292,12 @@ export class CodeApp extends React.Component<Props, State> {
? translate('projects.page')
: translate('code.page');
+ const isPortfolio = isPortfolioLike(qualifier);
+
return (
<div className="page page-limited">
<A11ySkipTarget anchor="code_main" />
- {!canBrowseAllChildProjects && isPortfolioLike(qualifier) && (
+ {!canBrowseAllChildProjects && isPortfolio && (
<StyledAlert variant="warning" className="it__portfolio_warning">
<AlertContent>
{translate('code_viewer.not_all_measures_are_shown')}
@@ -358,6 +360,7 @@ export class CodeApp extends React.Component<Props, State> {
rootComponent={component}
selected={highlighted}
newCodeSelected={newCodeSelected}
+ showAnalysisDate={isPortfolio}
/>
</div>
<ListFooter count={components.length} loadMore={this.handleLoadMore} total={total} />
diff --git a/server/sonar-web/src/main/js/apps/code/components/Component.tsx b/server/sonar-web/src/main/js/apps/code/components/Component.tsx
index 245e9188b88..7393e3f7556 100644
--- a/server/sonar-web/src/main/js/apps/code/components/Component.tsx
+++ b/server/sonar-web/src/main/js/apps/code/components/Component.tsx
@@ -20,10 +20,10 @@
import classNames from 'classnames';
import * as React from 'react';
import { withScrollTo } from '../../../components/hoc/withScrollTo';
+import DateFromNow from '../../../components/intl/DateFromNow';
import { WorkspaceContext } from '../../../components/workspace/context';
import { BranchLike } from '../../../types/branch-like';
import { ComponentQualifier } from '../../../types/component';
-import { MetricType } from '../../../types/metrics';
import { ComponentMeasure as TypeComponentMeasure, Metric } from '../../../types/types';
import ComponentMeasure from './ComponentMeasure';
import ComponentName from './ComponentName';
@@ -41,6 +41,7 @@ interface Props {
rootComponent: TypeComponentMeasure;
selected?: boolean;
newCodeSelected?: boolean;
+ showAnalysisDate?: boolean;
}
export class Component extends React.PureComponent<Props> {
@@ -57,6 +58,7 @@ export class Component extends React.PureComponent<Props> {
rootComponent,
selected = false,
newCodeSelected,
+ showAnalysisDate,
} = this.props;
const isFile =
@@ -64,22 +66,19 @@ export class Component extends React.PureComponent<Props> {
component.qualifier === ComponentQualifier.TestFile;
return (
- <tr className={classNames({ selected })}>
- <td className="blank" />
+ <tr className={classNames({ selected, 'current-folder': isBaseComponent })}>
{canBePinned && (
<td className="thin nowrap">
{isFile && (
- <span className="spacer-right">
- <WorkspaceContext.Consumer>
- {({ openComponent }) => (
- <ComponentPin
- branchLike={branchLike}
- component={component}
- openComponent={openComponent}
- />
- )}
- </WorkspaceContext.Consumer>
- </span>
+ <WorkspaceContext.Consumer>
+ {({ openComponent }) => (
+ <ComponentPin
+ branchLike={branchLike}
+ component={component}
+ openComponent={openComponent}
+ />
+ )}
+ </WorkspaceContext.Consumer>
)}
</td>
)}
@@ -99,24 +98,18 @@ export class Component extends React.PureComponent<Props> {
</td>
{metrics.map((metric) => (
- <td
- className={classNames('thin', {
- 'text-center': metric.type === MetricType.Rating,
- 'nowrap text-right': metric.type !== MetricType.Rating,
- })}
- key={metric.key}
- >
- <div
- className={classNames({
- 'code-components-rating-cell': metric.type === MetricType.Rating,
- 'code-components-cell': metric.type !== MetricType.Rating,
- })}
- >
- <ComponentMeasure component={component} metric={metric} />
- </div>
+ <td className="text-center" key={metric.key}>
+ <ComponentMeasure component={component} metric={metric} />
</td>
))}
- <td className="blank" />
+
+ {showAnalysisDate && isBaseComponent && <td />}
+
+ {showAnalysisDate && !isBaseComponent && (
+ <td className="text-center">
+ {component.analysisDate ? <DateFromNow date={component.analysisDate} /> : '—'}
+ </td>
+ )}
</tr>
);
}
diff --git a/server/sonar-web/src/main/js/apps/code/components/Components.tsx b/server/sonar-web/src/main/js/apps/code/components/Components.tsx
index 84fb036497c..3e0c96d8a0e 100644
--- a/server/sonar-web/src/main/js/apps/code/components/Components.tsx
+++ b/server/sonar-web/src/main/js/apps/code/components/Components.tsx
@@ -22,12 +22,13 @@ import * as React from 'react';
import withKeyboardNavigation from '../../../components/hoc/withKeyboardNavigation';
import { getComponentMeasureUniqueKey } from '../../../helpers/component';
import { BranchLike } from '../../../types/branch-like';
+import { ComponentQualifier } from '../../../types/component';
import { ComponentMeasure, Metric } from '../../../types/types';
import Component from './Component';
import ComponentsEmpty from './ComponentsEmpty';
import ComponentsHeader from './ComponentsHeader';
-interface Props {
+interface ComponentsProps {
baseComponent?: ComponentMeasure;
branchLike?: BranchLike;
components: ComponentMeasure[];
@@ -35,33 +36,39 @@ interface Props {
rootComponent: ComponentMeasure;
selected?: ComponentMeasure;
newCodeSelected?: boolean;
+ showAnalysisDate?: boolean;
}
-const BASE_COLUMN_COUNT = 4;
+export function Components(props: ComponentsProps) {
+ const {
+ baseComponent,
+ branchLike,
+ components,
+ rootComponent,
+ selected,
+ metrics,
+ newCodeSelected,
+ showAnalysisDate,
+ } = props;
-export class Components extends React.PureComponent<Props> {
- render() {
- const {
- baseComponent,
- branchLike,
- components,
- rootComponent,
- selected,
- metrics,
- newCodeSelected,
- } = this.props;
+ const canBePinned =
+ baseComponent &&
+ ![
+ ComponentQualifier.Application,
+ ComponentQualifier.Portfolio,
+ ComponentQualifier.SubPortfolio,
+ ].includes(baseComponent.qualifier as ComponentQualifier);
- const colSpan = metrics.length + BASE_COLUMN_COUNT;
- const canBePinned = baseComponent && !['APP', 'VW', 'SVW'].includes(baseComponent.qualifier);
-
- return (
- <table className="data boxed-padding zebra">
+ return (
+ <div className="big-spacer-bottom table-wrapper">
+ <table className="data zebra">
{baseComponent && (
<ComponentsHeader
baseComponent={baseComponent}
canBePinned={canBePinned}
metrics={metrics.map((metric) => metric.key)}
rootComponent={rootComponent}
+ showAnalysisDate={showAnalysisDate}
/>
)}
<tbody>
@@ -77,14 +84,12 @@ export class Components extends React.PureComponent<Props> {
metrics={metrics}
rootComponent={rootComponent}
newCodeSelected={newCodeSelected}
+ showAnalysisDate={showAnalysisDate}
/>
<tr className="blank">
- <td colSpan={3}>
- <hr className="null-spacer-top" />
- </td>
- <td colSpan={colSpan}>
- <hr className="null-spacer-top" />
- </td>
+ <td
+ colSpan={metrics.length + 1 + (canBePinned ? 1 : 0) + (showAnalysisDate ? 1 : 0)}
+ />
</tr>
</>
)}
@@ -103,10 +108,11 @@ export class Components extends React.PureComponent<Props> {
component={component}
hasBaseComponent={baseComponent !== undefined}
key={getComponentMeasureUniqueKey(component)}
- metrics={this.props.metrics}
+ metrics={metrics}
previous={index > 0 ? list[index - 1] : undefined}
rootComponent={rootComponent}
newCodeSelected={newCodeSelected}
+ showAnalysisDate={showAnalysisDate}
selected={
selected &&
getComponentMeasureUniqueKey(component) === getComponentMeasureUniqueKey(selected)
@@ -116,15 +122,10 @@ export class Components extends React.PureComponent<Props> {
) : (
<ComponentsEmpty canBePinned={canBePinned} />
)}
-
- <tr className="blank">
- <td colSpan={3} />
- <td colSpan={colSpan} />
- </tr>
</tbody>
</table>
- );
- }
+ </div>
+ );
}
export default withKeyboardNavigation(Components);
diff --git a/server/sonar-web/src/main/js/apps/code/components/ComponentsEmpty.tsx b/server/sonar-web/src/main/js/apps/code/components/ComponentsEmpty.tsx
index 2c3e345f5d8..c6214240a13 100644
--- a/server/sonar-web/src/main/js/apps/code/components/ComponentsEmpty.tsx
+++ b/server/sonar-web/src/main/js/apps/code/components/ComponentsEmpty.tsx
@@ -28,10 +28,9 @@ export default function ComponentsEmpty({ canBePinned = true }: Props) {
return (
<tr>
{canBePinned && <td />}
- <td className="note" colSpan={2}>
+ <td className="note" colSpan={10}>
{translate('no_results')}
</td>
- <td colSpan={10} />
</tr>
);
}
diff --git a/server/sonar-web/src/main/js/apps/code/components/ComponentsHeader.tsx b/server/sonar-web/src/main/js/apps/code/components/ComponentsHeader.tsx
index f5149241d1a..6041ebe807f 100644
--- a/server/sonar-web/src/main/js/apps/code/components/ComponentsHeader.tsx
+++ b/server/sonar-web/src/main/js/apps/code/components/ComponentsHeader.tsx
@@ -17,16 +17,17 @@
* 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 { translate } from '../../../helpers/l10n';
+import { isPortfolioLike } from '../../../types/component';
import { ComponentMeasure } from '../../../types/types';
-interface Props {
+interface ComponentsHeaderProps {
baseComponent?: ComponentMeasure;
canBePinned?: boolean;
metrics: string[];
rootComponent: ComponentMeasure;
+ showAnalysisDate?: boolean;
}
const SHORT_NAME_METRICS = [
@@ -36,13 +37,9 @@ const SHORT_NAME_METRICS = [
'new_duplicated_lines_density',
];
-export default function ComponentsHeader({
- baseComponent,
- canBePinned = true,
- metrics,
- rootComponent,
-}: Props) {
- const isPortfolio = ['VW', 'SVW'].includes(rootComponent.qualifier);
+export default function ComponentsHeader(props: ComponentsHeaderProps) {
+ const { baseComponent, canBePinned = true, metrics, rootComponent, showAnalysisDate } = props;
+ const isPortfolio = isPortfolioLike(rootComponent.qualifier);
let columns: string[] = [];
if (isPortfolio) {
columns = [
@@ -51,8 +48,12 @@ export default function ComponentsHeader({
translate('portfolio.metric_domain.vulnerabilities'),
translate('portfolio.metric_domain.security_hotspots'),
translate('metric_domain.Maintainability'),
- translate('metric', 'ncloc', 'name'),
+ translate('metric.ncloc.name'),
];
+
+ if (showAnalysisDate) {
+ columns.push(translate('code.last_analysis_date'));
+ }
} else {
columns = metrics.map((metric) =>
translate('metric', metric, SHORT_NAME_METRICS.includes(metric) ? 'short_name' : 'name')
@@ -62,23 +63,14 @@ export default function ComponentsHeader({
return (
<thead>
<tr className="code-components-header">
- <th className="thin nowrap" colSpan={canBePinned ? 2 : 1} />
- <th />
+ {canBePinned && <th className="thin" aria-label={translate('code.pin')} />}
+ <th className="code-name-cell" aria-label={translate('code.name')} />
{baseComponent &&
- columns.map((column, index) => (
- <th
- className={classNames('thin', {
- 'code-components-cell': !isPortfolio && index > 0,
- nowrap: !isPortfolio,
- 'text-center': isPortfolio && index < columns.length - 1,
- 'text-right': !isPortfolio || index === columns.length - 1,
- })}
- key={column}
- >
+ columns.map((column) => (
+ <th className="text-center" key={column}>
{column}
</th>
))}
- <th />
</tr>
</thead>
);
diff --git a/server/sonar-web/src/main/js/apps/code/components/SourceViewerWrapper.tsx b/server/sonar-web/src/main/js/apps/code/components/SourceViewerWrapper.tsx
index 2c96a3d4fab..634327f86e3 100644
--- a/server/sonar-web/src/main/js/apps/code/components/SourceViewerWrapper.tsx
+++ b/server/sonar-web/src/main/js/apps/code/components/SourceViewerWrapper.tsx
@@ -24,7 +24,7 @@ import SourceViewer from '../../../components/SourceViewer/SourceViewer';
import { BranchLike } from '../../../types/branch-like';
import { Issue, Measure } from '../../../types/types';
-interface SourceViewerWrapperProps {
+export interface SourceViewerWrapperProps {
branchLike?: BranchLike;
component: string;
componentMeasures: Measure[] | undefined;
diff --git a/server/sonar-web/src/main/js/apps/code/components/__tests__/SourceViewerWrapper-test.tsx b/server/sonar-web/src/main/js/apps/code/components/__tests__/SourceViewerWrapper-test.tsx
new file mode 100644
index 00000000000..c7ad0fe7058
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/code/components/__tests__/SourceViewerWrapper-test.tsx
@@ -0,0 +1,76 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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 { screen } from '@testing-library/react';
+import * as React from 'react';
+import ComponentsServiceMock from '../../../../api/mocks/ComponentsServiceMock';
+import IssuesServiceMock from '../../../../api/mocks/IssuesServiceMock';
+import { mockLocation } from '../../../../helpers/testMocks';
+import { renderComponent } from '../../../../helpers/testReactTestingUtils';
+import SourceViewerWrapper, { SourceViewerWrapperProps } from '../SourceViewerWrapper';
+
+jest.mock('../../../../api/components');
+jest.mock('../../../../api/issues');
+// The following 2 mocks are needed, because IssuesServiceMock mocks more than it should.
+// This should be removed once IssuesServiceMock is cleaned up.
+jest.mock('../../../../api/rules');
+jest.mock('../../../../api/users');
+
+const issuesHandler = new IssuesServiceMock();
+const componentsHandler = new ComponentsServiceMock();
+// eslint-disable-next-line testing-library/no-node-access
+const originalQuerySelector = document.querySelector;
+const scrollIntoView = jest.fn();
+
+beforeAll(() => {
+ Object.defineProperty(document, 'querySelector', {
+ writable: true,
+ value: () => ({ scrollIntoView }),
+ });
+});
+
+afterAll(() => {
+ Object.defineProperty(document, 'querySelector', {
+ writable: true,
+ value: originalQuerySelector,
+ });
+});
+
+beforeEach(() => {
+ issuesHandler.reset();
+ componentsHandler.reset();
+});
+
+it('should scroll to a line directly', async () => {
+ renderSourceViewerWrapper();
+ await screen.findAllByText('function Test() {}');
+ expect(scrollIntoView).toHaveBeenCalled();
+});
+
+function renderSourceViewerWrapper(props: Partial<SourceViewerWrapperProps> = {}) {
+ return renderComponent(
+ <SourceViewerWrapper
+ component="foo:index.tsx"
+ componentMeasures={[]}
+ location={mockLocation({ query: { line: '2' } })}
+ {...props}
+ />
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/code/utils.ts b/server/sonar-web/src/main/js/apps/code/utils.ts
index 437b2c3d7a1..c1363ecf84e 100644
--- a/server/sonar-web/src/main/js/apps/code/utils.ts
+++ b/server/sonar-web/src/main/js/apps/code/utils.ts
@@ -17,10 +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 { getBreadcrumbs, getChildren, getComponent } from '../../api/components';
+import { getBreadcrumbs, getChildren, getComponent, getComponentData } from '../../api/components';
import { getBranchLikeQuery, isPullRequest } from '../../helpers/branch-like';
import { BranchLike } from '../../types/branch-like';
-import { isPortfolioLike } from '../../types/component';
+import { ComponentQualifier, isPortfolioLike } from '../../types/component';
import { MetricKey } from '../../types/metrics';
import { Breadcrumb, ComponentMeasure } from '../../types/types';
import {
@@ -128,7 +128,7 @@ export function getCodeMetrics(
}
return options.includeQGStatus ? metrics.concat(MetricKey.alert_status) : metrics;
}
- if (qualifier === 'APP') {
+ if (qualifier === ComponentQualifier.Application) {
return [...APPLICATION_METRICS];
}
if (showLeakMeasure(branchLike)) {
@@ -162,7 +162,7 @@ function retrieveComponentBase(
});
}
-export function retrieveComponentChildren(
+export async function retrieveComponentChildren(
componentKey: string,
qualifier: string,
instance: { mounted: boolean },
@@ -181,20 +181,34 @@ export function retrieveComponentChildren(
includeQGStatus: true,
});
- return getChildren(componentKey, metrics, {
+ const result = await getChildren(componentKey, metrics, {
ps: PAGE_SIZE,
s: 'qualifier,name',
...getBranchLikeQuery(branchLike),
- })
- .then(prepareChildren)
- .then((r) => {
- if (instance.mounted) {
- addComponentChildren(componentKey, r.components, r.total, r.page);
- storeChildrenBase(r.components);
- storeChildrenBreadcrumbs(componentKey, r.components);
+ }).then(prepareChildren);
+
+ if (instance.mounted && isPortfolioLike(qualifier)) {
+ await Promise.all(
+ result.components.map((c) => getComponentData({ component: c.refKey || c.key }))
+ ).then(
+ (data) => {
+ data.forEach(({ component: { analysisDate } }, i) => {
+ result.components[i].analysisDate = analysisDate;
+ });
+ },
+ () => {
+ // noop
}
- return r;
- });
+ );
+ }
+
+ if (instance.mounted) {
+ addComponentChildren(componentKey, result.components, result.total, result.page);
+ storeChildrenBase(result.components);
+ storeChildrenBreadcrumbs(componentKey, result.components);
+ }
+
+ return result;
}
function retrieveComponentBreadcrumbs(
diff --git a/server/sonar-web/src/main/js/components/intl/__mocks__/DateFromNow.tsx b/server/sonar-web/src/main/js/components/intl/__mocks__/DateFromNow.tsx
index 3c912090934..8304c482b39 100644
--- a/server/sonar-web/src/main/js/components/intl/__mocks__/DateFromNow.tsx
+++ b/server/sonar-web/src/main/js/components/intl/__mocks__/DateFromNow.tsx
@@ -26,5 +26,5 @@ interface Props {
}
export default function DateFromNow({ children, date }: Props) {
- return children && children(date.toString());
+ return children ? children(date.toString()) : date.toString();
}
diff --git a/server/sonar-web/src/main/js/types/types.ts b/server/sonar-web/src/main/js/types/types.ts
index a449b2a5935..07e7367f97e 100644
--- a/server/sonar-web/src/main/js/types/types.ts
+++ b/server/sonar-web/src/main/js/types/types.ts
@@ -113,6 +113,7 @@ export interface ComponentQualityProfile {
}
export interface ComponentMeasureIntern {
+ analysisDate?: string;
branch?: string;
description?: string;
isFavorite?: boolean;
diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties
index 257dd0798c0..f42e670123d 100644
--- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties
+++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties
@@ -3385,6 +3385,9 @@ code.open_component_page=Open Component's Page
code.search_placeholder=Search for files...
code.search_placeholder.portfolio=Search for projects and sub-portfolios...
code.parent_folder=Parent folder
+code.last_analysis_date=Last analysis
+code.name=Name
+code.pin=Pin file
#------------------------------------------------------------------------------