RULEAPI-688 Display covered languages on search page
This commit is contained in:
parent
6d8404981c
commit
1d44a68991
@ -4,8 +4,6 @@ import App from './App';
|
|||||||
|
|
||||||
test('renders see the GH PR link', () => {
|
test('renders see the GH PR link', () => {
|
||||||
const { getByText } = render(<App />);
|
const { getByText } = render(<App />);
|
||||||
const linkElement = getByText(/Unimplemented rules/i);
|
const linkElement = getByText(/Rules under specification/i);
|
||||||
expect(linkElement).toBeInTheDocument();
|
expect(linkElement).toBeInTheDocument();
|
||||||
const searchLinkElement = getByText(/Search in unimplemented/i);
|
|
||||||
expect(searchLinkElement).toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
|
@ -7,6 +7,7 @@ import Tabs from '@material-ui/core/Tabs';
|
|||||||
import Tab from '@material-ui/core/Tab';
|
import Tab from '@material-ui/core/Tab';
|
||||||
import Box from '@material-ui/core/Box';
|
import Box from '@material-ui/core/Box';
|
||||||
import { Link } from '@material-ui/core';
|
import { Link } from '@material-ui/core';
|
||||||
|
import { Link as RouterLink } from 'react-router-dom';
|
||||||
|
|
||||||
import { useHistory } from "react-router-dom";
|
import { useHistory } from "react-router-dom";
|
||||||
|
|
||||||
@ -24,6 +25,9 @@ const useStyles = makeStyles((theme) => ({
|
|||||||
marginTop: theme.spacing(3),
|
marginTop: theme.spacing(3),
|
||||||
marginBottom: theme.spacing(3),
|
marginBottom: theme.spacing(3),
|
||||||
},
|
},
|
||||||
|
ruleidLink: {
|
||||||
|
color: 'inherit',
|
||||||
|
},
|
||||||
title: {
|
title: {
|
||||||
textAlign: 'justify',
|
textAlign: 'justify',
|
||||||
marginTop: theme.spacing(4),
|
marginTop: theme.spacing(4),
|
||||||
@ -46,7 +50,34 @@ const useStyles = makeStyles((theme) => ({
|
|||||||
},
|
},
|
||||||
unimplemented: {
|
unimplemented: {
|
||||||
color: 'red'
|
color: 'red'
|
||||||
}
|
},
|
||||||
|
|
||||||
|
tab: {
|
||||||
|
display: 'flex',
|
||||||
|
|
||||||
|
"&::before": {
|
||||||
|
content: '""',
|
||||||
|
display: 'block',
|
||||||
|
width: theme.spacing(1),
|
||||||
|
height: theme.spacing(1),
|
||||||
|
marginRight: theme.spacing(1),
|
||||||
|
borderRadius: theme.spacing(1),
|
||||||
|
},
|
||||||
|
|
||||||
|
'& > .MuiTab-wrapper': {
|
||||||
|
width: 'auto',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tabCovered: {
|
||||||
|
"&::before": {
|
||||||
|
backgroundColor: '#4c9bd6',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tabTargeted: {
|
||||||
|
"&::before": {
|
||||||
|
backgroundColor: '#fd6a00',
|
||||||
|
}
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const languageToJiraProject = new Map(Object.entries({
|
const languageToJiraProject = new Map(Object.entries({
|
||||||
@ -161,7 +192,7 @@ export function RulePage(props: any) {
|
|||||||
let [descHTML, descError, descIsLoading] = useFetch<string>(descUrl, false);
|
let [descHTML, descError, descIsLoading] = useFetch<string>(descUrl, false);
|
||||||
let [metadataJSON, metadataError, metadataIsLoading] = useFetch<RuleMetadata>(metadataUrl);
|
let [metadataJSON, metadataError, metadataIsLoading] = useFetch<RuleMetadata>(metadataUrl);
|
||||||
|
|
||||||
const {ruleCoverage, allLangsRuleCoverage} = useRuleCoverage();
|
const {ruleCoverage, allLangsRuleCoverage, isLanguageCovered} = useRuleCoverage();
|
||||||
let coverage: any = "Loading...";
|
let coverage: any = "Loading...";
|
||||||
|
|
||||||
let title = "Loading..."
|
let title = "Loading..."
|
||||||
@ -175,7 +206,11 @@ export function RulePage(props: any) {
|
|||||||
}
|
}
|
||||||
branch = metadataJSON.branch;
|
branch = metadataJSON.branch;
|
||||||
metadataJSON.all_languages.sort();
|
metadataJSON.all_languages.sort();
|
||||||
languagesTabs = metadataJSON.all_languages.map(lang => <Tab label={lang} value={lang}/>);
|
languagesTabs = metadataJSON.all_languages.map(lang => {
|
||||||
|
const isImplemented = isLanguageCovered(lang, metadataJSON!.allKeys);
|
||||||
|
const classNames = classes.tab + ' ' + (isImplemented ? classes.tabCovered : classes.tabTargeted);
|
||||||
|
return <Tab label={lang} value={lang} className={classNames} />;
|
||||||
|
});
|
||||||
metadataJSONString = JSON.stringify(metadataJSON, null, 2);
|
metadataJSONString = JSON.stringify(metadataJSON, null, 2);
|
||||||
|
|
||||||
const coverageMapper = (key: any, range: any) => {
|
const coverageMapper = (key: any, range: any) => {
|
||||||
@ -233,20 +268,22 @@ export function RulePage(props: any) {
|
|||||||
<div>
|
<div>
|
||||||
<div className={classes.ruleBar}>
|
<div className={classes.ruleBar}>
|
||||||
<Container>
|
<Container>
|
||||||
<Typography variant="h2" classes={{root: classes.ruleid}}>{ruleid}</Typography>
|
<Typography variant="h2" classes={{root: classes.ruleid}}>
|
||||||
<Typography variant="h4" classes={{root: classes.ruleid}}>{prLink}</Typography>
|
<Link className={classes.ruleidLink} component={RouterLink} to={`/${ruleid}`} underline="none">{ruleid}</Link>
|
||||||
<Tabs
|
</Typography>
|
||||||
{...tabsValue}
|
<Typography variant="h4" classes={{root: classes.ruleid}}>{prLink}</Typography>
|
||||||
onChange={handleLanguageChange}
|
<Tabs
|
||||||
indicatorColor="primary"
|
{...tabsValue}
|
||||||
textColor="primary"
|
onChange={handleLanguageChange}
|
||||||
centered
|
indicatorColor="primary"
|
||||||
variant="scrollable"
|
textColor="primary"
|
||||||
scrollButtons="auto"
|
centered
|
||||||
classes={{ root: classes.tabRoot, scroller: classes.tabScroller }}
|
variant="scrollable"
|
||||||
>
|
scrollButtons="auto"
|
||||||
{languagesTabs}
|
classes={{ root: classes.tabRoot, scroller: classes.tabScroller }}
|
||||||
</Tabs>
|
>
|
||||||
|
{languagesTabs}
|
||||||
|
</Tabs>
|
||||||
</Container>
|
</Container>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -9,7 +9,12 @@ import Chip from '@material-ui/core/Chip';
|
|||||||
import { Link as RouterLink } from 'react-router-dom';
|
import { Link as RouterLink } from 'react-router-dom';
|
||||||
import { Link } from '@material-ui/core';
|
import { Link } from '@material-ui/core';
|
||||||
import { IndexedRule } from './types/IndexStore';
|
import { IndexedRule } from './types/IndexStore';
|
||||||
|
import { useRuleCoverage } from './utils/useRuleCoverage';
|
||||||
|
|
||||||
|
const blue = '#4c9bd6';
|
||||||
|
const darkerBlue = '#25699d';
|
||||||
|
const orange = '#fd6a00';
|
||||||
|
const darkerOrange = '#c45200';
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
searchHit: {
|
searchHit: {
|
||||||
@ -24,18 +29,33 @@ const useStyles = makeStyles((theme) => ({
|
|||||||
marginRight: theme.spacing(1),
|
marginRight: theme.spacing(1),
|
||||||
marginTop: theme.spacing(2),
|
marginTop: theme.spacing(2),
|
||||||
},
|
},
|
||||||
languageChip: {
|
coveredLanguageChip: {
|
||||||
marginRight: theme.spacing(1),
|
marginRight: theme.spacing(1),
|
||||||
marginTop: theme.spacing(2),
|
marginTop: theme.spacing(2),
|
||||||
backgroundColor: '#4c9bd6',
|
backgroundColor: blue,
|
||||||
'&:hover, &:focus': {
|
'&:hover, &:focus': {
|
||||||
backgroundColor: '#25699D'
|
backgroundColor: darkerBlue
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
unimplementedMarker: {
|
targetedLanguageChip: {
|
||||||
marginRight: theme.spacing(1),
|
marginRight: theme.spacing(1),
|
||||||
marginTop: theme.spacing(2),
|
marginTop: theme.spacing(2),
|
||||||
backgroundColor: '#fd6a00'
|
backgroundColor: orange,
|
||||||
|
'&:hover, &:focus': {
|
||||||
|
backgroundColor: darkerOrange
|
||||||
|
},
|
||||||
|
},
|
||||||
|
targetedMarker: {
|
||||||
|
marginTop: theme.spacing(2),
|
||||||
|
marginRight: theme.spacing(2),
|
||||||
|
borderColor: orange,
|
||||||
|
color: orange
|
||||||
|
},
|
||||||
|
coveredMarker: {
|
||||||
|
marginTop: theme.spacing(2),
|
||||||
|
marginRight: theme.spacing(2),
|
||||||
|
borderColor: blue,
|
||||||
|
color: blue
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -44,27 +64,43 @@ type SearchHitProps = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function SearchHit(props: SearchHitProps) {
|
export function SearchHit(props: SearchHitProps) {
|
||||||
|
const { isLanguageCovered } = useRuleCoverage();
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
|
|
||||||
|
const coveredLanguages: JSX.Element[] = [];
|
||||||
|
const targetedLanguages: JSX.Element[] = [];
|
||||||
|
|
||||||
const actualLanguages = props.data.languages.filter(language => language !== 'default');
|
const actualLanguages = props.data.languages.filter(language => language !== 'default');
|
||||||
const languages = actualLanguages.map(lang => (
|
actualLanguages.forEach(lang => {
|
||||||
<Link component={RouterLink} to={`/${props.data.id}/${lang}`} style={{ textDecoration: 'none' }}>
|
const covered = isLanguageCovered(lang, props.data.all_keys);
|
||||||
<Chip
|
const chip = <Link component={RouterLink} to={`/${props.data.id}/${lang}`} style={{ textDecoration: 'none' }}>
|
||||||
classes={{root: classes.languageChip}}
|
<Chip
|
||||||
label={lang}
|
classes={{root: covered ? classes.coveredLanguageChip : classes.targetedLanguageChip }}
|
||||||
color="primary"
|
label={lang}
|
||||||
clickable
|
color="primary"
|
||||||
/>
|
clickable
|
||||||
</Link>
|
/>
|
||||||
));
|
</Link>;
|
||||||
|
(covered ? coveredLanguages : targetedLanguages).push(chip);
|
||||||
|
});
|
||||||
const titles = props.data.titles.map(title => (
|
const titles = props.data.titles.map(title => (
|
||||||
<Typography variant="body1" component="p" gutterBottom>
|
<Typography variant="body1" component="p" gutterBottom>
|
||||||
{title}
|
{title}
|
||||||
</Typography>
|
</Typography>
|
||||||
));
|
));
|
||||||
let unimplementedMarker = <></>;
|
|
||||||
if (props.data.prUrl) {
|
const coveredBlock = coveredLanguages.length === 0 ? <></>
|
||||||
unimplementedMarker = <Chip classes={{root: classes.unimplementedMarker}} label="Not implemented" color="secondary" />
|
: <Typography variant="body2" component="p" classes={{root: classes.language}}>
|
||||||
}
|
<Chip classes={{root: classes.coveredMarker}} label="Covered" color="primary" variant="outlined" />
|
||||||
|
{coveredLanguages}
|
||||||
|
</Typography>;
|
||||||
|
|
||||||
|
const targetedBlock = targetedLanguages.length === 0 ? <></>
|
||||||
|
:<Typography variant="body2" component="p" classes={{root: classes.language}}>
|
||||||
|
<Chip classes={{root: classes.targetedMarker}} label="Targeted" color="secondary" variant="outlined" />
|
||||||
|
{targetedLanguages}
|
||||||
|
</Typography>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card variant="outlined" classes={{root: classes.searchHit}}>
|
<Card variant="outlined" classes={{root: classes.searchHit}}>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
@ -72,12 +108,10 @@ export function SearchHit(props: SearchHitProps) {
|
|||||||
<Link component={RouterLink} to={`/${props.data.id}`}>
|
<Link component={RouterLink} to={`/${props.data.id}`}>
|
||||||
<div> Rule {props.data.id} </div>
|
<div> Rule {props.data.id} </div>
|
||||||
</Link>
|
</Link>
|
||||||
{unimplementedMarker}
|
|
||||||
</Typography>
|
</Typography>
|
||||||
{titles}
|
{titles}
|
||||||
<Typography variant="body2" component="p" classes={{root: classes.language}}>
|
{coveredBlock}
|
||||||
{languages}
|
{targetedBlock}
|
||||||
</Typography>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)
|
)
|
||||||
|
@ -227,9 +227,6 @@ export const SearchPage = () => {
|
|||||||
<Box className={classes.resultsCount}>
|
<Box className={classes.resultsCount}>
|
||||||
<Typography variant="subtitle1">Number of rules found: {numberOfHits}</Typography>
|
<Typography variant="subtitle1">Number of rules found: {numberOfHits}</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<Typography variant="subtitle1">
|
|
||||||
<a href={"https://github.com/SonarSource/rspec/pulls?q=is%3Aopen+is%3Apr+%22Create+rule%22+" + query}>Search in unimplemented</a>
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
</Box>
|
||||||
{resultsDisplay}
|
{resultsDisplay}
|
||||||
<Pagination count={totalPages} page={pageNumber} siblingCount={2}
|
<Pagination count={totalPages} page={pageNumber} siblingCount={2}
|
||||||
|
@ -21,7 +21,7 @@ export default function TopBar() {
|
|||||||
SonarSource Rule Specifications
|
SonarSource Rule Specifications
|
||||||
</Typography>
|
</Typography>
|
||||||
<Button href="https://github.com/SonarSource/rspec/pulls?q=is%3Aopen+is%3Apr+%22Create+rule%22">
|
<Button href="https://github.com/SonarSource/rspec/pulls?q=is%3Aopen+is%3Apr+%22Create+rule%22">
|
||||||
<span className={classes.unimplemented} > Unimplemented rules </span>
|
<span className={classes.unimplemented} > Rules under specification </span>
|
||||||
</Button>
|
</Button>
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
</AppBar>
|
</AppBar>
|
||||||
|
@ -2,51 +2,42 @@ import { useFetch } from './useFetch';
|
|||||||
|
|
||||||
type RuleCoverage = Record<string, Record<string, string>>;
|
type RuleCoverage = Record<string, Record<string, string>>;
|
||||||
|
|
||||||
export function useRuleCoverage() {
|
const languageToSonarpedia = new Map<string, string[]>(Object.entries({
|
||||||
|
'abap': ['ABAP'],
|
||||||
|
'apex': ['APEX'],
|
||||||
|
'cfamily': ['CPP', 'C', 'OBJC'],
|
||||||
|
'cobol': ['COBOL'],
|
||||||
|
'csharp': ['CSH'],
|
||||||
|
'vbnet': ['VBNET'],
|
||||||
|
'css': ['CSS'],
|
||||||
|
'flex': ['FLEX'],
|
||||||
|
'kotlin': ['KOTLIN'],
|
||||||
|
'scala': ['SCALA'],
|
||||||
|
'ruby': ['RUBY'],
|
||||||
|
'go': ['GO'],
|
||||||
|
'java': ['JAVA'],
|
||||||
|
'javascript': ['JAVASCRIPT', 'JS', 'TYPESCRIPT'],
|
||||||
|
'php': ['PHP'],
|
||||||
|
'pli': ['PLI'],
|
||||||
|
'plsql': ['PLSQL'],
|
||||||
|
'python': ['PY'],
|
||||||
|
'rpg': ['RPG'],
|
||||||
|
'secrets': ['SECRETS'],
|
||||||
|
'swift': ['SWIFT'],
|
||||||
|
'tsql': ['TSQL'],
|
||||||
|
'vb6': ['VB'],
|
||||||
|
'WEB': ['WEB'],
|
||||||
|
'xml': ['XML'],
|
||||||
|
'html': ['HTML'],
|
||||||
|
'cloudformation': ['CLOUDFORMATION'],
|
||||||
|
'terraform': ['TERRAFORM']
|
||||||
|
}));
|
||||||
|
|
||||||
|
export function useRuleCoverage() {
|
||||||
const coveredRulesUrl = `${process.env.PUBLIC_URL}/covered_rules.json`;
|
const coveredRulesUrl = `${process.env.PUBLIC_URL}/covered_rules.json`;
|
||||||
const [coveredRules, coveredRulesError, coveredRulesIsLoading] = useFetch<RuleCoverage>(coveredRulesUrl);
|
const [coveredRules, coveredRulesError, coveredRulesIsLoading] = useFetch<RuleCoverage>(coveredRulesUrl);
|
||||||
const languageToSonarpedia = new Map<string, string[]>(Object.entries({
|
|
||||||
'abap': ['ABAP'],
|
|
||||||
'apex': ['APEX'],
|
|
||||||
'cfamily': ['CPP', 'C', 'OBJC'],
|
|
||||||
'cobol': ['COBOL'],
|
|
||||||
'csharp': ['CSH'],
|
|
||||||
'vbnet': ['VBNET'],
|
|
||||||
'css': ['CSS'],
|
|
||||||
'flex': ['FLEX'],
|
|
||||||
'kotlin': ['KOTLIN'],
|
|
||||||
'scala': ['SCALA'],
|
|
||||||
'ruby': ['RUBY'],
|
|
||||||
'go': ['GO'],
|
|
||||||
'java': ['JAVA'],
|
|
||||||
'javascript': ['JAVASCRIPT', 'JS', 'TYPESCRIPT'],
|
|
||||||
'php': ['PHP'],
|
|
||||||
'pli': ['PLI'],
|
|
||||||
'plsql': ['PLSQL'],
|
|
||||||
'python': ['PY'],
|
|
||||||
'rpg': ['RPG'],
|
|
||||||
'secrets': ['SECRETS'],
|
|
||||||
'swift': ['SWIFT'],
|
|
||||||
'tsql': ['TSQL'],
|
|
||||||
'vb6': ['VB'],
|
|
||||||
'WEB': ['WEB'],
|
|
||||||
'xml': ['XML'],
|
|
||||||
'html': ['HTML'],
|
|
||||||
'cloudformation': ['CLOUDFORMATION'],
|
|
||||||
'terraform': ['TERRAFORM']
|
|
||||||
}));
|
|
||||||
const allLanguageKeys = collectAllLanguageKeys();
|
|
||||||
|
|
||||||
function collectAllLanguageKeys() {
|
function ruleCoverageForSonarpediaKeys(languageKeys: string[], ruleKeys: string[], mapper: any) {
|
||||||
let ret = new Set<string>();
|
|
||||||
languageToSonarpedia.forEach((sonarpediaKeys, lang) => {
|
|
||||||
sonarpediaKeys.forEach(key => ret.add(key));
|
|
||||||
});
|
|
||||||
return Array.from(ret);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ruleCoverageForSonarpediaKeys(sonarpediaKeys: string[], ruleKeys: string[], mapper: any) {
|
|
||||||
if (coveredRulesError) {
|
if (coveredRulesError) {
|
||||||
return 'Failed Loading';
|
return 'Failed Loading';
|
||||||
}
|
}
|
||||||
@ -57,11 +48,10 @@ export function useRuleCoverage() {
|
|||||||
throw new Error('coveredRules is empty');
|
throw new Error('coveredRules is empty');
|
||||||
}
|
}
|
||||||
const result: any[] = [];
|
const result: any[] = [];
|
||||||
sonarpediaKeys.forEach(sonarpediaKey => {
|
languageKeys.forEach(language => {
|
||||||
ruleKeys.forEach(ruleKey => {
|
ruleKeys.forEach(ruleKey => {
|
||||||
if (sonarpediaKey in coveredRules &&
|
if (language in coveredRules && ruleKey in coveredRules[language]) {
|
||||||
ruleKey in coveredRules[sonarpediaKey]) {
|
result.push(mapper(language, coveredRules[language][ruleKey]))
|
||||||
result.push(mapper(sonarpediaKey, coveredRules[sonarpediaKey][ruleKey]))
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -81,8 +71,25 @@ export function useRuleCoverage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function allLangsRuleCoverage(ruleKeys: string[], mapper: any) {
|
function allLangsRuleCoverage(ruleKeys: string[], mapper: any) {
|
||||||
|
const allLanguageKeys = Array.from(languageToSonarpedia.values()).flat();
|
||||||
return ruleCoverageForSonarpediaKeys(allLanguageKeys, ruleKeys, mapper);
|
return ruleCoverageForSonarpediaKeys(allLanguageKeys, ruleKeys, mapper);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {ruleCoverage, allLangsRuleCoverage};
|
function isLanguageCovered(language: string, ruleKeys: string[]): boolean {
|
||||||
|
const languageKeys = languageToSonarpedia.get(language);
|
||||||
|
if (!languageKeys) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (coveredRulesError || coveredRulesIsLoading) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!coveredRules) {
|
||||||
|
throw new Error('coveredRules is empty');
|
||||||
|
}
|
||||||
|
return !!languageKeys.find(lang =>
|
||||||
|
ruleKeys.find(ruleKey => lang in coveredRules && ruleKey in coveredRules[lang])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {ruleCoverage, allLangsRuleCoverage, isLanguageCovered};
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user