Browse Source

SONAR-15999 Drop the visualizations of projects as bubble charts

tags/9.4.0.54424
Wouter Admiraal 2 years ago
parent
commit
caf973b676
40 changed files with 73 additions and 2122 deletions
  1. 3
    3
      server/sonar-web/src/main/js/apps/projects/__tests__/utils-test.ts
  2. 6
    32
      server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx
  3. 12
    22
      server/sonar-web/src/main/js/apps/projects/components/PageHeader.tsx
  4. 1
    6
      server/sonar-web/src/main/js/apps/projects/components/PageSidebar.tsx
  5. 6
    21
      server/sonar-web/src/main/js/apps/projects/components/PerspectiveSelect.tsx
  6. 6
    34
      server/sonar-web/src/main/js/apps/projects/components/__tests__/AllProjects-test.tsx
  7. 2
    16
      server/sonar-web/src/main/js/apps/projects/components/__tests__/PageHeader-test.tsx
  8. 5
    22
      server/sonar-web/src/main/js/apps/projects/components/__tests__/PageSidebar-test.tsx
  9. 5
    18
      server/sonar-web/src/main/js/apps/projects/components/__tests__/PerspectiveSelect-test.tsx
  10. 0
    122
      server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/AllProjects-test.tsx.snap
  11. 21
    94
      server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/PageHeader-test.tsx.snap
  12. 0
    113
      server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/PerspectiveSelect-test.tsx.snap
  13. 5
    12
      server/sonar-web/src/main/js/apps/projects/query.ts
  14. 0
    68
      server/sonar-web/src/main/js/apps/projects/styles.css
  15. 1
    44
      server/sonar-web/src/main/js/apps/projects/utils.ts
  16. 0
    41
      server/sonar-web/src/main/js/apps/projects/visualizations/Coverage.tsx
  17. 0
    40
      server/sonar-web/src/main/js/apps/projects/visualizations/Duplications.tsx
  18. 0
    41
      server/sonar-web/src/main/js/apps/projects/visualizations/Maintainability.tsx
  19. 0
    41
      server/sonar-web/src/main/js/apps/projects/visualizations/Reliability.tsx
  20. 0
    194
      server/sonar-web/src/main/js/apps/projects/visualizations/Risk.tsx
  21. 0
    41
      server/sonar-web/src/main/js/apps/projects/visualizations/Security.tsx
  22. 0
    185
      server/sonar-web/src/main/js/apps/projects/visualizations/SimpleBubbleChart.tsx
  23. 0
    89
      server/sonar-web/src/main/js/apps/projects/visualizations/Visualizations.tsx
  24. 0
    26
      server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/Coverage-test.tsx
  25. 0
    26
      server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/Duplications-test.tsx
  26. 0
    26
      server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/Maintainability-test.tsx
  27. 0
    26
      server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/Reliability-test.tsx
  28. 0
    56
      server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/Risk-test.tsx
  29. 0
    26
      server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/Security-test.tsx
  30. 0
    59
      server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/SimpleBubbleChart-test.tsx
  31. 0
    32
      server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/Visualizations-test.tsx
  32. 0
    33
      server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/__snapshots__/Coverage-test.tsx.snap
  33. 0
    27
      server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/__snapshots__/Duplications-test.tsx.snap
  34. 0
    28
      server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/__snapshots__/Maintainability-test.tsx.snap
  35. 0
    28
      server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/__snapshots__/Reliability-test.tsx.snap
  36. 0
    179
      server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/__snapshots__/Risk-test.tsx.snap
  37. 0
    28
      server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/__snapshots__/Security-test.tsx.snap
  38. 0
    171
      server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/__snapshots__/SimpleBubbleChart-test.tsx.snap
  39. 0
    40
      server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/__snapshots__/Visualizations-test.tsx.snap
  40. 0
    12
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 3
- 3
server/sonar-web/src/main/js/apps/projects/__tests__/utils-test.ts View File

@@ -85,13 +85,13 @@ describe('formatDuration', () => {

describe('fetchProjects', () => {
it('correctly converts the passed arguments to the desired query format', async () => {
await utils.fetchProjects({ view: 'visualizations' }, true);
await utils.fetchProjects({}, true);
expect(searchProjects).toBeCalledWith({
f: 'analysisDate,leakPeriodDate',
facets: utils.FACETS.join(),
filter: 'isFavorite',
p: undefined,
ps: 99
ps: 50
});

await utils.fetchProjects({ view: 'leak' }, false, 3);
@@ -119,7 +119,7 @@ describe('fetchProjects', () => {
],
paging: { total: 2 }
});
await utils.fetchProjects({ view: 'visualizations' }, true).then(r => {
await utils.fetchProjects({}, true).then(r => {
expect(r).toEqual({
facets: {
new_coverage: { NO_DATA: 0 },

+ 6
- 32
server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx View File

@@ -34,11 +34,10 @@ import { get, save } from '../../../helpers/storage';
import { isLoggedIn } from '../../../helpers/users';
import { ComponentQualifier } from '../../../types/component';
import { CurrentUser, RawQuery } from '../../../types/types';
import { hasFilterParams, hasVisualizationParams, parseUrlQuery, Query } from '../query';
import { hasFilterParams, hasViewParams, parseUrlQuery, Query } from '../query';
import '../styles.css';
import { Facets, Project } from '../types';
import { fetchProjects, parseSorting, SORTING_SWITCH } from '../utils';
import Visualizations from '../visualizations/Visualizations';
import PageHeader from './PageHeader';
import PageSidebar from './PageSidebar';
import ProjectsList from './ProjectsList';
@@ -62,7 +61,6 @@ interface State {

export const LS_PROJECTS_SORT = 'sonarqube.projects.sort';
export const LS_PROJECTS_VIEW = 'sonarqube.projects.view';
export const LS_PROJECTS_VISUALIZATION = 'sonarqube.projects.visualization';

export class AllProjects extends React.PureComponent<Props, State> {
mounted = false;
@@ -131,7 +129,6 @@ export class AllProjects extends React.PureComponent<Props, State> {
const options: {
sort?: string;
view?: string;
visualization?: string;
} = {};
if (get(LS_PROJECTS_SORT)) {
options.sort = get(LS_PROJECTS_SORT) || undefined;
@@ -139,16 +136,11 @@ export class AllProjects extends React.PureComponent<Props, State> {
if (get(LS_PROJECTS_VIEW)) {
options.view = get(LS_PROJECTS_VIEW) || undefined;
}
if (get(LS_PROJECTS_VISUALIZATION)) {
options.visualization = get(LS_PROJECTS_VISUALIZATION) || undefined;
}
return options;
};

getView = () => this.state.query.view || 'overall';

getVisualization = () => this.state.query.visualization || 'risk';

handleClearAll = () => {
this.props.router.push({ pathname: this.props.location.pathname });
};
@@ -165,14 +157,12 @@ export class AllProjects extends React.PureComponent<Props, State> {
});
};

handlePerspectiveChange = ({ view, visualization }: { view: string; visualization?: string }) => {
handlePerspectiveChange = ({ view }: { view: string }) => {
const query: {
view: string | undefined;
visualization: string | undefined;
sort?: string | undefined;
} = {
view: view === 'overall' ? undefined : view,
visualization
view: view === 'overall' ? undefined : view
};

if (this.state.query.view === 'leak' || view === 'leak') {
@@ -189,16 +179,14 @@ export class AllProjects extends React.PureComponent<Props, State> {

save(LS_PROJECTS_SORT, query.sort);
save(LS_PROJECTS_VIEW, query.view);
save(LS_PROJECTS_VISUALIZATION, visualization);
};

handleQueryChange(initialMount: boolean) {
const query = parseUrlQuery(this.props.location.query);
const savedOptions = this.getStorageOptions();
const savedOptionsSet = savedOptions.sort || savedOptions.view || savedOptions.visualization;
const savedOptionsSet = savedOptions.sort || savedOptions.view;

// if there is no visualization parameters (sort, view, visualization), but there are saved preferences in the localStorage
if (initialMount && !hasVisualizationParams(query) && savedOptionsSet) {
if (initialMount && !hasViewParams(query) && savedOptionsSet) {
this.props.router.replace({ pathname: this.props.location.pathname, query: savedOptions });
} else {
this.fetchProjects(query);
@@ -244,7 +232,6 @@ export class AllProjects extends React.PureComponent<Props, State> {
onQueryChange={this.updateLocationQuery}
query={this.state.query}
view={this.getView()}
visualization={this.getVisualization()}
/>
</div>
</div>
@@ -263,12 +250,10 @@ export class AllProjects extends React.PureComponent<Props, State> {
onPerspectiveChange={this.handlePerspectiveChange}
onQueryChange={this.updateLocationQuery}
onSortChange={this.handleSortChange}
projects={this.state.projects}
query={this.state.query}
selectedSort={this.getSort()}
total={this.state.total}
view={this.getView()}
visualization={this.getVisualization()}
/>
</div>
</div>
@@ -280,18 +265,7 @@ export class AllProjects extends React.PureComponent<Props, State> {
return <DeferredSpinner />;
}

return this.getView() === 'visualizations' ? (
<div className="layout-page-main-inner">
{this.state.projects && (
<Visualizations
projects={this.state.projects}
sort={this.state.query.sort}
total={this.state.total}
visualization={this.getVisualization()}
/>
)}
</div>
) : (
return (
<div className="layout-page-main-inner">
{this.state.projects && (
<ProjectsList

+ 12
- 22
server/sonar-web/src/main/js/apps/projects/components/PageHeader.tsx View File

@@ -20,12 +20,10 @@
import classNames from 'classnames';
import * as React from 'react';
import HomePageSelect from '../../../components/controls/HomePageSelect';
import Tooltip from '../../../components/controls/Tooltip';
import { translate } from '../../../helpers/l10n';
import { isLoggedIn } from '../../../helpers/users';
import { CurrentUser, RawQuery } from '../../../types/types';
import SearchFilterContainer from '../filters/SearchFilterContainer';
import { Project } from '../types';
import ApplicationCreation from './ApplicationCreation';
import PerspectiveSelect from './PerspectiveSelect';
import ProjectCreationMenu from './ProjectCreationMenu';
@@ -34,24 +32,19 @@ import ProjectsSortingSelect from './ProjectsSortingSelect';
interface Props {
currentUser: CurrentUser;
loading: boolean;
onPerspectiveChange: (x: { view: string; visualization?: string }) => void;
onPerspectiveChange: (x: { view: string }) => void;
onQueryChange: (change: RawQuery) => void;
onSortChange: (sort: string, desc: boolean) => void;
projects?: Project[];
query: RawQuery;
selectedSort: string;
total?: number;
view: string;
visualization?: string;
}

export default function PageHeader(props: Props) {
const { loading, total, projects, currentUser, view } = props;
const limitReached = projects != null && total != null && projects.length < total;
const { loading, total, currentUser, view } = props;
const defaultOption = isLoggedIn(currentUser) ? 'name' : 'analysis_date';

const sortingDisabled = view === 'visualizations' && !limitReached;

return (
<div className="page-header">
<div className="display-flex-space-between spacer-top">
@@ -81,21 +74,18 @@ export default function PageHeader(props: Props) {
<PerspectiveSelect
className="projects-topbar-item js-projects-perspective-select"
onChange={props.onPerspectiveChange}
view={props.view}
visualization={props.visualization}
view={view}
/>

<Tooltip overlay={sortingDisabled ? translate('projects.sort.disabled') : undefined}>
<div className={classNames('projects-topbar-item', { disabled: sortingDisabled })}>
<ProjectsSortingSelect
className="js-projects-sorting-select"
defaultOption={defaultOption}
onChange={props.onSortChange}
selectedSort={props.selectedSort}
view={props.view}
/>
</div>
</Tooltip>
<div className={classNames('projects-topbar-item')}>
<ProjectsSortingSelect
className="js-projects-sorting-select"
defaultOption={defaultOption}
onChange={props.onSortChange}
selectedSort={props.selectedSort}
view={view}
/>
</div>
</div>
</div>
</div>

+ 1
- 6
server/sonar-web/src/main/js/apps/projects/components/PageSidebar.tsx View File

@@ -50,11 +50,10 @@ export interface PageSidebarProps {
onQueryChange: (change: RawQuery) => void;
query: RawQuery;
view: string;
visualization: string;
}

export default function PageSidebar(props: PageSidebarProps) {
const { applicationsEnabled, facets, onQueryChange, query, view, visualization } = props;
const { applicationsEnabled, facets, onQueryChange, query, view } = props;
const isFiltered = hasFilterParams(query);
const isLeakView = view === 'leak';
const maxFacetValue = getMaxFacetValue(facets);
@@ -63,10 +62,6 @@ export default function PageSidebar(props: PageSidebarProps) {
let linkQuery: RawQuery | undefined = undefined;
if (view !== 'overall') {
linkQuery = { view };

if (view === 'visualizations') {
linkQuery.visualization = visualization;
}
}

return (

+ 6
- 21
server/sonar-web/src/main/js/apps/projects/components/PerspectiveSelect.tsx View File

@@ -21,31 +21,24 @@ import { omit } from 'lodash';
import * as React from 'react';
import { components, OptionProps } from 'react-select';
import Select from '../../../components/controls/Select';
import BubblesIcon from '../../../components/icons/BubblesIcon';
import ListIcon from '../../../components/icons/ListIcon';
import { translate } from '../../../helpers/l10n';
import { VIEWS, VISUALIZATIONS } from '../utils';
import { VIEWS } from '../utils';

interface Props {
className?: string;
onChange: (x: { view: string; visualization?: string }) => void;
onChange: (x: { view: string }) => void;
view: string;
visualization?: string;
}

export interface PerspectiveOption {
type: string;
value: string;
label: string;
}

export default class PerspectiveSelect extends React.PureComponent<Props> {
handleChange = (option: PerspectiveOption) => {
if (option && option.type === 'view') {
this.props.onChange({ view: option.value });
} else if (option && option.type === 'visualization') {
this.props.onChange({ view: 'visualizations', visualization: option.value });
}
this.props.onChange({ view: option.value });
};

perspectiveOptionRender = (props: OptionProps<PerspectiveOption, false>) => {
@@ -54,26 +47,18 @@ export default class PerspectiveSelect extends React.PureComponent<Props> {
<components.Option
{...omit(props, ['children', 'className'])}
className={`it__projects-perspective-option-${data.value} ${className}`}>
{data.type === 'view' && <ListIcon className="little-spacer-right" />}
{data.type === 'visualization' && <BubblesIcon className="little-spacer-right" />}
<ListIcon className="little-spacer-right" />
{props.children}
</components.Option>
);
};

render() {
const { view, visualization } = this.props;
const perspective = view === 'visualizations' ? visualization : view;
const { view } = this.props;
const options: PerspectiveOption[] = [
...VIEWS.map(opt => ({
type: 'view',
value: opt.value,
label: translate('projects.view', opt.label)
})),
...VISUALIZATIONS.map(opt => ({
type: 'visualization',
value: opt,
label: translate('projects.visualization', opt)
}))
];
return (
@@ -89,7 +74,7 @@ export default class PerspectiveSelect extends React.PureComponent<Props> {
}}
options={options}
isSearchable={false}
value={options.find(option => option.value === perspective)}
value={options.find(option => option.value === view)}
/>
</div>
);

+ 6
- 34
server/sonar-web/src/main/js/apps/projects/components/__tests__/AllProjects-test.tsx View File

@@ -22,12 +22,7 @@ import * as React from 'react';
import { get, save } from '../../../../helpers/storage';
import { ComponentQualifier } from '../../../../types/component';
import { Dict } from '../../../../types/types';
import {
AllProjects,
LS_PROJECTS_SORT,
LS_PROJECTS_VIEW,
LS_PROJECTS_VISUALIZATION
} from '../AllProjects';
import { AllProjects, LS_PROJECTS_SORT, LS_PROJECTS_VIEW } from '../AllProjects';

jest.mock(
'../ProjectsList',
@@ -78,8 +73,6 @@ beforeEach(() => {
it('renders', () => {
const wrapper = shallowRender();
expect(wrapper).toMatchSnapshot();
wrapper.setState({ query: { view: 'visualizations' } });
expect(wrapper).toMatchSnapshot();
});

it('fetches projects', () => {
@@ -103,8 +96,7 @@ it('fetches projects', () => {
size: undefined,
sort: undefined,
tags: undefined,
view: undefined,
visualization: undefined
view: undefined
},
false
);
@@ -113,8 +105,7 @@ it('fetches projects', () => {
it('redirects to the saved search', () => {
const localeStorageMock: Dict<string> = {
[LS_PROJECTS_VIEW]: 'leak',
[LS_PROJECTS_SORT]: 'coverage',
[LS_PROJECTS_VISUALIZATION]: 'security'
[LS_PROJECTS_SORT]: 'coverage'
};

(get as jest.Mock).mockImplementation((key: string) => localeStorageMock[key]);
@@ -125,8 +116,7 @@ it('redirects to the saved search', () => {
pathname: '/projects',
query: {
view: localeStorageMock[LS_PROJECTS_VIEW],
sort: localeStorageMock[LS_PROJECTS_SORT],
visualization: localeStorageMock[LS_PROJECTS_VISUALIZATION]
sort: localeStorageMock[LS_PROJECTS_SORT]
}
});
});
@@ -145,11 +135,10 @@ it('changes perspective to leak', () => {
wrapper.find('PageHeader').prop<Function>('onPerspectiveChange')({ view: 'leak' });
expect(push).lastCalledWith({
pathname: '/projects',
query: { view: 'leak', visualization: undefined }
query: { view: 'leak' }
});
expect(save).toHaveBeenCalledWith(LS_PROJECTS_SORT, undefined);
expect(save).toHaveBeenCalledWith(LS_PROJECTS_VIEW, 'leak');
expect(save).toHaveBeenCalledWith(LS_PROJECTS_VISUALIZATION, undefined);
});

it('updates sorting when changing perspective from leak', () => {
@@ -161,27 +150,10 @@ it('updates sorting when changing perspective from leak', () => {
});
expect(push).lastCalledWith({
pathname: '/projects',
query: { sort: 'coverage', view: undefined, visualization: undefined }
query: { sort: 'coverage', view: undefined }
});
expect(save).toHaveBeenCalledWith(LS_PROJECTS_SORT, 'coverage');
expect(save).toHaveBeenCalledWith(LS_PROJECTS_VIEW, undefined);
expect(save).toHaveBeenCalledWith(LS_PROJECTS_VISUALIZATION, undefined);
});

it('changes perspective to risk visualization', () => {
const push = jest.fn();
const wrapper = shallowRender({}, push);
wrapper.find('PageHeader').prop<Function>('onPerspectiveChange')({
view: 'visualizations',
visualization: 'risk'
});
expect(push).lastCalledWith({
pathname: '/projects',
query: { view: 'visualizations', visualization: 'risk' }
});
expect(save).toHaveBeenCalledWith(LS_PROJECTS_SORT, undefined);
expect(save).toHaveBeenCalledWith(LS_PROJECTS_VIEW, 'visualizations');
expect(save).toHaveBeenCalledWith(LS_PROJECTS_VISUALIZATION, 'risk');
});

it('handles favorite projects', () => {

+ 2
- 16
server/sonar-web/src/main/js/apps/projects/components/__tests__/PageHeader-test.tsx View File

@@ -37,23 +37,11 @@ it('should not render projects total', () => {
).toBe(false);
});

it('should render disabled sorting options for visualizations', () => {
expect(
shallowRender({
open: true,
total: undefined,
view: 'visualizations',
visualization: 'coverage'
})
).toMatchSnapshot();
});

it('should render switch the default sorting option for anonymous users', () => {
expect(
shallowRender({
currentUser: { isLoggedIn: true },
open: true,
visualization: 'risk'
open: true
}).find('ProjectsSortingSelect')
).toMatchSnapshot();

@@ -61,8 +49,7 @@ it('should render switch the default sorting option for anonymous users', () =>
shallowRender({
currentUser: { isLoggedIn: false },
open: true,
view: 'leak',
visualization: 'risk'
view: 'leak'
}).find('ProjectsSortingSelect')
).toMatchSnapshot();
});
@@ -75,7 +62,6 @@ function shallowRender(props?: {}) {
onPerspectiveChange={jest.fn()}
onQueryChange={jest.fn()}
onSortChange={jest.fn()}
projects={[]}
query={{ search: 'test' }}
selectedSort="size"
total={12}

+ 5
- 22
server/sonar-web/src/main/js/apps/projects/components/__tests__/PageSidebar-test.tsx View File

@@ -24,8 +24,7 @@ import PageSidebar, { PageSidebarProps } from '../PageSidebar';
it('should render correctly', () => {
const sidebar = shallowRender({
query: { size: '3' },
view: 'overall',
visualization: 'risk'
view: 'overall'
});

expect(sidebar).toMatchSnapshot();
@@ -35,8 +34,7 @@ it('should render correctly with no applications', () => {
const sidebar = shallowRender({
applicationsEnabled: false,
query: { size: '3' },
view: 'overall',
visualization: 'risk'
view: 'overall'
});

expect(sidebar).toMatchSnapshot();
@@ -45,8 +43,7 @@ it('should render correctly with no applications', () => {
it('should render `leak` view correctly', () => {
const sidebar = shallowRender({
query: { view: 'leak' },
view: 'leak',
visualization: 'risk'
view: 'leak'
});
expect(sidebar).toMatchSnapshot();
});
@@ -55,33 +52,19 @@ it('should render `leak` view correctly with no applications', () => {
const sidebar = shallowRender({
applicationsEnabled: false,
query: { view: 'leak' },
view: 'leak',
visualization: 'risk'
view: 'leak'
});
expect(sidebar).toMatchSnapshot();
});

it('reset function should work correctly with view and visualizations', () => {
const sidebar = shallowRender({
query: { view: 'visualizations', visualization: 'bugs' },
view: 'visualizations',
visualization: 'bugs'
});

expect(sidebar.find('ClearAll').exists()).toBe(false);
sidebar.setProps({ query: { size: '3' } });
expect(sidebar.find('ClearAll').exists()).toBe(true);
});

function shallowRender(overrides: Partial<PageSidebarProps> = {}) {
return shallow(
<PageSidebar
applicationsEnabled={true}
onClearAll={jest.fn()}
onQueryChange={jest.fn()}
query={{ view: 'visualizations', visualization: 'bugs' }}
query={{ view: 'overall' }}
view="overall"
visualization="bugs"
{...overrides}
/>
);

+ 5
- 18
server/sonar-web/src/main/js/apps/projects/components/__tests__/PerspectiveSelect-test.tsx View File

@@ -19,7 +19,6 @@
*/
import { shallow } from 'enzyme';
import * as React from 'react';
import BubblesIcon from '../../../../components/icons/BubblesIcon';
import ListIcon from '../../../../components/icons/ListIcon';
import { mockReactSelectOptionProps } from '../../../../helpers/mocks/react-select';
import PerspectiveSelect from '../PerspectiveSelect';
@@ -28,37 +27,25 @@ it('should render correctly', () => {
expect(shallow(<PerspectiveSelect onChange={jest.fn()} view="overall" />)).toMatchSnapshot();
});

it('should render with coverage selected', () => {
expect(shallowRender({ view: 'visualizations', visualization: 'coverage' })).toMatchSnapshot();
});

it('should render option correctly', () => {
const wrapper = shallowRender();
const OptionRender = wrapper.instance().perspectiveOptionRender;
let option = shallow(
<OptionRender {...mockReactSelectOptionProps({ type: 'view', value: 'test', label: 'test' })} />
const option = shallow(
<OptionRender {...mockReactSelectOptionProps({ value: 'test', label: 'test' })} />
);

expect(option.find(ListIcon).type).toBeDefined();
option = shallow(
<OptionRender
{...mockReactSelectOptionProps({ type: 'visualization', value: 'test', label: '' })}
/>
);
expect(option.find(BubblesIcon).type).toBeDefined();
});

it('should handle perspective change correctly', () => {
const onChange = jest.fn();
const instance = shallowRender({ onChange }).instance();
instance.handleChange({ label: 'overall', value: 'overall', type: 'view' });
instance.handleChange({ label: 'leak', value: 'leak', type: 'view' });
instance.handleChange({ label: 'coverage', value: 'coverage', type: 'visualization' });
instance.handleChange({ label: 'overall', value: 'overall' });
instance.handleChange({ label: 'leak', value: 'leak' });
expect(onChange.mock.calls).toMatchSnapshot();
});

function shallowRender(overrides: Partial<PerspectiveSelect['props']> = {}) {
return shallow<PerspectiveSelect>(
<PerspectiveSelect onChange={jest.fn()} view="visualizations" {...overrides} />
<PerspectiveSelect onChange={jest.fn()} view="overall" {...overrides} />
);
}

+ 0
- 122
server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/AllProjects-test.tsx.snap View File

@@ -84,18 +84,6 @@ exports[`renders 1`] = `
onPerspectiveChange={[Function]}
onQueryChange={[Function]}
onSortChange={[Function]}
projects={
Array [
Object {
"key": "foo",
"measures": Object {},
"name": "Foo",
"qualifier": "TRK",
"tags": Array [],
"visibility": "public",
},
]
}
query={
Object {
"coverage": undefined,
@@ -119,13 +107,11 @@ exports[`renders 1`] = `
"sort": undefined,
"tags": undefined,
"view": undefined,
"visualization": undefined,
}
}
selectedSort="name"
total={0}
view="overall"
visualization="risk"
/>
</div>
</div>
@@ -178,7 +164,6 @@ exports[`renders 1`] = `
"sort": undefined,
"tags": undefined,
"view": undefined,
"visualization": undefined,
}
}
/>
@@ -194,110 +179,3 @@ exports[`renders 1`] = `
</div>
</div>
`;

exports[`renders 2`] = `
<div
className="layout-page projects-page"
id="projects-page"
>
<Suggestions
suggestions="projects"
/>
<Helmet
defer={false}
encodeSpecialCharacters={true}
prioritizeSeoTags={false}
title="projects.page"
/>
<h1
className="a11y-hidden"
>
projects.page
</h1>
<ScreenPositionHelper
className="layout-page-side-outer"
>
<Component />
</ScreenPositionHelper>
<div
className="layout-page-main"
>
<A11ySkipTarget
anchor="projects_main"
/>
<div
role="main"
>
<h2
className="a11y-hidden"
>
list_of_projects
</h2>
<div
className="layout-page-header-panel layout-page-main-header"
>
<div
className="layout-page-header-panel-inner layout-page-main-header-inner"
>
<div
className="layout-page-main-inner"
>
<PageHeader
currentUser={
Object {
"isLoggedIn": true,
}
}
loading={false}
onPerspectiveChange={[Function]}
onQueryChange={[Function]}
onSortChange={[Function]}
projects={
Array [
Object {
"key": "foo",
"measures": Object {},
"name": "Foo",
"qualifier": "TRK",
"tags": Array [],
"visibility": "public",
},
]
}
query={
Object {
"view": "visualizations",
}
}
selectedSort="name"
total={0}
view="visualizations"
visualization="risk"
/>
</div>
</div>
</div>
<div
className="layout-page-main-inner"
>
<Visualizations
projects={
Array [
Object {
"key": "foo",
"measures": Object {},
"name": "Foo",
"qualifier": "TRK",
"tags": Array [],
"visibility": "public",
},
]
}
total={0}
visualization="risk"
/>
</div>
</div>
</div>
</div>
`;

+ 21
- 94
server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/PageHeader-test.tsx.snap View File

@@ -60,19 +60,17 @@ exports[`should render correctly 1`] = `
onChange={[MockFunction]}
view="overall"
/>
<Tooltip>
<div
className="projects-topbar-item"
>
<ProjectsSortingSelect
className="js-projects-sorting-select"
defaultOption="analysis_date"
onChange={[MockFunction]}
selectedSort="size"
view="overall"
/>
</div>
</Tooltip>
<div
className="projects-topbar-item"
>
<ProjectsSortingSelect
className="js-projects-sorting-select"
defaultOption="analysis_date"
onChange={[MockFunction]}
selectedSort="size"
view="overall"
/>
</div>
</div>
</div>
</div>
@@ -138,88 +136,17 @@ exports[`should render correctly while loading 1`] = `
onChange={[MockFunction]}
view="overall"
/>
<Tooltip>
<div
className="projects-topbar-item"
>
<ProjectsSortingSelect
className="js-projects-sorting-select"
defaultOption="analysis_date"
onChange={[MockFunction]}
selectedSort="size"
view="overall"
/>
</div>
</Tooltip>
</div>
</div>
</div>
`;

exports[`should render disabled sorting options for visualizations 1`] = `
<div
className="page-header"
>
<div
className="display-flex-space-between spacer-top"
>
<SearchFilterContainer
onQueryChange={[MockFunction]}
query={
Object {
"search": "test",
}
}
/>
<div
className="display-flex-center"
>
<Connect(withCurrentUser(ProjectCreationMenu))
className="little-spacer-right"
/>
<Connect(withAppState(Connect(withCurrentUser(withRouter(ApplicationCreation)))))
className="little-spacer-right"
/>
<Connect(HomePageSelect)
className="spacer-left little-spacer-right"
currentPage={
Object {
"type": "PROJECTS",
}
}
/>
</div>
</div>
<div
className="big-spacer-top display-flex-space-between"
>
<div
className="display-flex-center"
/>
<div
className="display-flex-center"
>
<PerspectiveSelect
className="projects-topbar-item js-projects-perspective-select"
onChange={[MockFunction]}
view="visualizations"
visualization="coverage"
/>
<Tooltip
overlay="projects.sort.disabled"
<div
className="projects-topbar-item"
>
<div
className="projects-topbar-item disabled"
>
<ProjectsSortingSelect
className="js-projects-sorting-select"
defaultOption="analysis_date"
onChange={[MockFunction]}
selectedSort="size"
view="visualizations"
/>
</div>
</Tooltip>
<ProjectsSortingSelect
className="js-projects-sorting-select"
defaultOption="analysis_date"
onChange={[MockFunction]}
selectedSort="size"
view="overall"
/>
</div>
</div>
</div>
</div>

+ 0
- 113
server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/PerspectiveSelect-test.tsx.snap View File

@@ -12,12 +12,6 @@ Array [
"view": "leak",
},
],
Array [
Object {
"view": "visualizations",
"visualization": "coverage",
},
],
]
`;

@@ -44,127 +38,20 @@ exports[`should render correctly 1`] = `
Array [
Object {
"label": "projects.view.overall",
"type": "view",
"value": "overall",
},
Object {
"label": "projects.view.new_code",
"type": "view",
"value": "leak",
},
Object {
"label": "projects.visualization.risk",
"type": "visualization",
"value": "risk",
},
Object {
"label": "projects.visualization.reliability",
"type": "visualization",
"value": "reliability",
},
Object {
"label": "projects.visualization.security",
"type": "visualization",
"value": "security",
},
Object {
"label": "projects.visualization.maintainability",
"type": "visualization",
"value": "maintainability",
},
Object {
"label": "projects.visualization.coverage",
"type": "visualization",
"value": "coverage",
},
Object {
"label": "projects.visualization.duplications",
"type": "visualization",
"value": "duplications",
},
]
}
value={
Object {
"label": "projects.view.overall",
"type": "view",
"value": "overall",
}
}
/>
</div>
`;

exports[`should render with coverage selected 1`] = `
<div>
<label
id="aria-projects-perspective"
>
projects.perspective
:
</label>
<Select
aria-labelledby="aria-projects-perspective"
className="little-spacer-left input-medium it__projects-perspective-select"
components={
Object {
"Option": [Function],
}
}
isClearable={false}
isSearchable={false}
onChange={[Function]}
options={
Array [
Object {
"label": "projects.view.overall",
"type": "view",
"value": "overall",
},
Object {
"label": "projects.view.new_code",
"type": "view",
"value": "leak",
},
Object {
"label": "projects.visualization.risk",
"type": "visualization",
"value": "risk",
},
Object {
"label": "projects.visualization.reliability",
"type": "visualization",
"value": "reliability",
},
Object {
"label": "projects.visualization.security",
"type": "visualization",
"value": "security",
},
Object {
"label": "projects.visualization.maintainability",
"type": "visualization",
"value": "maintainability",
},
Object {
"label": "projects.visualization.coverage",
"type": "visualization",
"value": "coverage",
},
Object {
"label": "projects.visualization.duplications",
"type": "visualization",
"value": "duplications",
},
]
}
value={
Object {
"label": "projects.visualization.coverage",
"type": "visualization",
"value": "coverage",
}
}
/>
</div>
`;

+ 5
- 12
server/sonar-web/src/main/js/apps/projects/query.ts View File

@@ -19,7 +19,6 @@
*/
import { ComponentQualifier } from '../../types/component';
import { Dict, RawQuery } from '../../types/types';
import { VISUALIZATIONS } from './utils';

type Level = 'ERROR' | 'WARN' | 'OK';

@@ -45,7 +44,6 @@ export interface Query {
search?: string;
sort?: string;
view?: string;
visualization?: string;
[x: string]: string | number | string[] | undefined;
}

@@ -71,8 +69,7 @@ export function parseUrlQuery(urlQuery: RawQuery): Query {
qualifier: getAsQualifier(urlQuery['qualifier']),
search: getAsString(urlQuery['search']),
sort: getAsString(urlQuery['sort']),
view: getView(urlQuery['view']),
visualization: getVisualization(urlQuery['visualization'])
view: getView(urlQuery['view'])
};
}

@@ -121,17 +118,17 @@ export function convertToFilter(query: Query, isFavorite: boolean): string {
return conditions.join(' and ');
}

const visualizationParams = ['sort', 'view', 'visualization'];
const viewParems = ['sort', 'view'];

export function hasFilterParams(query: Query) {
return Object.keys(query)
.filter(key => !visualizationParams.includes(key))
.filter(key => !viewParems.includes(key))
.some(key => query[key] !== undefined);
}

export function hasVisualizationParams(query: Query) {
export function hasViewParams(query: Query) {
return Object.keys(query)
.filter(key => visualizationParams.includes(key))
.filter(key => viewParems.includes(key))
.some(key => query[key] !== undefined);
}

@@ -172,10 +169,6 @@ function getView(value: any): string | undefined {
return typeof value !== 'string' || value === 'overall' ? undefined : value;
}

function getVisualization(value: string): string | undefined {
return VISUALIZATIONS.includes(value) ? value : undefined;
}

function convertIssuesRating(metric: string, rating: number): string {
if (rating > 1 && rating < 5) {
return `${metric} >= ${rating}`;

+ 0
- 68
server/sonar-web/src/main/js/apps/projects/styles.css View File

@@ -36,15 +36,6 @@
padding-left: 32px;
}

.projects-topbar-item.disabled {
cursor: not-allowed;
opacity: 0.4;
}

.projects-topbar-item.disabled * {
pointer-events: none !important;
}

.projects-topbar-item-search {
position: relative;
flex: 1;
@@ -99,65 +90,6 @@
background-color: var(--blue);
}

.projects-visualization {
position: relative;
height: 600px;
}

.projects-visualization .measure-details-bubble-chart-axis.y {
width: 300px;
left: 15px;
margin-top: 150px;
transform-origin: 0 0;
text-align: center;
}

.projects-visualizations-footer {
margin-top: 8px;
padding: 15px 60px;
border-top: 1px solid var(--barBorderColor);
font-size: var(--smallFontSize);
line-height: 1.4;
text-align: center;
}

.projects-visualizations-footer .note {
font-style: italic;
}

.measure-details-bubble-chart-title {
position: absolute;
left: 20px;
}

.measure-details-bubble-chart-axis {
position: absolute;
color: var(--secondFontColor);
font-size: var(--smallFontSize);
}

.measure-details-bubble-chart-axis.x {
left: 50%;
bottom: 16px;
width: 500px;
margin-left: -250px;
text-align: center;
}

.measure-details-bubble-chart-axis.y {
top: 50%;
left: -20px;
transform: rotate(-90deg);
}

.measure-details-bubble-chart-axis.size {
display: flex;
align-items: center;
justify-content: space-around;
top: 16px;
width: 100%;
}

.projects-empty-list {
padding: calc(4 * var(--gridSize)) 0;
text-align: center;

+ 1
- 44
server/sonar-web/src/main/js/apps/projects/utils.ts View File

@@ -84,17 +84,7 @@ export const VIEWS = [
{ value: 'leak', label: 'new_code' }
];

export const VISUALIZATIONS = [
'risk',
'reliability',
'security',
'maintainability',
'coverage',
'duplications'
];

const PAGE_SIZE = 50;
const PAGE_SIZE_VISUALIZATIONS = 99;

const METRICS = [
MetricKey.alert_status,
@@ -129,37 +119,6 @@ const LEAK_METRICS = [
MetricKey.projects
];

const METRICS_BY_VISUALIZATION: Dict<string[]> = {
risk: [
MetricKey.reliability_rating,
MetricKey.security_rating,
MetricKey.coverage,
MetricKey.ncloc,
MetricKey.sqale_index
],
// x, y, size, color
reliability: [
MetricKey.ncloc,
MetricKey.reliability_remediation_effort,
MetricKey.bugs,
MetricKey.reliability_rating
],
security: [
MetricKey.ncloc,
MetricKey.security_remediation_effort,
MetricKey.vulnerabilities,
MetricKey.security_rating
],
maintainability: [
MetricKey.ncloc,
MetricKey.sqale_index,
MetricKey.code_smells,
MetricKey.sqale_rating
],
coverage: [MetricKey.complexity, MetricKey.coverage, MetricKey.uncovered_lines],
duplications: [MetricKey.ncloc, MetricKey.duplicated_lines_density, MetricKey.duplicated_blocks]
};

export const FACETS = [
'reliability_rating',
'security_rating',
@@ -200,7 +159,7 @@ export function parseSorting(sort: string): { sortValue: string; sortDesc: boole
}

export function fetchProjects(query: Query, isFavorite: boolean, pageIndex = 1) {
const ps = query.view === 'visualizations' ? PAGE_SIZE_VISUALIZATIONS : PAGE_SIZE;
const ps = PAGE_SIZE;
const data = convertToQueryData(query, isFavorite, {
p: pageIndex > 1 ? pageIndex : undefined,
ps,
@@ -233,8 +192,6 @@ export function fetchProjects(query: Query, isFavorite: boolean, pageIndex = 1)

function defineMetrics(query: Query): string[] {
switch (query.view) {
case 'visualizations':
return METRICS_BY_VISUALIZATION[query.visualization || 'risk'];
case 'leak':
return LEAK_METRICS;
default:

+ 0
- 41
server/sonar-web/src/main/js/apps/projects/visualizations/Coverage.tsx View File

@@ -1,41 +0,0 @@
/*
* 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 * as React from 'react';
import { translate } from '../../../helpers/l10n';
import { Project } from '../types';
import SimpleBubbleChart from './SimpleBubbleChart';

interface Props {
helpText: string;
projects: Project[];
}

export default function Coverage(props: Props) {
return (
<SimpleBubbleChart
{...props}
sizeMetric={{ key: 'uncovered_lines', type: 'SHORT_INT' }}
title={translate('projects.visualization', 'coverage')}
xMetric={{ key: 'complexity', type: 'SHORT_INT' }}
yDomain={[100, 0]}
yMetric={{ key: 'coverage', type: 'PERCENT' }}
/>
);
}

+ 0
- 40
server/sonar-web/src/main/js/apps/projects/visualizations/Duplications.tsx View File

@@ -1,40 +0,0 @@
/*
* 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 * as React from 'react';
import { translate } from '../../../helpers/l10n';
import { Project } from '../types';
import SimpleBubbleChart from './SimpleBubbleChart';

interface Props {
helpText: string;
projects: Project[];
}

export default function Duplications(props: Props) {
return (
<SimpleBubbleChart
{...props}
sizeMetric={{ key: 'duplicated_blocks', type: 'SHORT_INT' }}
title={translate('projects.visualization', 'duplications')}
xMetric={{ key: 'ncloc', type: 'SHORT_INT' }}
yMetric={{ key: 'duplicated_lines_density', type: 'PERCENT' }}
/>
);
}

+ 0
- 41
server/sonar-web/src/main/js/apps/projects/visualizations/Maintainability.tsx View File

@@ -1,41 +0,0 @@
/*
* 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 * as React from 'react';
import { translate } from '../../../helpers/l10n';
import { Project } from '../types';
import SimpleBubbleChart from './SimpleBubbleChart';

interface Props {
helpText: string;
projects: Project[];
}

export default function Maintainability(props: Props) {
return (
<SimpleBubbleChart
{...props}
colorMetric="sqale_rating"
sizeMetric={{ key: 'code_smells', type: 'SHORT_INT' }}
title={translate('projects.visualization', 'maintainability')}
xMetric={{ key: 'ncloc', type: 'SHORT_INT' }}
yMetric={{ key: 'sqale_index', type: 'SHORT_WORK_DUR' }}
/>
);
}

+ 0
- 41
server/sonar-web/src/main/js/apps/projects/visualizations/Reliability.tsx View File

@@ -1,41 +0,0 @@
/*
* 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 * as React from 'react';
import { translate } from '../../../helpers/l10n';
import { Project } from '../types';
import SimpleBubbleChart from './SimpleBubbleChart';

interface Props {
helpText: string;
projects: Project[];
}

export default function Reliability(props: Props) {
return (
<SimpleBubbleChart
{...props}
colorMetric="reliability_rating"
sizeMetric={{ key: 'bugs', type: 'SHORT_INT' }}
title={translate('projects.visualization', 'reliability')}
xMetric={{ key: 'ncloc', type: 'SHORT_INT' }}
yMetric={{ key: 'reliability_remediation_effort', type: 'SHORT_WORK_DUR' }}
/>
);
}

+ 0
- 194
server/sonar-web/src/main/js/apps/projects/visualizations/Risk.tsx View File

@@ -1,194 +0,0 @@
/*
* 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 * as React from 'react';
import BubbleChart from '../../../components/charts/BubbleChart';
import ColorRatingsLegend from '../../../components/charts/ColorRatingsLegend';
import HelpTooltip from '../../../components/controls/HelpTooltip';
import QualifierIcon from '../../../components/icons/QualifierIcon';
import { RATING_COLORS } from '../../../helpers/constants';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { formatMeasure } from '../../../helpers/measures';
import { isDefined } from '../../../helpers/types';
import { getProjectUrl } from '../../../helpers/urls';
import { ComponentQualifier } from '../../../types/component';
import { Project } from '../types';

const X_METRIC = 'sqale_index';
const X_METRIC_TYPE = 'SHORT_WORK_DUR';
const Y_METRIC = 'coverage';
const Y_METRIC_TYPE = 'PERCENT';
const SIZE_METRIC = 'ncloc';
const SIZE_METRIC_TYPE = 'SHORT_INT';
const COLOR_METRIC_1 = 'reliability_rating';
const COLOR_METRIC_2 = 'security_rating';
const COLOR_METRIC_TYPE = 'RATING';

interface Props {
helpText: string;
projects: Project[];
}

interface State {
ratingFilters: { [rating: number]: boolean };
}

export default class Risk extends React.PureComponent<Props, State> {
state: State = {
ratingFilters: {}
};

getMetricTooltip(metric: { key: string; type: string }, value?: number) {
const name = translate('metric', metric.key, 'name');
const formattedValue = value != null ? formatMeasure(value, metric.type) : '–';
return (
<div>
{name}
{': '}
{formattedValue}
</div>
);
}

getTooltip(
project: Project,
x?: number,
y?: number,
size?: number,
color1?: number,
color2?: number
) {
return (
<div className="text-left">
<div className="little-spacer-bottom display-flex-center display-flex-space-between">
<strong>{project.name}</strong>

{project.qualifier === ComponentQualifier.Application && (
<div className="big-spacer-left nowrap">
<QualifierIcon
className="little-spacer-right"
fill="currentColor"
qualifier={ComponentQualifier.Application}
/>
{translate('qualifier.APP')}
</div>
)}
</div>
{this.getMetricTooltip({ key: COLOR_METRIC_1, type: COLOR_METRIC_TYPE }, color1)}
{this.getMetricTooltip({ key: COLOR_METRIC_2, type: COLOR_METRIC_TYPE }, color2)}
{this.getMetricTooltip({ key: Y_METRIC, type: Y_METRIC_TYPE }, y)}
{this.getMetricTooltip({ key: X_METRIC, type: X_METRIC_TYPE }, x)}
{this.getMetricTooltip({ key: SIZE_METRIC, type: SIZE_METRIC_TYPE }, size)}
</div>
);
}

handleRatingFilterClick = (selection: number) => {
this.setState(({ ratingFilters }) => {
return { ratingFilters: { ...ratingFilters, [selection]: !ratingFilters[selection] } };
});
};

render() {
const { ratingFilters } = this.state;

const items = this.props.projects
.map(project => {
const x =
project.measures[X_METRIC] != null ? Number(project.measures[X_METRIC]) : undefined;
const y =
project.measures[Y_METRIC] != null ? Number(project.measures[Y_METRIC]) : undefined;
const size =
project.measures[SIZE_METRIC] != null ? Number(project.measures[SIZE_METRIC]) : undefined;
const color1 =
project.measures[COLOR_METRIC_1] != null
? Number(project.measures[COLOR_METRIC_1])
: undefined;
const color2 =
project.measures[COLOR_METRIC_2] != null
? Number(project.measures[COLOR_METRIC_2])
: undefined;

const colorRating =
color1 !== undefined && color2 !== undefined ? Math.max(color1, color2) : undefined;

// Filter out items that match ratingFilters
if (colorRating !== undefined && ratingFilters[colorRating]) {
return undefined;
}

return {
x: x || 0,
y: y || 0,
size: size || 0,
color: colorRating !== undefined ? RATING_COLORS[colorRating - 1] : undefined,
key: project.key,
tooltip: this.getTooltip(project, x, y, size, color1, color2),
link: getProjectUrl(project.key)
};
})
.filter(isDefined);

const formatXTick = (tick: number) => formatMeasure(tick, X_METRIC_TYPE);
const formatYTick = (tick: number) => formatMeasure(tick, Y_METRIC_TYPE);

return (
<div>
<BubbleChart
formatXTick={formatXTick}
formatYTick={formatYTick}
height={600}
items={items}
padding={[80, 20, 60, 100]}
yDomain={[100, 0]}
/>

<div className="measure-details-bubble-chart-axis x">
{translate('metric', X_METRIC, 'name')}
</div>
<div className="measure-details-bubble-chart-axis y">
{translate('metric', Y_METRIC, 'name')}
</div>
<div className="measure-details-bubble-chart-axis size">
<span className="measure-details-bubble-chart-title">
<span className="text-middle">{translate('projects.visualization.risk')}</span>
<HelpTooltip className="spacer-left" overlay={this.props.helpText} />
</span>
<div>
<span className="spacer-right">
{translateWithParameters(
'component_measures.legend.color_x',
translate('projects.worse_of_reliablity_and_security')
)}
</span>
{translateWithParameters(
'component_measures.legend.size_x',
translate('metric', SIZE_METRIC, 'name')
)}
<ColorRatingsLegend
className="big-spacer-top"
filters={ratingFilters}
onRatingClick={this.handleRatingFilterClick}
/>
</div>
</div>
</div>
);
}
}

+ 0
- 41
server/sonar-web/src/main/js/apps/projects/visualizations/Security.tsx View File

@@ -1,41 +0,0 @@
/*
* 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 * as React from 'react';
import { translate } from '../../../helpers/l10n';
import { Project } from '../types';
import SimpleBubbleChart from './SimpleBubbleChart';

interface Props {
helpText: string;
projects: Project[];
}

export default function Security(props: Props) {
return (
<SimpleBubbleChart
{...props}
colorMetric="security_rating"
sizeMetric={{ key: 'vulnerabilities', type: 'SHORT_INT' }}
title={translate('projects.visualization', 'security')}
xMetric={{ key: 'ncloc', type: 'SHORT_INT' }}
yMetric={{ key: 'security_remediation_effort', type: 'SHORT_WORK_DUR' }}
/>
);
}

+ 0
- 185
server/sonar-web/src/main/js/apps/projects/visualizations/SimpleBubbleChart.tsx View File

@@ -1,185 +0,0 @@
/*
* 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 * as React from 'react';
import BubbleChart from '../../../components/charts/BubbleChart';
import ColorRatingsLegend from '../../../components/charts/ColorRatingsLegend';
import HelpTooltip from '../../../components/controls/HelpTooltip';
import QualifierIcon from '../../../components/icons/QualifierIcon';
import { RATING_COLORS } from '../../../helpers/constants';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { formatMeasure } from '../../../helpers/measures';
import { isDefined } from '../../../helpers/types';
import { getProjectUrl } from '../../../helpers/urls';
import { ComponentQualifier } from '../../../types/component';
import { Project } from '../types';

interface Metric {
key: string;
type: string;
}

interface Props {
colorMetric?: string;
helpText: string;
projects: Project[];
sizeMetric: Metric;
title?: string;
xMetric: Metric;
yDomain?: [number, number];
yMetric: Metric;
}

interface State {
ratingFilters: { [rating: number]: boolean };
}

export default class SimpleBubbleChart extends React.PureComponent<Props, State> {
state: State = {
ratingFilters: {}
};

getMetricTooltip(metric: Metric, value?: number) {
const name = translate('metric', metric.key, 'name');
const formattedValue = value != null ? formatMeasure(value, metric.type) : '–';
return (
<div>
{name}
{': '}
{formattedValue}
</div>
);
}

getTooltip(project: Project, x?: number, y?: number, size?: number, color?: number) {
return (
<div className="text-left">
<div className="little-spacer-bottom display-flex-center display-flex-space-between">
<strong>{project.name}</strong>

{project.qualifier === ComponentQualifier.Application && (
<div className="big-spacer-left nowrap">
<QualifierIcon
className="little-spacer-right"
fill="currentColor"
qualifier={ComponentQualifier.Application}
/>
{translate('qualifier.APP')}
</div>
)}
</div>
{this.getMetricTooltip(this.props.xMetric, x)}
{this.getMetricTooltip(this.props.yMetric, y)}
{this.getMetricTooltip(this.props.sizeMetric, size)}
{/* if `color` is defined then `this.props.colorMetric` is defined too */}
{color && this.getMetricTooltip({ key: this.props.colorMetric!, type: 'RATING' }, color)}
</div>
);
}

handleRatingFilterClick = (selection: number) => {
this.setState(({ ratingFilters }) => {
return { ratingFilters: { ...ratingFilters, [selection]: !ratingFilters[selection] } };
});
};

render() {
const { xMetric, yMetric, sizeMetric, colorMetric } = this.props;
const { ratingFilters } = this.state;

const items = this.props.projects
.filter(project => colorMetric == null || project.measures[colorMetric] !== null)
.map(project => {
const x =
project.measures[xMetric.key] != null ? Number(project.measures[xMetric.key]) : undefined;
const y =
project.measures[yMetric.key] != null ? Number(project.measures[yMetric.key]) : undefined;
const size =
project.measures[sizeMetric.key] != null
? Number(project.measures[sizeMetric.key])
: undefined;
const color = colorMetric ? Number(project.measures[colorMetric]) : undefined;

// Filter out items that match ratingFilters
if (color && ratingFilters[color]) {
return undefined;
}

return {
x: x || 0,
y: y || 0,
size: size || 0,
color: color ? RATING_COLORS[color - 1] : undefined,
key: project.key,
tooltip: this.getTooltip(project, x, y, size, color),
link: getProjectUrl(project.key)
};
})
.filter(isDefined);

const formatXTick = (tick: number) => formatMeasure(tick, xMetric.type);
const formatYTick = (tick: number) => formatMeasure(tick, yMetric.type);

return (
<div>
<BubbleChart
formatXTick={formatXTick}
formatYTick={formatYTick}
height={600}
items={items}
padding={[colorMetric ? 80 : 40, 20, 60, 100]}
yDomain={this.props.yDomain}
/>
<div className="measure-details-bubble-chart-axis x">
{translate('metric', xMetric.key, 'name')}
</div>
<div className="measure-details-bubble-chart-axis y">
{translate('metric', yMetric.key, 'name')}
</div>
<div className="measure-details-bubble-chart-axis size">
<span className="measure-details-bubble-chart-title">
<span className="text-middle">{this.props.title}</span>
<HelpTooltip className="spacer-left" overlay={this.props.helpText} />
</span>
<div>
{colorMetric != null && (
<span className="spacer-right">
{translateWithParameters(
'component_measures.legend.color_x',
translate('metric', colorMetric, 'name')
)}
</span>
)}
{translateWithParameters(
'component_measures.legend.size_x',
translate('metric', sizeMetric.key, 'name')
)}
{colorMetric != null && (
<ColorRatingsLegend
className="big-spacer-top"
filters={ratingFilters}
onRatingClick={this.handleRatingFilterClick}
/>
)}
</div>
</div>
</div>
);
}
}

+ 0
- 89
server/sonar-web/src/main/js/apps/projects/visualizations/Visualizations.tsx View File

@@ -1,89 +0,0 @@
/*
* 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 * as React from 'react';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { Dict } from '../../../types/types';
import { Project } from '../types';
import { localizeSorting } from '../utils';
import Coverage from './Coverage';
import Duplications from './Duplications';
import Maintainability from './Maintainability';
import Reliability from './Reliability';
import Risk from './Risk';
import Security from './Security';

interface Props {
projects: Project[];
sort?: string;
total?: number;
visualization: string;
}

export default class Visualizations extends React.PureComponent<Props> {
renderVisualization(projects: Project[]) {
const visualizationToComponent: Dict<any> = {
risk: Risk,
reliability: Reliability,
security: Security,
maintainability: Maintainability,
coverage: Coverage,
duplications: Duplications
};
const Component = visualizationToComponent[this.props.visualization];

return Component ? (
<Component
helpText={translate('projects.visualization', this.props.visualization, 'description')}
projects={projects}
/>
) : null;
}

renderFooter() {
const { projects, total, sort } = this.props;

const limitReached = projects != null && total != null && projects.length < total;

return limitReached ? (
<footer className="projects-visualizations-footer">
<p className="note spacer-top">
{translateWithParameters(
'projects.limited_set_of_projects',
projects!.length,
localizeSorting(sort)
)}
</p>
</footer>
) : null;
}

render() {
const { projects } = this.props;

return (
<div className="boxed-group projects-visualizations">
<main className="projects-visualization">
{projects != null && this.renderVisualization(projects)}
</main>
{this.renderFooter()}
</div>
);
}
}

+ 0
- 26
server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/Coverage-test.tsx View File

@@ -1,26 +0,0 @@
/*
* 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 { shallow } from 'enzyme';
import * as React from 'react';
import Coverage from '../Coverage';

it('renders', () => {
expect(shallow(<Coverage helpText="foobar" projects={[]} />)).toMatchSnapshot();
});

+ 0
- 26
server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/Duplications-test.tsx View File

@@ -1,26 +0,0 @@
/*
* 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 { shallow } from 'enzyme';
import * as React from 'react';
import Duplications from '../Duplications';

it('renders', () => {
expect(shallow(<Duplications helpText="foobar" projects={[]} />)).toMatchSnapshot();
});

+ 0
- 26
server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/Maintainability-test.tsx View File

@@ -1,26 +0,0 @@
/*
* 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 { shallow } from 'enzyme';
import * as React from 'react';
import Maintainability from '../Maintainability';

it('renders', () => {
expect(shallow(<Maintainability helpText="foobar" projects={[]} />)).toMatchSnapshot();
});

+ 0
- 26
server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/Reliability-test.tsx View File

@@ -1,26 +0,0 @@
/*
* 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 { shallow } from 'enzyme';
import * as React from 'react';
import Reliability from '../Reliability';

it('renders', () => {
expect(shallow(<Reliability helpText="foobar" projects={[]} />)).toMatchSnapshot();
});

+ 0
- 56
server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/Risk-test.tsx View File

@@ -1,56 +0,0 @@
/*
* 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 { shallow } from 'enzyme';
import * as React from 'react';
import { mockProject } from '../../../../helpers/mocks/projects';
import Risk from '../Risk';

it('renders', () => {
expect(shallowRender()).toMatchSnapshot();
});

it('should handle filtering', () => {
const wrapper = shallowRender();

wrapper.instance().handleRatingFilterClick(2);

expect(wrapper.state().ratingFilters).toEqual({ 2: true });
});

function shallowRender(overrides: Partial<Risk['props']> = {}) {
const project1 = mockProject({
key: 'foo',
measures: {
complexity: '17.2',
coverage: '53.5',
ncloc: '1734',
sqale_index: '1',
reliability_rating: '3',
security_rating: '2'
},
name: 'Foo'
});
const project2 = mockProject({
key: 'bar',
name: 'Bar',
measures: {}
});
return shallow<Risk>(<Risk helpText="foobar" projects={[project1, project2]} {...overrides} />);
}

+ 0
- 26
server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/Security-test.tsx View File

@@ -1,26 +0,0 @@
/*
* 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 { shallow } from 'enzyme';
import * as React from 'react';
import Security from '../Security';

it('renders', () => {
expect(shallow(<Security helpText="foobar" projects={[]} />)).toMatchSnapshot();
});

+ 0
- 59
server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/SimpleBubbleChart-test.tsx View File

@@ -1,59 +0,0 @@
/*
* 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 { shallow } from 'enzyme';
import * as React from 'react';
import { mockProject } from '../../../../helpers/mocks/projects';
import { ComponentQualifier } from '../../../../types/component';
import SimpleBubbleChart from '../SimpleBubbleChart';

it('renders', () => {
expect(shallowRender()).toMatchSnapshot();
});

it('should handle filtering', () => {
const wrapper = shallowRender();

wrapper.instance().handleRatingFilterClick(2);

expect(wrapper.state().ratingFilters).toEqual({ 2: true });
});

function shallowRender(overrides: Partial<SimpleBubbleChart['props']> = {}) {
const project1 = mockProject({
measures: { complexity: '17.2', coverage: '53.5', ncloc: '1734', security_rating: '2' }
});
const app = mockProject({
key: 'app',
measures: { complexity: '23.1', coverage: '87.3', ncloc: '32478', security_rating: '1' },
name: 'App',
qualifier: ComponentQualifier.Application
});
return shallow<SimpleBubbleChart>(
<SimpleBubbleChart
colorMetric="security_rating"
helpText="foobar"
projects={[app, project1]}
sizeMetric={{ key: 'ncloc', type: 'INT' }}
xMetric={{ key: 'complexity', type: 'INT' }}
yMetric={{ key: 'coverage', type: 'PERCENT' }}
{...overrides}
/>
);
}

+ 0
- 32
server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/Visualizations-test.tsx View File

@@ -1,32 +0,0 @@
/*
* 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 { shallow } from 'enzyme';
import * as React from 'react';
import Visualizations from '../Visualizations';

it('renders', () => {
expect(shallow(<Visualizations projects={[]} visualization="coverage" />)).toMatchSnapshot();
});

it('renders when limit is reached', () => {
expect(
shallow(<Visualizations projects={[]} total={1000} visualization="coverage" />)
).toMatchSnapshot();
});

+ 0
- 33
server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/__snapshots__/Coverage-test.tsx.snap View File

@@ -1,33 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`renders 1`] = `
<SimpleBubbleChart
helpText="foobar"
projects={Array []}
sizeMetric={
Object {
"key": "uncovered_lines",
"type": "SHORT_INT",
}
}
title="projects.visualization.coverage"
xMetric={
Object {
"key": "complexity",
"type": "SHORT_INT",
}
}
yDomain={
Array [
100,
0,
]
}
yMetric={
Object {
"key": "coverage",
"type": "PERCENT",
}
}
/>
`;

+ 0
- 27
server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/__snapshots__/Duplications-test.tsx.snap View File

@@ -1,27 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`renders 1`] = `
<SimpleBubbleChart
helpText="foobar"
projects={Array []}
sizeMetric={
Object {
"key": "duplicated_blocks",
"type": "SHORT_INT",
}
}
title="projects.visualization.duplications"
xMetric={
Object {
"key": "ncloc",
"type": "SHORT_INT",
}
}
yMetric={
Object {
"key": "duplicated_lines_density",
"type": "PERCENT",
}
}
/>
`;

+ 0
- 28
server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/__snapshots__/Maintainability-test.tsx.snap View File

@@ -1,28 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`renders 1`] = `
<SimpleBubbleChart
colorMetric="sqale_rating"
helpText="foobar"
projects={Array []}
sizeMetric={
Object {
"key": "code_smells",
"type": "SHORT_INT",
}
}
title="projects.visualization.maintainability"
xMetric={
Object {
"key": "ncloc",
"type": "SHORT_INT",
}
}
yMetric={
Object {
"key": "sqale_index",
"type": "SHORT_WORK_DUR",
}
}
/>
`;

+ 0
- 28
server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/__snapshots__/Reliability-test.tsx.snap View File

@@ -1,28 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`renders 1`] = `
<SimpleBubbleChart
colorMetric="reliability_rating"
helpText="foobar"
projects={Array []}
sizeMetric={
Object {
"key": "bugs",
"type": "SHORT_INT",
}
}
title="projects.visualization.reliability"
xMetric={
Object {
"key": "ncloc",
"type": "SHORT_INT",
}
}
yMetric={
Object {
"key": "reliability_remediation_effort",
"type": "SHORT_WORK_DUR",
}
}
/>
`;

+ 0
- 179
server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/__snapshots__/Risk-test.tsx.snap View File

@@ -1,179 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`renders 1`] = `
<div>
<BubbleChart
displayXGrid={true}
displayXTicks={true}
displayYGrid={true}
displayYTicks={true}
formatXTick={[Function]}
formatYTick={[Function]}
height={600}
items={
Array [
Object {
"color": "#eabe06",
"key": "foo",
"link": Object {
"pathname": "/dashboard",
"query": Object {
"branch": undefined,
"id": "foo",
},
},
"size": 1734,
"tooltip": <div
className="text-left"
>
<div
className="little-spacer-bottom display-flex-center display-flex-space-between"
>
<strong>
Foo
</strong>
</div>
<div>
metric.reliability_rating.name
:
C
</div>
<div>
metric.security_rating.name
:
B
</div>
<div>
metric.coverage.name
:
53.5%
</div>
<div>
metric.sqale_index.name
:
work_duration.x_minutes.1
</div>
<div>
metric.ncloc.name
:
1.7short_number_suffix.k
</div>
</div>,
"x": 1,
"y": 53.5,
},
Object {
"color": undefined,
"key": "bar",
"link": Object {
"pathname": "/dashboard",
"query": Object {
"branch": undefined,
"id": "bar",
},
},
"size": 0,
"tooltip": <div
className="text-left"
>
<div
className="little-spacer-bottom display-flex-center display-flex-space-between"
>
<strong>
Bar
</strong>
</div>
<div>
metric.reliability_rating.name
:
</div>
<div>
metric.security_rating.name
:
</div>
<div>
metric.coverage.name
:
</div>
<div>
metric.sqale_index.name
:
</div>
<div>
metric.ncloc.name
:
</div>
</div>,
"x": 0,
"y": 0,
},
]
}
padding={
Array [
80,
20,
60,
100,
]
}
sizeRange={
Array [
5,
45,
]
}
yDomain={
Array [
100,
0,
]
}
/>
<div
className="measure-details-bubble-chart-axis x"
>
metric.sqale_index.name
</div>
<div
className="measure-details-bubble-chart-axis y"
>
metric.coverage.name
</div>
<div
className="measure-details-bubble-chart-axis size"
>
<span
className="measure-details-bubble-chart-title"
>
<span
className="text-middle"
>
projects.visualization.risk
</span>
<HelpTooltip
className="spacer-left"
overlay="foobar"
/>
</span>
<div>
<span
className="spacer-right"
>
component_measures.legend.color_x.projects.worse_of_reliablity_and_security
</span>
component_measures.legend.size_x.metric.ncloc.name
<ColorRatingsLegend
className="big-spacer-top"
filters={Object {}}
onRatingClick={[Function]}
/>
</div>
</div>
</div>
`;

+ 0
- 28
server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/__snapshots__/Security-test.tsx.snap View File

@@ -1,28 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`renders 1`] = `
<SimpleBubbleChart
colorMetric="security_rating"
helpText="foobar"
projects={Array []}
sizeMetric={
Object {
"key": "vulnerabilities",
"type": "SHORT_INT",
}
}
title="projects.visualization.security"
xMetric={
Object {
"key": "ncloc",
"type": "SHORT_INT",
}
}
yMetric={
Object {
"key": "security_remediation_effort",
"type": "SHORT_WORK_DUR",
}
}
/>
`;

+ 0
- 171
server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/__snapshots__/SimpleBubbleChart-test.tsx.snap View File

@@ -1,171 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`renders 1`] = `
<div>
<BubbleChart
displayXGrid={true}
displayXTicks={true}
displayYGrid={true}
displayYTicks={true}
formatXTick={[Function]}
formatYTick={[Function]}
height={600}
items={
Array [
Object {
"color": "#00aa00",
"key": "app",
"link": Object {
"pathname": "/dashboard",
"query": Object {
"branch": undefined,
"id": "app",
},
},
"size": 32478,
"tooltip": <div
className="text-left"
>
<div
className="little-spacer-bottom display-flex-center display-flex-space-between"
>
<strong>
App
</strong>
<div
className="big-spacer-left nowrap"
>
<QualifierIcon
className="little-spacer-right"
fill="currentColor"
qualifier="APP"
/>
qualifier.APP
</div>
</div>
<div>
metric.complexity.name
:
23
</div>
<div>
metric.coverage.name
:
87.3%
</div>
<div>
metric.ncloc.name
:
32,478
</div>
<div>
metric.security_rating.name
:
A
</div>
</div>,
"x": 23.1,
"y": 87.3,
},
Object {
"color": "#b0d513",
"key": "foo",
"link": Object {
"pathname": "/dashboard",
"query": Object {
"branch": undefined,
"id": "foo",
},
},
"size": 1734,
"tooltip": <div
className="text-left"
>
<div
className="little-spacer-bottom display-flex-center display-flex-space-between"
>
<strong>
Foo
</strong>
</div>
<div>
metric.complexity.name
:
17
</div>
<div>
metric.coverage.name
:
53.5%
</div>
<div>
metric.ncloc.name
:
1,734
</div>
<div>
metric.security_rating.name
:
B
</div>
</div>,
"x": 17.2,
"y": 53.5,
},
]
}
padding={
Array [
80,
20,
60,
100,
]
}
sizeRange={
Array [
5,
45,
]
}
/>
<div
className="measure-details-bubble-chart-axis x"
>
metric.complexity.name
</div>
<div
className="measure-details-bubble-chart-axis y"
>
metric.coverage.name
</div>
<div
className="measure-details-bubble-chart-axis size"
>
<span
className="measure-details-bubble-chart-title"
>
<span
className="text-middle"
/>
<HelpTooltip
className="spacer-left"
overlay="foobar"
/>
</span>
<div>
<span
className="spacer-right"
>
component_measures.legend.color_x.metric.security_rating.name
</span>
component_measures.legend.size_x.metric.ncloc.name
<ColorRatingsLegend
className="big-spacer-top"
filters={Object {}}
onRatingClick={[Function]}
/>
</div>
</div>
</div>
`;

+ 0
- 40
server/sonar-web/src/main/js/apps/projects/visualizations/__tests__/__snapshots__/Visualizations-test.tsx.snap View File

@@ -1,40 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`renders 1`] = `
<div
className="boxed-group projects-visualizations"
>
<main
className="projects-visualization"
>
<Coverage
helpText="projects.visualization.coverage.description"
projects={Array []}
/>
</main>
</div>
`;

exports[`renders when limit is reached 1`] = `
<div
className="boxed-group projects-visualizations"
>
<main
className="projects-visualization"
>
<Coverage
helpText="projects.visualization.coverage.description"
projects={Array []}
/>
</main>
<footer
className="projects-visualizations-footer"
>
<p
className="note spacer-top"
>
projects.limited_set_of_projects.0.projects.sort.name
</p>
</footer>
</div>
`;

+ 0
- 12
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

@@ -1030,18 +1030,6 @@ projects.view.overall=Overall Status
projects.view.overall_code=Overall Code
projects.view.new_code=New Code
projects.worse_of_reliablity_and_security=Worse of Reliability and Security
projects.visualization.risk=Risk
projects.visualization.risk.description=Get quick insights into the operational risks in your projects. Any color but green indicates immediate risks: Bugs or Vulnerabilities that should be examined. A position at the top or right of the graph means that the longer-term health of the project may be at risk. Green bubbles at the bottom-left are best.
projects.visualization.reliability=Reliability
projects.visualization.reliability.description=See bugs' operational risks to your projects. The closer a bubble's color is to red, the more severe the worst bugs in the project. Bubble size indicates bug volume in the project, and each bubble's vertical position reflects the estimated time to address the bugs in the project. Small green bubbles on the bottom edge are best.
projects.visualization.security=Security
projects.visualization.security.description=See vulnerabilities' operational risks to your projects. The closer a bubble's color is to red, the more severe the worst vulnerabilities in the project. Bubble size indicates vulnerability volume in the project, and each bubble's vertical position reflects the estimated time to address the vulnerabilities in the project. Small green bubbles on the bottom edge are best.
projects.visualization.maintainability=Maintainability
projects.visualization.maintainability.description=See code smells' long-term risks to your projects. The closer a bubble's color is to red, the higher the ratio of technical debt to project size. Bubble size indicates code smell volume in the project, and each bubble's vertical position reflects the estimated time to address the code smells in the project. Small green bubbles on the bottom edge are best.
projects.visualization.coverage=Coverage
projects.visualization.coverage.description=See missing test coverage's long-term risks to your projects. Bubble size indicates the volume of uncovered lines in the project, and each bubble's vertical position reflects the volume of missing coverage. Small bubbles on the bottom edge are best.
projects.visualization.duplications=Duplications
projects.visualization.duplications.description=See duplications' long-term risks to your projects. Bubble size indicates the volume of duplicated blocks in the project, and each bubble's vertical position reflects the volume of lines in those blocks. Small bubbles on the bottom edge are best.
projects.limited_set_of_projects=Displayed project set limited to the top {0} projects based on current sort: {1}.
projects.facets.quality_gate=Quality Gate
projects.facets.quality_gate.warning_help=Warning status is deprecated. This filter will disappear when no Warning Quality Gate remains.

Loading…
Cancel
Save