Browse Source

SONAR-9693 Better display facets with long names (#586)

tags/7.5
Stas Vilchik 5 years ago
parent
commit
86be783880
29 changed files with 905 additions and 665 deletions
  1. 2
    2
      server/sonar-web/src/main/js/apps/coding-rules/components/Facet.tsx
  2. 6
    0
      server/sonar-web/src/main/js/apps/coding-rules/components/ProfileFacet.tsx
  3. 26
    3
      server/sonar-web/src/main/js/apps/component-measures/style.css
  4. 43
    30
      server/sonar-web/src/main/js/apps/issues/sidebar/AssigneeFacet.tsx
  5. 9
    2
      server/sonar-web/src/main/js/apps/issues/sidebar/AuthorFacet.tsx
  6. 5
    0
      server/sonar-web/src/main/js/apps/issues/sidebar/CreationDateFacet.tsx
  7. 15
    6
      server/sonar-web/src/main/js/apps/issues/sidebar/DirectoryFacet.tsx
  8. 10
    3
      server/sonar-web/src/main/js/apps/issues/sidebar/FileFacet.tsx
  9. 10
    3
      server/sonar-web/src/main/js/apps/issues/sidebar/LanguageFacet.tsx
  10. 10
    3
      server/sonar-web/src/main/js/apps/issues/sidebar/ModuleFacet.tsx
  11. 52
    33
      server/sonar-web/src/main/js/apps/issues/sidebar/ProjectFacet.tsx
  12. 15
    8
      server/sonar-web/src/main/js/apps/issues/sidebar/ResolutionFacet.tsx
  13. 12
    4
      server/sonar-web/src/main/js/apps/issues/sidebar/RuleFacet.tsx
  14. 12
    4
      server/sonar-web/src/main/js/apps/issues/sidebar/SeverityFacet.tsx
  15. 45
    12
      server/sonar-web/src/main/js/apps/issues/sidebar/StandardFacet.tsx
  16. 12
    4
      server/sonar-web/src/main/js/apps/issues/sidebar/StatusFacet.tsx
  17. 10
    3
      server/sonar-web/src/main/js/apps/issues/sidebar/TagFacet.tsx
  18. 12
    4
      server/sonar-web/src/main/js/apps/issues/sidebar/TypeFacet.tsx
  19. 210
    186
      server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/AssigneeFacet-test.tsx.snap
  20. 128
    104
      server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/StandardFacet-test.tsx.snap
  21. 22
    35
      server/sonar-web/src/main/js/components/facet/FacetItem.tsx
  22. 14
    2
      server/sonar-web/src/main/js/components/facet/MultipleSelectionHint.css
  23. 50
    0
      server/sonar-web/src/main/js/components/facet/MultipleSelectionHint.tsx
  24. 9
    1
      server/sonar-web/src/main/js/components/facet/__tests__/FacetItem-test.tsx
  25. 46
    0
      server/sonar-web/src/main/js/components/facet/__tests__/MultipleSelectionHint-test.tsx
  26. 66
    85
      server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetItem-test.tsx.snap
  27. 25
    0
      server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/MultipleSelectionHint-test.tsx.snap
  28. 28
    127
      server/sonar-web/src/main/js/components/search-navigator.css
  29. 1
    1
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 2
- 2
server/sonar-web/src/main/js/apps/coding-rules/components/Facet.tsx View File

@@ -76,7 +76,7 @@ export default class Facet extends React.PureComponent<Props> {
renderItem = (value: string) => {
const active = this.props.values.includes(value);
const stat = this.getStat(value);
const { renderName = defaultRenderName } = this.props;
const { renderName = defaultRenderName, renderTextName = defaultRenderName } = this.props;

return (
<FacetItem
@@ -87,7 +87,7 @@ export default class Facet extends React.PureComponent<Props> {
name={renderName(value)}
onClick={this.handleItemClick}
stat={stat && formatMeasure(stat, 'SHORT_INT')}
tooltip={this.props.values.length === 1 && !active}
tooltip={renderTextName(value)}
value={value}
/>
);

+ 6
- 0
server/sonar-web/src/main/js/apps/coding-rules/components/ProfileFacet.tsx View File

@@ -88,6 +88,11 @@ export default class ProfileFacet extends React.PureComponent<Props> {
}
};

getTooltip = (profile: Profile) => {
const base = `${profile.name} ${profile.languageName}`;
return profile.isBuiltIn ? `${base} (${translate('quality_profiles.built_in')})` : base;
};

renderName = (profile: Profile) => (
<>
{profile.name}
@@ -138,6 +143,7 @@ export default class ProfileFacet extends React.PureComponent<Props> {
name={this.renderName(profile)}
onClick={this.handleItemClick}
stat={this.renderActivation(profile)}
tooltip={this.getTooltip(profile)}
value={profile.key}
/>
);

+ 26
- 3
server/sonar-web/src/main/js/apps/component-measures/style.css View File

@@ -36,9 +36,32 @@
padding: 4px 6px;
}

.facet .domain-measures-leak {
padding: 3px 8px;
margin: -5px -5px;
.search-navigator-facet .domain-measures-leak {
height: var(--controlHeight);
line-height: var(--controlHeight);
padding: 0 var(--gridSize);
margin-top: -1px;
margin-right: calc(-0.75 * var(--gridSize) - 1px);
border-radius: 2px;
box-sizing: border-box;
}

.search-navigator-facet:hover .domain-measures-leak,
.search-navigator-facet.active .domain-measures-leak {
height: calc(var(--controlHeight) - 2px);
margin-top: 0;
margin-right: calc(-0.75 * var(--gridSize));
border-top: none;
border-bottom: none;
border-right: none;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}

.search-navigator-facet.active .domain-measures-leak {
border-left: none;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}

.domain-measures-leak-header {

+ 43
- 30
server/sonar-web/src/main/js/apps/issues/sidebar/AssigneeFacet.tsx View File

@@ -29,6 +29,7 @@ import FacetItemsList from '../../../components/facet/FacetItemsList';
import Avatar from '../../../components/ui/Avatar';
import { translate } from '../../../helpers/l10n';
import DeferredSpinner from '../../../components/common/DeferredSpinner';
import MultipleSelectionHint from '../../../components/facet/MultipleSelectionHint';

export interface Props {
assigned: boolean;
@@ -90,25 +91,28 @@ export default class AssigneeFacet extends React.PureComponent<Props> {
return assignee === '' ? !this.props.assigned : this.props.assignees.includes(assignee);
}

getAssigneeName(assignee: string) {
getAssigneeNameAndTooltip(assignee: string) {
if (assignee === '') {
return translate('unassigned');
return { name: translate('unassigned'), tooltip: translate('unassigned') };
} else {
const { referencedUsers } = this.props;
if (referencedUsers[assignee]) {
return (
<span>
<Avatar
className="little-spacer-right"
hash={referencedUsers[assignee].avatar}
name={referencedUsers[assignee].name}
size={16}
/>
{referencedUsers[assignee].name}
</span>
);
return {
name: (
<span>
<Avatar
className="little-spacer-right"
hash={referencedUsers[assignee].avatar}
name={referencedUsers[assignee].name}
size={16}
/>
{referencedUsers[assignee].name}
</span>
),
tooltip: referencedUsers[assignee].name
};
} else {
return assignee;
return { name: assignee, tooltip: assignee };
}
}
}
@@ -145,6 +149,22 @@ export default class AssigneeFacet extends React.PureComponent<Props> {
);
};

renderListItem(assignee: string) {
const { name, tooltip } = this.getAssigneeNameAndTooltip(assignee);
return (
<FacetItem
active={this.isAssigneeActive(assignee)}
key={assignee}
loading={this.props.loading}
name={name}
onClick={this.handleItemClick}
stat={formatFacetStat(this.getStat(assignee))}
tooltip={tooltip}
value={assignee}
/>
);
}

renderList() {
const { stats } = this.props;

@@ -161,20 +181,7 @@ export default class AssigneeFacet extends React.PureComponent<Props> {
);

return (
<FacetItemsList>
{assignees.map(assignee => (
<FacetItem
active={this.isAssigneeActive(assignee)}
key={assignee}
loading={this.props.loading}
name={this.getAssigneeName(assignee)}
onClick={this.handleItemClick}
stat={formatFacetStat(this.getStat(assignee))}
tooltip={this.props.assignees.length === 1 && !this.isAssigneeActive(assignee)}
value={assignee}
/>
))}
</FacetItemsList>
<FacetItemsList>{assignees.map(assignee => this.renderListItem(assignee))}</FacetItemsList>
);
}

@@ -193,6 +200,7 @@ export default class AssigneeFacet extends React.PureComponent<Props> {
}

render() {
const { assignees, stats = {} } = this.props;
return (
<FacetBox property={this.property}>
<FacetHeader
@@ -204,8 +212,13 @@ export default class AssigneeFacet extends React.PureComponent<Props> {
/>

<DeferredSpinner loading={this.props.fetching} />
{this.props.open && this.renderList()}
{this.props.open && this.renderFooter()}
{this.props.open && (
<>
{this.renderList()}
{this.renderFooter()}
<MultipleSelectionHint options={Object.keys(stats).length} values={assignees.length} />
</>
)}
</FacetBox>
);
}

+ 9
- 2
server/sonar-web/src/main/js/apps/issues/sidebar/AuthorFacet.tsx View File

@@ -26,6 +26,7 @@ import FacetItem from '../../../components/facet/FacetItem';
import FacetItemsList from '../../../components/facet/FacetItemsList';
import { translate } from '../../../helpers/l10n';
import DeferredSpinner from '../../../components/common/DeferredSpinner';
import MultipleSelectionHint from '../../../components/facet/MultipleSelectionHint';

interface Props {
fetching: boolean;
@@ -90,7 +91,7 @@ export default class AuthorFacet extends React.PureComponent<Props> {
name={author}
onClick={this.handleItemClick}
stat={formatFacetStat(this.getStat(author))}
tooltip={this.props.authors.length === 1 && !this.props.authors.includes(author)}
tooltip={author}
value={author}
/>
))}
@@ -99,6 +100,7 @@ export default class AuthorFacet extends React.PureComponent<Props> {
}

render() {
const { authors, stats = {} } = this.props;
return (
<FacetBox property={this.property}>
<FacetHeader
@@ -110,7 +112,12 @@ export default class AuthorFacet extends React.PureComponent<Props> {
/>

<DeferredSpinner loading={this.props.fetching} />
{this.props.open && this.renderList()}
{this.props.open && (
<>
{this.renderList()}
<MultipleSelectionHint options={Object.keys(stats).length} values={authors.length} />
</>
)}
</FacetBox>
);
}

+ 5
- 0
server/sonar-web/src/main/js/apps/issues/sidebar/CreationDateFacet.tsx View File

@@ -229,6 +229,7 @@ export default class CreationDateFacet extends React.PureComponent<Props> {
loading={this.props.loading}
name={translate('issues.facet.createdAt.all')}
onClick={this.handlePeriodClick}
tooltip={translate('issues.facet.createdAt.all')}
value=""
/>
{component ? (
@@ -237,6 +238,7 @@ export default class CreationDateFacet extends React.PureComponent<Props> {
loading={this.props.loading}
name={translate('issues.new_code')}
onClick={this.handleLeakPeriodClick}
tooltip={translate('issues.leak_period')}
value=""
/>
) : (
@@ -246,6 +248,7 @@ export default class CreationDateFacet extends React.PureComponent<Props> {
loading={this.props.loading}
name={translate('issues.facet.createdAt.last_week')}
onClick={this.handlePeriodClick}
tooltip={translate('issues.facet.createdAt.last_week')}
value="1w"
/>
<FacetItem
@@ -253,6 +256,7 @@ export default class CreationDateFacet extends React.PureComponent<Props> {
loading={this.props.loading}
name={translate('issues.facet.createdAt.last_month')}
onClick={this.handlePeriodClick}
tooltip={translate('issues.facet.createdAt.last_month')}
value="1m"
/>
<FacetItem
@@ -260,6 +264,7 @@ export default class CreationDateFacet extends React.PureComponent<Props> {
loading={this.props.loading}
name={translate('issues.facet.createdAt.last_year')}
onClick={this.handlePeriodClick}
tooltip={translate('issues.facet.createdAt.last_year')}
value="1y"
/>
</>

+ 15
- 6
server/sonar-web/src/main/js/apps/issues/sidebar/DirectoryFacet.tsx View File

@@ -28,6 +28,7 @@ import QualifierIcon from '../../../components/icons-components/QualifierIcon';
import { translate } from '../../../helpers/l10n';
import { collapsePath } from '../../../helpers/path';
import DeferredSpinner from '../../../components/common/DeferredSpinner';
import MultipleSelectionHint from '../../../components/facet/MultipleSelectionHint';

interface Props {
fetching: boolean;
@@ -93,7 +94,8 @@ export default class DirectoryFacet extends React.PureComponent<Props> {
return null;
}

const directories = sortBy(Object.keys(stats), key => -stats[key]);
// sort directories first by counts, then by path
const directories = sortBy(Object.keys(stats), key => -stats[key], d => d);

return (
<FacetItemsList>
@@ -105,9 +107,7 @@ export default class DirectoryFacet extends React.PureComponent<Props> {
name={this.renderName(directory)}
onClick={this.handleItemClick}
stat={formatFacetStat(this.getStat(directory))}
tooltip={
this.props.directories.length === 1 && !this.props.directories.includes(directory)
}
tooltip={directory}
value={directory}
/>
))}
@@ -116,7 +116,8 @@ export default class DirectoryFacet extends React.PureComponent<Props> {
}

render() {
const values = this.props.directories.map(dir => collapsePath(dir));
const { directories, stats = {} } = this.props;
const values = directories.map(dir => collapsePath(dir));
return (
<FacetBox property={this.property}>
<FacetHeader
@@ -128,7 +129,15 @@ export default class DirectoryFacet extends React.PureComponent<Props> {
/>

<DeferredSpinner loading={this.props.fetching} />
{this.props.open && this.renderList()}
{this.props.open && (
<>
{this.renderList()}
<MultipleSelectionHint
options={Object.keys(stats).length}
values={directories.length}
/>
</>
)}
</FacetBox>
);
}

+ 10
- 3
server/sonar-web/src/main/js/apps/issues/sidebar/FileFacet.tsx View File

@@ -28,6 +28,7 @@ import QualifierIcon from '../../../components/icons-components/QualifierIcon';
import { translate } from '../../../helpers/l10n';
import { collapsePath } from '../../../helpers/path';
import DeferredSpinner from '../../../components/common/DeferredSpinner';
import MultipleSelectionHint from '../../../components/facet/MultipleSelectionHint';

interface Props {
fetching: boolean;
@@ -108,7 +109,7 @@ export default class FileFacet extends React.PureComponent<Props> {
name={this.renderName(file)}
onClick={this.handleItemClick}
stat={formatFacetStat(this.getStat(file))}
tooltip={this.props.files.length === 1 && !this.props.files.includes(file)}
tooltip={this.getFileName(file)}
value={file}
/>
))}
@@ -117,7 +118,8 @@ export default class FileFacet extends React.PureComponent<Props> {
}

render() {
const values = this.props.files.map(file => this.getFileName(file));
const { files, stats = {} } = this.props;
const values = files.map(file => this.getFileName(file));
return (
<FacetBox property={this.property}>
<FacetHeader
@@ -129,7 +131,12 @@ export default class FileFacet extends React.PureComponent<Props> {
/>

<DeferredSpinner loading={this.props.fetching} />
{this.props.open && this.renderList()}
{this.props.open && (
<>
{this.renderList()}
<MultipleSelectionHint options={Object.keys(stats).length} values={files.length} />
</>
)}
</FacetBox>
);
}

+ 10
- 3
server/sonar-web/src/main/js/apps/issues/sidebar/LanguageFacet.tsx View File

@@ -27,6 +27,7 @@ import FacetItem from '../../../components/facet/FacetItem';
import FacetItemsList from '../../../components/facet/FacetItemsList';
import { translate } from '../../../helpers/l10n';
import DeferredSpinner from '../../../components/common/DeferredSpinner';
import MultipleSelectionHint from '../../../components/facet/MultipleSelectionHint';

interface Props {
fetching: boolean;
@@ -102,7 +103,7 @@ export default class LanguageFacet extends React.PureComponent<Props> {
name={this.getLanguageName(language)}
onClick={this.handleItemClick}
stat={formatFacetStat(this.getStat(language))}
tooltip={this.props.languages.length === 1 && !this.props.languages.includes(language)}
tooltip={this.getLanguageName(language)}
value={language}
/>
))}
@@ -121,6 +122,7 @@ export default class LanguageFacet extends React.PureComponent<Props> {
}

render() {
const { languages, stats = {} } = this.props;
const values = this.props.languages.map(language => this.getLanguageName(language));
return (
<FacetBox property={this.property}>
@@ -133,8 +135,13 @@ export default class LanguageFacet extends React.PureComponent<Props> {
/>

<DeferredSpinner loading={this.props.fetching} />
{this.props.open && this.renderList()}
{this.props.open && this.renderFooter()}
{this.props.open && (
<>
{this.renderList()}
{this.renderFooter()}
<MultipleSelectionHint options={Object.keys(stats).length} values={languages.length} />
</>
)}
</FacetBox>
);
}

+ 10
- 3
server/sonar-web/src/main/js/apps/issues/sidebar/ModuleFacet.tsx View File

@@ -27,6 +27,7 @@ import FacetItemsList from '../../../components/facet/FacetItemsList';
import QualifierIcon from '../../../components/icons-components/QualifierIcon';
import { translate } from '../../../helpers/l10n';
import DeferredSpinner from '../../../components/common/DeferredSpinner';
import MultipleSelectionHint from '../../../components/facet/MultipleSelectionHint';

interface Props {
fetching: boolean;
@@ -106,7 +107,7 @@ export default class ModuleFacet extends React.PureComponent<Props> {
name={this.renderName(module)}
onClick={this.handleItemClick}
stat={formatFacetStat(this.getStat(module))}
tooltip={this.props.modules.length === 1 && !this.props.modules.includes(module)}
tooltip={this.getModuleName(module)}
value={module}
/>
))}
@@ -115,7 +116,8 @@ export default class ModuleFacet extends React.PureComponent<Props> {
}

render() {
const values = this.props.modules.map(module => this.getModuleName(module));
const { modules, stats = {} } = this.props;
const values = modules.map(module => this.getModuleName(module));
return (
<FacetBox property={this.property}>
<FacetHeader
@@ -127,7 +129,12 @@ export default class ModuleFacet extends React.PureComponent<Props> {
/>

<DeferredSpinner loading={this.props.fetching} />
{this.props.open && this.renderList()}
{this.props.open && (
<>
{this.renderList()}
<MultipleSelectionHint options={Object.keys(stats).length} values={modules.length} />
</>
)}
</FacetBox>
);
}

+ 52
- 33
server/sonar-web/src/main/js/apps/issues/sidebar/ProjectFacet.tsx View File

@@ -31,6 +31,7 @@ import Organization from '../../../components/shared/Organization';
import QualifierIcon from '../../../components/icons-components/QualifierIcon';
import { translate } from '../../../helpers/l10n';
import DeferredSpinner from '../../../components/common/DeferredSpinner';
import MultipleSelectionHint from '../../../components/facet/MultipleSelectionHint';

interface Props {
component: Component | undefined;
@@ -114,22 +115,33 @@ export default class ProjectFacet extends React.PureComponent<Props> {
return referencedComponents[project] ? referencedComponents[project].name : project;
}

renderName(project: string) {
getProjectNameAndTooltip(project: string) {
const { organization, referencedComponents } = this.props;
return referencedComponents[project] ? (
<span>
<QualifierIcon className="little-spacer-right" qualifier="TRK" />
{!organization && (
<Organization link={false} organizationKey={referencedComponents[project].organization} />
)}
{referencedComponents[project].name}
</span>
) : (
<span>
<QualifierIcon className="little-spacer-right" qualifier="TRK" />
{project}
</span>
);
return referencedComponents[project]
? {
name: (
<span>
<QualifierIcon className="little-spacer-right" qualifier="TRK" />
{!organization && (
<Organization
link={false}
organizationKey={referencedComponents[project].organization}
/>
)}
{referencedComponents[project].name}
</span>
),
tooltip: referencedComponents[project].name
}
: {
name: (
<span>
<QualifierIcon className="little-spacer-right" qualifier="TRK" />
{project}
</span>
),
tooltip: project
};
}

renderOption = (option: { label: string; organization: string }) => {
@@ -141,6 +153,22 @@ export default class ProjectFacet extends React.PureComponent<Props> {
);
};

renderListItem(project: string) {
const { name, tooltip } = this.getProjectNameAndTooltip(project);
return (
<FacetItem
active={this.props.projects.includes(project)}
key={project}
loading={this.props.loading}
name={name}
onClick={this.handleItemClick}
stat={formatFacetStat(this.getStat(project))}
tooltip={tooltip}
value={project}
/>
);
}

renderList() {
const { stats } = this.props;

@@ -150,22 +178,7 @@ export default class ProjectFacet extends React.PureComponent<Props> {

const projects = sortBy(Object.keys(stats), key => -stats[key]);

return (
<FacetItemsList>
{projects.map(project => (
<FacetItem
active={this.props.projects.includes(project)}
key={project}
loading={this.props.loading}
name={this.renderName(project)}
onClick={this.handleItemClick}
stat={formatFacetStat(this.getStat(project))}
tooltip={this.props.projects.length === 1 && !this.props.projects.includes(project)}
value={project}
/>
))}
</FacetItemsList>
);
return <FacetItemsList>{projects.map(project => this.renderListItem(project))}</FacetItemsList>;
}

renderFooter() {
@@ -184,6 +197,7 @@ export default class ProjectFacet extends React.PureComponent<Props> {
}

render() {
const { projects, stats = {} } = this.props;
const values = this.props.projects.map(project => this.getProjectName(project));
return (
<FacetBox property={this.property}>
@@ -195,8 +209,13 @@ export default class ProjectFacet extends React.PureComponent<Props> {
values={values}
/>
<DeferredSpinner loading={this.props.fetching} />
{this.props.open && this.renderList()}
{this.props.open && this.renderFooter()}
{this.props.open && (
<>
{this.renderList()}
{this.renderFooter()}
<MultipleSelectionHint options={Object.keys(stats).length} values={projects.length} />
</>
)}
</FacetBox>
);
}

+ 15
- 8
server/sonar-web/src/main/js/apps/issues/sidebar/ResolutionFacet.tsx View File

@@ -26,6 +26,7 @@ import FacetItem from '../../../components/facet/FacetItem';
import FacetItemsList from '../../../components/facet/FacetItemsList';
import { translate } from '../../../helpers/l10n';
import DeferredSpinner from '../../../components/common/DeferredSpinner';
import MultipleSelectionHint from '../../../components/facet/MultipleSelectionHint';

interface Props {
fetching: boolean;
@@ -38,6 +39,8 @@ interface Props {
stats: { [x: string]: number } | undefined;
}

const RESOLUTIONS = ['', 'FIXED', 'FALSE-POSITIVE', 'WONTFIX', 'REMOVED'];

export default class ResolutionFacet extends React.PureComponent<Props> {
property = 'resolutions';

@@ -101,19 +104,15 @@ export default class ResolutionFacet extends React.PureComponent<Props> {
name={this.getFacetItemName(resolution)}
onClick={this.handleItemClick}
stat={formatFacetStat(stat)}
tooltip={
this.props.resolutions.length === 1 &&
resolution !== '' &&
!this.props.resolutions.includes(resolution)
}
tooltip={this.getFacetItemName(resolution)}
value={resolution}
/>
);
};

render() {
const resolutions = ['', 'FIXED', 'FALSE-POSITIVE', 'WONTFIX', 'REMOVED'];
const values = this.props.resolutions.map(resolution => this.getFacetItemName(resolution));
const { resolutions, stats = {} } = this.props;
const values = resolutions.map(resolution => this.getFacetItemName(resolution));

return (
<FacetBox property={this.property}>
@@ -127,7 +126,15 @@ export default class ResolutionFacet extends React.PureComponent<Props> {
/>

<DeferredSpinner loading={this.props.fetching} />
{this.props.open && <FacetItemsList>{resolutions.map(this.renderItem)}</FacetItemsList>}
{this.props.open && (
<>
<FacetItemsList>{RESOLUTIONS.map(this.renderItem)}</FacetItemsList>
<MultipleSelectionHint
options={Object.keys(stats).length}
values={resolutions.length}
/>
</>
)}
</FacetBox>
);
}

+ 12
- 4
server/sonar-web/src/main/js/apps/issues/sidebar/RuleFacet.tsx View File

@@ -28,6 +28,7 @@ import FacetItemsList from '../../../components/facet/FacetItemsList';
import FacetFooter from '../../../components/facet/FacetFooter';
import { translate } from '../../../helpers/l10n';
import DeferredSpinner from '../../../components/common/DeferredSpinner';
import MultipleSelectionHint from '../../../components/facet/MultipleSelectionHint';

interface Props {
fetching: boolean;
@@ -78,6 +79,7 @@ export default class RuleFacet extends React.PureComponent<Props> {
languages: languages.length ? languages.join() : undefined,
organization,
q: query,
// eslint-disable-next-line camelcase
include_external: true
}).then(response =>
response.rules.map(rule => ({ label: `(${rule.langName}) ${rule.name}`, value: rule.key }))
@@ -118,7 +120,7 @@ export default class RuleFacet extends React.PureComponent<Props> {
name={this.getRuleName(rule)}
onClick={this.handleItemClick}
stat={formatFacetStat(this.getStat(rule))}
tooltip={this.props.rules.length === 1 && !this.props.rules.includes(rule)}
tooltip={this.getRuleName(rule)}
value={rule}
/>
))}
@@ -135,7 +137,8 @@ export default class RuleFacet extends React.PureComponent<Props> {
}

render() {
const values = this.props.rules.map(rule => this.getRuleName(rule));
const { rules, stats = {} } = this.props;
const values = rules.map(rule => this.getRuleName(rule));
return (
<FacetBox property={this.property}>
<FacetHeader
@@ -147,8 +150,13 @@ export default class RuleFacet extends React.PureComponent<Props> {
/>

<DeferredSpinner loading={this.props.fetching} />
{this.props.open && this.renderList()}
{this.props.open && this.renderFooter()}
{this.props.open && (
<>
{this.renderList()}
{this.renderFooter()}
<MultipleSelectionHint options={Object.keys(stats).length} values={rules.length} />
</>
)}
</FacetBox>
);
}

+ 12
- 4
server/sonar-web/src/main/js/apps/issues/sidebar/SeverityFacet.tsx View File

@@ -27,6 +27,7 @@ import FacetItemsList from '../../../components/facet/FacetItemsList';
import SeverityHelper from '../../../components/shared/SeverityHelper';
import { translate } from '../../../helpers/l10n';
import DeferredSpinner from '../../../components/common/DeferredSpinner';
import MultipleSelectionHint from '../../../components/facet/MultipleSelectionHint';

interface Props {
fetching: boolean;
@@ -38,6 +39,8 @@ interface Props {
stats: { [x: string]: number } | undefined;
}

const SEVERITIES = ['BLOCKER', 'MINOR', 'CRITICAL', 'INFO', 'MAJOR'];

export default class SeverityFacet extends React.PureComponent<Props> {
property = 'severities';

@@ -86,15 +89,15 @@ export default class SeverityFacet extends React.PureComponent<Props> {
name={<SeverityHelper severity={severity} />}
onClick={this.handleItemClick}
stat={formatFacetStat(stat)}
tooltip={this.props.severities.length === 1 && !this.props.severities.includes(severity)}
tooltip={translate('severity', severity)}
value={severity}
/>
);
};

render() {
const severities = ['BLOCKER', 'MINOR', 'CRITICAL', 'INFO', 'MAJOR'];
const values = this.props.severities.map(severity => translate('severity', severity));
const { severities, stats = {} } = this.props;
const values = severities.map(severity => translate('severity', severity));

return (
<FacetBox property={this.property}>
@@ -107,7 +110,12 @@ export default class SeverityFacet extends React.PureComponent<Props> {
/>

<DeferredSpinner loading={this.props.fetching} />
{this.props.open && <FacetItemsList>{severities.map(this.renderItem)}</FacetItemsList>}
{this.props.open && (
<>
<FacetItemsList>{SEVERITIES.map(this.renderItem)}</FacetItemsList>
<MultipleSelectionHint options={Object.keys(stats).length} values={severities.length} />
</>
)}
</FacetBox>
);
}

+ 45
- 12
server/sonar-web/src/main/js/apps/issues/sidebar/StandardFacet.tsx View File

@@ -33,6 +33,7 @@ import {
Standards
} from '../../securityReports/utils';
import DeferredSpinner from '../../../components/common/DeferredSpinner';
import MultipleSelectionHint from '../../../components/facet/MultipleSelectionHint';

export interface Props {
cwe: string[];
@@ -57,6 +58,9 @@ interface State {
standards: Standards;
}

type StatsProp = 'owaspTop10Stats' | 'cweStats' | 'sansTop25Stats';
type ValuesProp = 'owaspTop10' | 'sansTop25' | 'cwe';

export default class StandardFacet extends React.PureComponent<Props, State> {
mounted = false;
property = STANDARDS;
@@ -131,11 +135,7 @@ export default class StandardFacet extends React.PureComponent<Props, State> {
this.props.onChange({ [this.property]: [], owaspTop10: [], sansTop25: [], cwe: [] });
};

handleItemClick = (
prop: 'owaspTop10' | 'sansTop25' | 'cwe',
itemValue: string,
multiple: boolean
) => {
handleItemClick = (prop: ValuesProp, itemValue: string, multiple: boolean) => {
const items = this.props[prop];
if (multiple) {
const newValue = sortBy(
@@ -166,8 +166,8 @@ export default class StandardFacet extends React.PureComponent<Props, State> {
};

renderList = (
statsProp: 'owaspTop10Stats' | 'cweStats' | 'sansTop25Stats',
valuesProp: 'owaspTop10' | 'cwe' | 'sansTop25',
statsProp: StatsProp,
valuesProp: ValuesProp,
renderName: (standards: Standards, category: string) => string,
onClick: (x: string, multiple?: boolean) => void
) => {
@@ -202,7 +202,7 @@ export default class StandardFacet extends React.PureComponent<Props, State> {
name={renderName(this.state.standards, category)}
onClick={onClick}
stat={formatFacetStat(getStat(category))}
tooltip={values.length === 1 && !values.includes(category)}
tooltip={renderName(this.state.standards, category)}
value={category}
/>
))}
@@ -210,6 +210,12 @@ export default class StandardFacet extends React.PureComponent<Props, State> {
);
};

renderHint = (statsProp: StatsProp, valuesProp: ValuesProp) => {
const stats = this.props[statsProp] || {};
const values = this.props[valuesProp];
return <MultipleSelectionHint options={Object.keys(stats).length} values={values.length} />;
};

renderOwaspTop10List() {
return this.renderList(
'owaspTop10Stats',
@@ -219,6 +225,10 @@ export default class StandardFacet extends React.PureComponent<Props, State> {
);
}

renderOwaspTop10Hint() {
return this.renderHint('owaspTop10Stats', 'owaspTop10');
}

renderCWEList() {
return this.renderList('cweStats', 'cwe', renderCWECategory, this.handleCWEItemClick);
}
@@ -243,6 +253,10 @@ export default class StandardFacet extends React.PureComponent<Props, State> {
);
}

renderCWEHint() {
return this.renderHint('cweStats', 'cwe');
}

renderSansTop25List() {
return this.renderList(
'sansTop25Stats',
@@ -252,6 +266,10 @@ export default class StandardFacet extends React.PureComponent<Props, State> {
);
}

renderSansTop25Hint() {
return this.renderHint('sansTop25Stats', 'sansTop25');
}

renderSubFacets() {
return (
<>
@@ -265,7 +283,12 @@ export default class StandardFacet extends React.PureComponent<Props, State> {
)}
/>
<DeferredSpinner loading={this.props.fetchingOwaspTop10} />
{this.props.owaspTop10Open && this.renderOwaspTop10List()}
{this.props.owaspTop10Open && (
<>
{this.renderOwaspTop10List()}
{this.renderOwaspTop10Hint()}
</>
)}
</FacetBox>
<FacetBox className="is-inner" property="sansTop25">
<FacetHeader
@@ -277,7 +300,12 @@ export default class StandardFacet extends React.PureComponent<Props, State> {
)}
/>
<DeferredSpinner loading={this.props.fetchingSansTop25} />
{this.props.sansTop25Open && this.renderSansTop25List()}
{this.props.sansTop25Open && (
<>
{this.renderSansTop25List()}
{this.renderSansTop25Hint()}
</>
)}
</FacetBox>
<FacetBox className="is-inner" property="cwe">
<FacetHeader
@@ -287,8 +315,13 @@ export default class StandardFacet extends React.PureComponent<Props, State> {
values={this.props.cwe.map(item => renderCWECategory(this.state.standards, item))}
/>
<DeferredSpinner loading={this.props.fetchingCwe} />
{this.props.cweOpen && this.renderCWEList()}
{this.props.cweOpen && this.renderCWESearch()}
{this.props.cweOpen && (
<>
{this.renderCWEList()}
{this.renderCWESearch()}
{this.renderCWEHint()}
</>
)}
</FacetBox>
</>
);

+ 12
- 4
server/sonar-web/src/main/js/apps/issues/sidebar/StatusFacet.tsx View File

@@ -27,6 +27,7 @@ import FacetItemsList from '../../../components/facet/FacetItemsList';
import StatusHelper from '../../../components/shared/StatusHelper';
import { translate } from '../../../helpers/l10n';
import DeferredSpinner from '../../../components/common/DeferredSpinner';
import MultipleSelectionHint from '../../../components/facet/MultipleSelectionHint';

interface Props {
fetching: boolean;
@@ -38,6 +39,8 @@ interface Props {
statuses: string[];
}

const STATUSES = ['OPEN', 'RESOLVED', 'REOPENED', 'CLOSED', 'CONFIRMED'];

export default class StatusFacet extends React.PureComponent<Props> {
property = 'statuses';

@@ -86,15 +89,15 @@ export default class StatusFacet extends React.PureComponent<Props> {
name={<StatusHelper resolution={undefined} status={status} />}
onClick={this.handleItemClick}
stat={formatFacetStat(stat)}
tooltip={this.props.statuses.length === 1 && !this.props.statuses.includes(status)}
tooltip={translate('issue.status', status)}
value={status}
/>
);
};

render() {
const statuses = ['OPEN', 'RESOLVED', 'REOPENED', 'CLOSED', 'CONFIRMED'];
const values = this.props.statuses.map(status => translate('issue.status', status));
const { statuses, stats = {} } = this.props;
const values = statuses.map(status => translate('issue.status', status));

return (
<FacetBox property={this.property}>
@@ -107,7 +110,12 @@ export default class StatusFacet extends React.PureComponent<Props> {
/>

<DeferredSpinner loading={this.props.fetching} />
{this.props.open && <FacetItemsList>{statuses.map(this.renderItem)}</FacetItemsList>}
{this.props.open && (
<>
<FacetItemsList>{STATUSES.map(this.renderItem)}</FacetItemsList>
<MultipleSelectionHint options={Object.keys(stats).length} values={statuses.length} />
</>
)}
</FacetBox>
);
}

+ 10
- 3
server/sonar-web/src/main/js/apps/issues/sidebar/TagFacet.tsx View File

@@ -31,6 +31,7 @@ import FacetItemsList from '../../../components/facet/FacetItemsList';
import TagsIcon from '../../../components/icons-components/TagsIcon';
import { translate } from '../../../helpers/l10n';
import DeferredSpinner from '../../../components/common/DeferredSpinner';
import MultipleSelectionHint from '../../../components/facet/MultipleSelectionHint';

interface Props {
component: Component | undefined;
@@ -118,7 +119,7 @@ export default class TagFacet extends React.PureComponent<Props> {
name={this.renderTag(tag)}
onClick={this.handleItemClick}
stat={formatFacetStat(this.getStat(tag))}
tooltip={this.props.tags.length === 1 && !this.props.tags.includes(tag)}
tooltip={tag}
value={tag}
/>
))}
@@ -135,6 +136,7 @@ export default class TagFacet extends React.PureComponent<Props> {
}

render() {
const { tags, stats = {} } = this.props;
return (
<FacetBox property={this.property}>
<FacetHeader
@@ -146,8 +148,13 @@ export default class TagFacet extends React.PureComponent<Props> {
/>

<DeferredSpinner loading={this.props.fetching} />
{this.props.open && this.renderList()}
{this.props.open && this.renderFooter()}
{this.props.open && (
<>
{this.renderList()}
{this.renderFooter()}
<MultipleSelectionHint options={Object.keys(stats).length} values={tags.length} />
</>
)}
</FacetBox>
);
}

+ 12
- 4
server/sonar-web/src/main/js/apps/issues/sidebar/TypeFacet.tsx View File

@@ -27,6 +27,7 @@ import FacetItemsList from '../../../components/facet/FacetItemsList';
import IssueTypeIcon from '../../../components/ui/IssueTypeIcon';
import { translate } from '../../../helpers/l10n';
import DeferredSpinner from '../../../components/common/DeferredSpinner';
import MultipleSelectionHint from '../../../components/facet/MultipleSelectionHint';

interface Props {
fetching: boolean;
@@ -38,6 +39,8 @@ interface Props {
types: string[];
}

const TYPES = ['BUG', 'VULNERABILITY', 'CODE_SMELL', 'SECURITY_HOTSPOT'];

export default class TypeFacet extends React.PureComponent<Props> {
property = 'types';

@@ -99,15 +102,15 @@ export default class TypeFacet extends React.PureComponent<Props> {
}
onClick={this.handleItemClick}
stat={formatFacetStat(stat)}
tooltip={this.props.types.length === 1 && !this.props.types.includes(type)}
tooltip={translate('issue.type', type)}
value={type}
/>
);
};

render() {
const types = ['BUG', 'VULNERABILITY', 'CODE_SMELL', 'SECURITY_HOTSPOT'];
const values = this.props.types.map(type => translate('issue.type', type));
const { types, stats = {} } = this.props;
const values = types.map(type => translate('issue.type', type));

return (
<FacetBox property={this.property}>
@@ -121,7 +124,12 @@ export default class TypeFacet extends React.PureComponent<Props> {
/>

<DeferredSpinner loading={this.props.fetching} />
{this.props.open && <FacetItemsList>{types.map(this.renderItem)}</FacetItemsList>}
{this.props.open && (
<>
<FacetItemsList>{TYPES.map(this.renderItem)}</FacetItemsList>
<MultipleSelectionHint options={Object.keys(stats).length} values={types.length} />
</>
)}
</FacetBox>
);
}

+ 210
- 186
server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/AssigneeFacet-test.tsx.snap View File

@@ -15,70 +15,76 @@ exports[`should render 1`] = `
loading={false}
timeout={100}
/>
<FacetItemsList>
<FacetItem
active={false}
disabled={false}
halfWidth={false}
loading={false}
name="unassigned"
onClick={[Function]}
stat="5"
tooltip={false}
value=""
<React.Fragment>
<FacetItemsList>
<FacetItem
active={false}
disabled={false}
halfWidth={false}
loading={false}
name="unassigned"
onClick={[Function]}
stat="5"
tooltip="unassigned"
value=""
/>
<FacetItem
active={false}
disabled={false}
halfWidth={false}
key="foo"
loading={false}
name={
<span>
<Connect(Avatar)
className="little-spacer-right"
hash="avatart-foo"
name="name-foo"
size={16}
/>
name-foo
</span>
}
onClick={[Function]}
stat="13"
tooltip="name-foo"
value="foo"
/>
<FacetItem
active={false}
disabled={false}
halfWidth={false}
key="bar"
loading={false}
name="bar"
onClick={[Function]}
stat="7"
tooltip="bar"
value="bar"
/>
<FacetItem
active={false}
disabled={false}
halfWidth={false}
key="baz"
loading={false}
name="baz"
onClick={[Function]}
stat="6"
tooltip="baz"
value="baz"
/>
</FacetItemsList>
<FacetFooter
onSearch={[Function]}
onSelect={[Function]}
renderOption={[Function]}
/>
<FacetItem
active={false}
disabled={false}
halfWidth={false}
key="foo"
loading={false}
name={
<span>
<Connect(Avatar)
className="little-spacer-right"
hash="avatart-foo"
name="name-foo"
size={16}
/>
name-foo
</span>
}
onClick={[Function]}
stat="13"
tooltip={false}
value="foo"
<MultipleSelectionHint
options={4}
values={0}
/>
<FacetItem
active={false}
disabled={false}
halfWidth={false}
key="bar"
loading={false}
name="bar"
onClick={[Function]}
stat="7"
tooltip={false}
value="bar"
/>
<FacetItem
active={false}
disabled={false}
halfWidth={false}
key="baz"
loading={false}
name="baz"
onClick={[Function]}
stat="6"
tooltip={false}
value="baz"
/>
</FacetItemsList>
<FacetFooter
onSearch={[Function]}
onSelect={[Function]}
renderOption={[Function]}
/>
</React.Fragment>
</FacetBox>
`;

@@ -109,6 +115,12 @@ exports[`should render without stats 1`] = `
loading={false}
timeout={100}
/>
<React.Fragment>
<MultipleSelectionHint
options={0}
values={0}
/>
</React.Fragment>
</FacetBox>
`;

@@ -131,70 +143,76 @@ exports[`should select unassigned 1`] = `
loading={false}
timeout={100}
/>
<FacetItemsList>
<FacetItem
active={true}
disabled={false}
halfWidth={false}
loading={false}
name="unassigned"
onClick={[Function]}
stat="5"
tooltip={false}
value=""
/>
<FacetItem
active={false}
disabled={false}
halfWidth={false}
key="foo"
loading={false}
name={
<span>
<Connect(Avatar)
className="little-spacer-right"
hash="avatart-foo"
name="name-foo"
size={16}
/>
name-foo
</span>
}
onClick={[Function]}
stat="13"
tooltip={false}
value="foo"
<React.Fragment>
<FacetItemsList>
<FacetItem
active={true}
disabled={false}
halfWidth={false}
loading={false}
name="unassigned"
onClick={[Function]}
stat="5"
tooltip="unassigned"
value=""
/>
<FacetItem
active={false}
disabled={false}
halfWidth={false}
key="foo"
loading={false}
name={
<span>
<Connect(Avatar)
className="little-spacer-right"
hash="avatart-foo"
name="name-foo"
size={16}
/>
name-foo
</span>
}
onClick={[Function]}
stat="13"
tooltip="name-foo"
value="foo"
/>
<FacetItem
active={false}
disabled={false}
halfWidth={false}
key="bar"
loading={false}
name="bar"
onClick={[Function]}
stat="7"
tooltip="bar"
value="bar"
/>
<FacetItem
active={false}
disabled={false}
halfWidth={false}
key="baz"
loading={false}
name="baz"
onClick={[Function]}
stat="6"
tooltip="baz"
value="baz"
/>
</FacetItemsList>
<FacetFooter
onSearch={[Function]}
onSelect={[Function]}
renderOption={[Function]}
/>
<FacetItem
active={false}
disabled={false}
halfWidth={false}
key="bar"
loading={false}
name="bar"
onClick={[Function]}
stat="7"
tooltip={false}
value="bar"
<MultipleSelectionHint
options={4}
values={0}
/>
<FacetItem
active={false}
disabled={false}
halfWidth={false}
key="baz"
loading={false}
name="baz"
onClick={[Function]}
stat="6"
tooltip={false}
value="baz"
/>
</FacetItemsList>
<FacetFooter
onSearch={[Function]}
onSelect={[Function]}
renderOption={[Function]}
/>
</React.Fragment>
</FacetBox>
`;

@@ -217,69 +235,75 @@ exports[`should select user 1`] = `
loading={false}
timeout={100}
/>
<FacetItemsList>
<FacetItem
active={false}
disabled={false}
halfWidth={false}
loading={false}
name="unassigned"
onClick={[Function]}
stat="5"
tooltip={true}
value=""
/>
<FacetItem
active={true}
disabled={false}
halfWidth={false}
key="foo"
loading={false}
name={
<span>
<Connect(Avatar)
className="little-spacer-right"
hash="avatart-foo"
name="name-foo"
size={16}
/>
name-foo
</span>
}
onClick={[Function]}
stat="13"
tooltip={false}
value="foo"
<React.Fragment>
<FacetItemsList>
<FacetItem
active={false}
disabled={false}
halfWidth={false}
loading={false}
name="unassigned"
onClick={[Function]}
stat="5"
tooltip="unassigned"
value=""
/>
<FacetItem
active={true}
disabled={false}
halfWidth={false}
key="foo"
loading={false}
name={
<span>
<Connect(Avatar)
className="little-spacer-right"
hash="avatart-foo"
name="name-foo"
size={16}
/>
name-foo
</span>
}
onClick={[Function]}
stat="13"
tooltip="name-foo"
value="foo"
/>
<FacetItem
active={false}
disabled={false}
halfWidth={false}
key="bar"
loading={false}
name="bar"
onClick={[Function]}
stat="7"
tooltip="bar"
value="bar"
/>
<FacetItem
active={false}
disabled={false}
halfWidth={false}
key="baz"
loading={false}
name="baz"
onClick={[Function]}
stat="6"
tooltip="baz"
value="baz"
/>
</FacetItemsList>
<FacetFooter
onSearch={[Function]}
onSelect={[Function]}
renderOption={[Function]}
/>
<FacetItem
active={false}
disabled={false}
halfWidth={false}
key="bar"
loading={false}
name="bar"
onClick={[Function]}
stat="7"
tooltip={true}
value="bar"
<MultipleSelectionHint
options={4}
values={1}
/>
<FacetItem
active={false}
disabled={false}
halfWidth={false}
key="baz"
loading={false}
name="baz"
onClick={[Function]}
stat="6"
tooltip={true}
value="baz"
/>
</FacetItemsList>
<FacetFooter
onSearch={[Function]}
onSelect={[Function]}
renderOption={[Function]}
/>
</React.Fragment>
</FacetBox>
`;

+ 128
- 104
server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/StandardFacet-test.tsx.snap View File

@@ -29,11 +29,17 @@ exports[`should render empty sub-facet 1`] = `
loading={false}
timeout={100}
/>
<div
className="search-navigator-facet-empty little-spacer-top"
>
no_results
</div>
<React.Fragment>
<div
className="search-navigator-facet-empty little-spacer-top"
>
no_results
</div>
<MultipleSelectionHint
options={0}
values={0}
/>
</React.Fragment>
</FacetBox>
`;

@@ -73,32 +79,38 @@ exports[`should render sub-facets 1`] = `
loading={false}
timeout={100}
/>
<FacetItemsList>
<FacetItem
active={false}
disabled={false}
halfWidth={false}
key="a1"
loading={false}
name="A1 - a1 title"
onClick={[Function]}
stat="15"
tooltip={true}
value="a1"
/>
<FacetItem
active={true}
disabled={false}
halfWidth={false}
key="a3"
loading={false}
name="A3"
onClick={[Function]}
stat="5"
tooltip={false}
value="a3"
<React.Fragment>
<FacetItemsList>
<FacetItem
active={false}
disabled={false}
halfWidth={false}
key="a1"
loading={false}
name="A1 - a1 title"
onClick={[Function]}
stat="15"
tooltip="A1 - a1 title"
value="a1"
/>
<FacetItem
active={true}
disabled={false}
halfWidth={false}
key="a3"
loading={false}
name="A3"
onClick={[Function]}
stat="5"
tooltip="A3"
value="a3"
/>
</FacetItemsList>
<MultipleSelectionHint
options={2}
values={1}
/>
</FacetItemsList>
</React.Fragment>
</FacetBox>
<FacetBox
className="is-inner"
@@ -118,32 +130,38 @@ exports[`should render sub-facets 1`] = `
loading={false}
timeout={100}
/>
<FacetItemsList>
<FacetItem
active={false}
disabled={false}
halfWidth={false}
key="foo"
loading={false}
name="foo"
onClick={[Function]}
stat="12"
tooltip={true}
value="foo"
<React.Fragment>
<FacetItemsList>
<FacetItem
active={false}
disabled={false}
halfWidth={false}
key="foo"
loading={false}
name="foo"
onClick={[Function]}
stat="12"
tooltip="foo"
value="foo"
/>
<FacetItem
active={true}
disabled={false}
halfWidth={false}
key="risky-resource"
loading={false}
name="Risky Resource Management"
onClick={[Function]}
stat="10"
tooltip="Risky Resource Management"
value="risky-resource"
/>
</FacetItemsList>
<MultipleSelectionHint
options={2}
values={1}
/>
<FacetItem
active={true}
disabled={false}
halfWidth={false}
key="risky-resource"
loading={false}
name="Risky Resource Management"
onClick={[Function]}
stat="10"
tooltip={false}
value="risky-resource"
/>
</FacetItemsList>
</React.Fragment>
</FacetBox>
<FacetBox
className="is-inner"
@@ -163,56 +181,62 @@ exports[`should render sub-facets 1`] = `
loading={false}
timeout={100}
/>
<FacetItemsList>
<FacetItem
active={true}
disabled={false}
halfWidth={false}
key="42"
loading={false}
name="CWE-42 - cwe-42 title"
onClick={[Function]}
stat="5"
tooltip={false}
value="42"
/>
<FacetItem
active={false}
disabled={false}
halfWidth={false}
key="173"
loading={false}
name="CWE-173"
onClick={[Function]}
stat="3"
tooltip={true}
value="173"
/>
</FacetItemsList>
<div
className="search-navigator-facet-footer"
>
<Select
className="input-super-large"
clearable={false}
noResultsText="select2.noMatches"
onChange={[Function]}
options={
Array [
Object {
"label": "CWE-42 - cwe-42 title",
"value": "42",
},
Object {
"label": "Unknown CWE",
"value": "unknown",
},
]
}
placeholder="search.search_for_cwe"
searchable={true}
<React.Fragment>
<FacetItemsList>
<FacetItem
active={true}
disabled={false}
halfWidth={false}
key="42"
loading={false}
name="CWE-42 - cwe-42 title"
onClick={[Function]}
stat="5"
tooltip="CWE-42 - cwe-42 title"
value="42"
/>
<FacetItem
active={false}
disabled={false}
halfWidth={false}
key="173"
loading={false}
name="CWE-173"
onClick={[Function]}
stat="3"
tooltip="CWE-173"
value="173"
/>
</FacetItemsList>
<div
className="search-navigator-facet-footer"
>
<Select
className="input-super-large"
clearable={false}
noResultsText="select2.noMatches"
onChange={[Function]}
options={
Array [
Object {
"label": "CWE-42 - cwe-42 title",
"value": "42",
},
Object {
"label": "Unknown CWE",
"value": "unknown",
},
]
}
placeholder="search.search_for_cwe"
searchable={true}
/>
</div>
<MultipleSelectionHint
options={2}
values={1}
/>
</div>
</React.Fragment>
</FacetBox>
</React.Fragment>
</FacetBox>

+ 22
- 35
server/sonar-web/src/main/js/components/facet/FacetItem.tsx View File

@@ -19,9 +19,6 @@
*/
import * as React from 'react';
import * as classNames from 'classnames';
import { isOnMac } from './utils';
import Tooltip from '../controls/Tooltip';
import { translate } from '../../helpers/l10n';

export interface Props {
active?: boolean;
@@ -32,7 +29,8 @@ export interface Props {
name: React.ReactNode;
onClick: (x: string, multiple?: boolean) => void;
stat?: React.ReactNode;
tooltip?: boolean;
/** Textual version of `name` */
tooltip: string;
value: string;
}

@@ -50,42 +48,31 @@ export default class FacetItem extends React.PureComponent<Props> {
};

render() {
const className = classNames('facet', 'search-navigator-facet', this.props.className, {
const { name } = this.props;
const className = classNames('search-navigator-facet', this.props.className, {
active: this.props.active,
'search-navigator-facet-half': this.props.halfWidth
});

const overlay =
this.props.tooltip && !this.props.disabled
? translate(
isOnMac()
? 'shortcuts.section.global.facets.multiselection.mac'
: 'shortcuts.section.global.facets.multiselection'
)
: undefined;

return (
<Tooltip overlay={overlay} placement="right">
{this.props.disabled ? (
<span className={className} data-facet={this.props.value}>
<span className="facet-name">{this.props.name}</span>
{this.props.stat != null && (
<span className="facet-stat">{this.props.loading ? '' : this.props.stat}</span>
)}
</span>
) : (
<a
className={className}
data-facet={this.props.value}
href="#"
onClick={this.handleClick}>
<span className="facet-name">{this.props.name}</span>
{this.props.stat != null && (
<span className="facet-stat">{this.props.loading ? '' : this.props.stat}</span>
)}
</a>
return this.props.disabled ? (
<span className={className} data-facet={this.props.value}>
<span className="facet-name">{name}</span>
{this.props.stat != null && (
<span className="facet-stat">{this.props.loading ? '' : this.props.stat}</span>
)}
</span>
) : (
<a
className={className}
data-facet={this.props.value}
href="#"
onClick={this.handleClick}
title={this.props.tooltip}>
<span className="facet-name">{name}</span>
{this.props.stat != null && (
<span className="facet-stat">{this.props.loading ? '' : this.props.stat}</span>
)}
</Tooltip>
</a>
);
}
}

server/sonar-web/src/main/js/components/facet/utils.ts → server/sonar-web/src/main/js/components/facet/MultipleSelectionHint.css View File

@@ -17,7 +17,19 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
.multiple-selection-hint {
margin-top: var(--gridSize);
margin-bottom: var(--gridSize);
text-align: center;
}

export function isOnMac() {
return navigator.userAgent.indexOf('Mac OS X') !== -1;
.multiple-selection-hint-inner {
display: inline-block;
height: var(--controlHeight);
line-height: var(--controlHeight);
border-radius: var(--controlHeight);
background-color: var(--barBorderColor);
text-align: center;
padding: 0 var(--gridSize);
font-size: var(--smallFontSize);
}

+ 50
- 0
server/sonar-web/src/main/js/components/facet/MultipleSelectionHint.tsx View File

@@ -0,0 +1,50 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 './MultipleSelectionHint.css';

interface Props {
options: number;
values: number;
}

export default function MultipleSelectionHint({ options, values }: Props) {
// do not render if nothing is selected or there are less than 2 possible options
if (values === 0 || options < 2) {
return null;
}

return (
<div className="multiple-selection-hint">
<div className="multiple-selection-hint-inner">
{translate(
isOnMac()
? 'shortcuts.section.global.facets.multiselection.mac'
: 'shortcuts.section.global.facets.multiselection'
)}
</div>
</div>
);
}

function isOnMac() {
return navigator.userAgent.indexOf('Mac OS X') !== -1;
}

+ 9
- 1
server/sonar-web/src/main/js/components/facet/__tests__/FacetItem-test.tsx View File

@@ -55,6 +55,14 @@ it('should call onClick', () => {

function renderFacetItem(props?: Partial<Props>) {
return shallow(
<FacetItem active={false} name="foo" onClick={jest.fn()} stat={null} value="bar" {...props} />
<FacetItem
active={false}
name="foo"
onClick={jest.fn()}
stat={null}
tooltip="foo"
value="bar"
{...props}
/>
);
}

+ 46
- 0
server/sonar-web/src/main/js/components/facet/__tests__/MultipleSelectionHint-test.tsx View File

@@ -0,0 +1,46 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 { shallow } from 'enzyme';
import MultipleSelectionHint from '../MultipleSelectionHint';

it('should render for mac', () => {
Object.defineProperty(navigator, 'userAgent', {
configurable: true,
value: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4)'
});
expect(shallow(<MultipleSelectionHint options={3} values={1} />)).toMatchSnapshot();
});

it('should render for windows', () => {
Object.defineProperty(navigator, 'userAgent', {
configurable: true,
value: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'
});
expect(shallow(<MultipleSelectionHint options={3} values={1} />)).toMatchSnapshot();
});

it('should not render when there is not selection', () => {
expect(shallow(<MultipleSelectionHint options={3} values={0} />).type()).toBe(null);
});

it('should not render when there are not enough options', () => {
expect(shallow(<MultipleSelectionHint options={1} values={1} />).type()).toBe(null);
});

+ 66
- 85
server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetItem-test.tsx.snap View File

@@ -1,118 +1,99 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should loading stat 1`] = `
<Tooltip
placement="right"
<a
className="search-navigator-facet"
data-facet="bar"
href="#"
onClick={[Function]}
title="foo"
>
<a
className="facet search-navigator-facet"
data-facet="bar"
href="#"
onClick={[Function]}
<span
className="facet-name"
>
<span
className="facet-name"
>
foo
</span>
</a>
</Tooltip>
foo
</span>
</a>
`;

exports[`should render active 1`] = `
<Tooltip
placement="right"
<a
className="search-navigator-facet active"
data-facet="bar"
href="#"
onClick={[Function]}
title="foo"
>
<a
className="facet search-navigator-facet active"
data-facet="bar"
href="#"
onClick={[Function]}
<span
className="facet-name"
>
<span
className="facet-name"
>
foo
</span>
</a>
</Tooltip>
foo
</span>
</a>
`;

exports[`should render disabled 1`] = `
<Tooltip
placement="right"
<span
className="search-navigator-facet"
data-facet="bar"
>
<span
className="facet search-navigator-facet"
data-facet="bar"
className="facet-name"
>
<span
className="facet-name"
>
foo
</span>
foo
</span>
</Tooltip>
</span>
`;

exports[`should render half width 1`] = `
<Tooltip
placement="right"
<a
className="search-navigator-facet search-navigator-facet-half"
data-facet="bar"
href="#"
onClick={[Function]}
title="foo"
>
<a
className="facet search-navigator-facet search-navigator-facet-half"
data-facet="bar"
href="#"
onClick={[Function]}
<span
className="facet-name"
>
<span
className="facet-name"
>
foo
</span>
</a>
</Tooltip>
foo
</span>
</a>
`;

exports[`should render inactive 1`] = `
<Tooltip
placement="right"
<a
className="search-navigator-facet"
data-facet="bar"
href="#"
onClick={[Function]}
title="foo"
>
<a
className="facet search-navigator-facet"
data-facet="bar"
href="#"
onClick={[Function]}
<span
className="facet-name"
>
<span
className="facet-name"
>
foo
</span>
</a>
</Tooltip>
foo
</span>
</a>
`;

exports[`should render stat 1`] = `
<Tooltip
placement="right"
<a
className="search-navigator-facet"
data-facet="bar"
href="#"
onClick={[Function]}
title="foo"
>
<a
className="facet search-navigator-facet"
data-facet="bar"
href="#"
onClick={[Function]}
<span
className="facet-name"
>
foo
</span>
<span
className="facet-stat"
>
<span
className="facet-name"
>
foo
</span>
<span
className="facet-stat"
>
13
</span>
</a>
</Tooltip>
13
</span>
</a>
`;

+ 25
- 0
server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/MultipleSelectionHint-test.tsx.snap View File

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

exports[`should render for mac 1`] = `
<div
className="multiple-selection-hint"
>
<div
className="multiple-selection-hint-inner"
>
shortcuts.section.global.facets.multiselection.mac
</div>
</div>
`;

exports[`should render for windows 1`] = `
<div
className="multiple-selection-hint"
>
<div
className="multiple-selection-hint-inner"
>
shortcuts.section.global.facets.multiselection
</div>
</div>
`;

+ 28
- 127
server/sonar-web/src/main/js/components/search-navigator.css View File

@@ -92,17 +92,17 @@

.search-navigator-facet {
position: relative;
display: inline-block;
vertical-align: middle;
display: inline-flex;
justify-content: space-between;
align-items: center;
width: 100%;
height: var(--controlHeight);
margin: 0 0 1px 0;
padding: 4px 6px;
border: none;
padding: 0 calc(0.75 * var(--gridSize));
border: 1px solid transparent;
border-radius: 2px;
box-sizing: border-box;
white-space: normal;
overflow: hidden;
font-size: 0;
opacity: 0.3;
cursor: not-allowed;
transition: none;
@@ -118,15 +118,9 @@ a.search-navigator-facet .facet-name {
}

a.search-navigator-facet:hover,
a.search-navigator-facet:focus {
border: 1px solid var(--blue);
padding: 3px 5px;
}

a.search-navigator-facet:hover .facet-stat,
a.search-navigator-facet:focus .facet-stat {
top: -1px;
right: -1px;
a.search-navigator-facet:focus,
.search-navigator-facet.active {
border-color: var(--blue);
}

.search-navigator-facet.facet-category {
@@ -139,38 +133,25 @@ a.search-navigator-facet:focus .facet-stat {
}

.search-navigator-facet .facet-name {
flex: 1 1 auto;
min-width: 0;
line-height: 16px;
background-color: var(--barBackgroundColor);
padding: 1px 0; /* needed to fit small ratings and levels */
color: var(--secondFontColor);
font-size: var(--smallFontSize);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}

.search-navigator-facet .facet-stat {
position: absolute;
top: 0;
right: 0;
margin-left: 5px;
padding: 5px 5px;
background-color: var(--barBackgroundColor);
display: flex;
align-items: center;
margin-left: var(--gridSize);
color: var(--secondFontColor);
font-size: var(--smallFontSize);
}

.search-navigator-facet .facet-stat:before {
content: ' ';
position: absolute;
top: 0;
bottom: 0;
right: 100%;
width: 10px;
background-image: linear-gradient(
to right,
rgba(243, 243, 243, 0),
var(--barBackgroundColor) 75%
);
}

.search-navigator-facet .facet-toggle {
display: none;
float: left;
@@ -210,27 +191,10 @@ a.search-navigator-facet:focus .facet-stat {
}

.search-navigator-facet.active {
border: 1px solid var(--blue);
padding: 3px 5px;
background-color: var(--lightBlue);
text-decoration: none;
}

.search-navigator-facet.active .facet-name {
background-color: var(--lightBlue);
}

.search-navigator-facet.active .facet-stat {
border-color: var(--blue);
background-color: var(--lightBlue);
top: -1px;
right: -1px;
}

.search-navigator-facet.active .facet-stat:before {
background-image: linear-gradient(to right, rgba(202, 227, 242, 0), var(--lightBlue) 75%);
}

.search-navigator-facet.active .facet-toggle {
display: inline;
}
@@ -269,7 +233,7 @@ a.search-navigator-facet:focus .facet-stat {
.search-navigator-facet-highlight-under-container .search-navigator-facet:hover,
.search-navigator-facet-highlight-under-container .search-navigator-facet.active {
border-bottom: none;
padding-bottom: 4px;
padding-bottom: 1px;
border-radius: 2px 2px 0 0;
}

@@ -279,38 +243,25 @@ a.search-navigator-facet:focus .facet-stat {
.search-navigator-facet-highlight-under-container
.search-navigator-facet.active
~ .search-navigator-facet {
padding-left: 5px;
padding-right: 5px;
border-left: 1px solid var(--blue);
border-right: 1px solid var(--blue);
border-top: none;
border-bottom: none;
border-left-color: var(--blue);
border-right-color: var(--blue);
border-radius: 0;
}

.search-navigator-facet-highlight-under-container
.search-navigator-facet:hover
~ .search-navigator-facet
.facet-stat,
.search-navigator-facet-highlight-under-container
.search-navigator-facet.active
~ .search-navigator-facet
.facet-stat {
right: -1px;
}

.search-navigator-facet-highlight-under-container
.search-navigator-facet:hover
~ .search-navigator-facet:last-of-type,
.search-navigator-facet-highlight-under-container
.search-navigator-facet.active
~ .search-navigator-facet:last-of-type {
padding-bottom: 3px;
border-bottom: 1px solid var(--blue);
border-radius: 0 0 2px 2px;
}

.search-navigator-facet-highlight-under-container .search-navigator-facet:hover:last-of-type,
.search-navigator-facet-highlight-under-container .search-navigator-facet.active:last-of-type {
padding-bottom: 3px;
border-bottom: 1px solid var(--blue);
border-radius: 2px;
}
@@ -322,28 +273,6 @@ a.search-navigator-facet:focus .facet-stat {
text-decoration: none;
}

.search-navigator-facet-highlight-under-container
.search-navigator-facet.active
~ .search-navigator-facet
.facet-name {
background-color: var(--lightBlue);
}

.search-navigator-facet-highlight-under-container
.search-navigator-facet.active
~ .search-navigator-facet
.facet-stat {
border-color: var(--blue);
background-color: var(--lightBlue);
}

.search-navigator-facet-highlight-under-container
.search-navigator-facet.active
~ .search-navigator-facet
.facet-stat:before {
background-image: linear-gradient(to right, rgba(202, 227, 242, 0), var(--lightBlue) 75%);
}

.search-navigator-facet-highlight-under-container
.search-navigator-facet.active
~ .search-navigator-facet
@@ -355,48 +284,20 @@ a.search-navigator-facet:focus .facet-stat {
.search-navigator-facet.active
~ .search-navigator-facet:hover,
.search-navigator-facet-highlight-under-container
.search-navigator-facet.active
~ .search-navigator-facet:hover
~ .search-navigator-facet {
background-color: #a1cde8;
text-decoration: none;
}

.search-navigator-facet-highlight-under-container
.search-navigator-facet.active
~ .search-navigator-facet:hover
.facet-name,
.search-navigator-facet-highlight-under-container
.search-navigator-facet.active
~ .search-navigator-facet:hover
~ .search-navigator-facet
.facet-name {
background-color: #a1cde8;
.search-navigator-facet:hover
~ .search-navigator-facet.active {
border-top: 1px solid var(--blue);
}

.search-navigator-facet-highlight-under-container
.search-navigator-facet.active
~ .search-navigator-facet:hover
.facet-stat,
~ .search-navigator-facet:hover,
.search-navigator-facet-highlight-under-container
.search-navigator-facet.active
~ .search-navigator-facet:hover
~ .search-navigator-facet
.facet-stat {
border-color: var(--blue);
~ .search-navigator-facet {
background-color: #a1cde8;
}

.search-navigator-facet-highlight-under-container
.search-navigator-facet.active
~ .search-navigator-facet:hover
.facet-stat:before,
.search-navigator-facet-highlight-under-container
.search-navigator-facet.active
~ .search-navigator-facet:hover
~ .search-navigator-facet
.facet-stat:before {
background-image: linear-gradient(to right, rgba(161, 205, 232, 0), #a1cde8 75%);
text-decoration: none;
}

.search-navigator-facet-highlight-under-container

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

@@ -935,7 +935,7 @@ shortcuts.section.global=Global
shortcuts.section.global.search=quickly open search bar
shortcuts.section.global.shortcuts=open this window
shortcuts.section.global.facets.multiselection=Ctrl + click to add to selection
shortcuts.section.global.facets.multiselection.mac=Cmd + click to add to selection
shortcuts.section.global.facets.multiselection.mac=\u2318 + click to add to selection

shortcuts.section.issues=Issues Page
shortcuts.section.issues.navigate_between_issues=navigate between issues

Loading…
Cancel
Save