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', () => {
|
||||
const { getByText } = render(<App />);
|
||||
const linkElement = getByText(/Unimplemented rules/i);
|
||||
const linkElement = getByText(/Rules under specification/i);
|
||||
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 Box from '@material-ui/core/Box';
|
||||
import { Link } from '@material-ui/core';
|
||||
import { Link as RouterLink } from 'react-router-dom';
|
||||
|
||||
import { useHistory } from "react-router-dom";
|
||||
|
||||
@ -24,6 +25,9 @@ const useStyles = makeStyles((theme) => ({
|
||||
marginTop: theme.spacing(3),
|
||||
marginBottom: theme.spacing(3),
|
||||
},
|
||||
ruleidLink: {
|
||||
color: 'inherit',
|
||||
},
|
||||
title: {
|
||||
textAlign: 'justify',
|
||||
marginTop: theme.spacing(4),
|
||||
@ -46,7 +50,34 @@ const useStyles = makeStyles((theme) => ({
|
||||
},
|
||||
unimplemented: {
|
||||
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({
|
||||
@ -161,7 +192,7 @@ export function RulePage(props: any) {
|
||||
let [descHTML, descError, descIsLoading] = useFetch<string>(descUrl, false);
|
||||
let [metadataJSON, metadataError, metadataIsLoading] = useFetch<RuleMetadata>(metadataUrl);
|
||||
|
||||
const {ruleCoverage, allLangsRuleCoverage} = useRuleCoverage();
|
||||
const {ruleCoverage, allLangsRuleCoverage, isLanguageCovered} = useRuleCoverage();
|
||||
let coverage: any = "Loading...";
|
||||
|
||||
let title = "Loading..."
|
||||
@ -175,7 +206,11 @@ export function RulePage(props: any) {
|
||||
}
|
||||
branch = metadataJSON.branch;
|
||||
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);
|
||||
|
||||
const coverageMapper = (key: any, range: any) => {
|
||||
@ -233,20 +268,22 @@ export function RulePage(props: any) {
|
||||
<div>
|
||||
<div className={classes.ruleBar}>
|
||||
<Container>
|
||||
<Typography variant="h2" classes={{root: classes.ruleid}}>{ruleid}</Typography>
|
||||
<Typography variant="h4" classes={{root: classes.ruleid}}>{prLink}</Typography>
|
||||
<Tabs
|
||||
{...tabsValue}
|
||||
onChange={handleLanguageChange}
|
||||
indicatorColor="primary"
|
||||
textColor="primary"
|
||||
centered
|
||||
variant="scrollable"
|
||||
scrollButtons="auto"
|
||||
classes={{ root: classes.tabRoot, scroller: classes.tabScroller }}
|
||||
>
|
||||
{languagesTabs}
|
||||
</Tabs>
|
||||
<Typography variant="h2" classes={{root: classes.ruleid}}>
|
||||
<Link className={classes.ruleidLink} component={RouterLink} to={`/${ruleid}`} underline="none">{ruleid}</Link>
|
||||
</Typography>
|
||||
<Typography variant="h4" classes={{root: classes.ruleid}}>{prLink}</Typography>
|
||||
<Tabs
|
||||
{...tabsValue}
|
||||
onChange={handleLanguageChange}
|
||||
indicatorColor="primary"
|
||||
textColor="primary"
|
||||
centered
|
||||
variant="scrollable"
|
||||
scrollButtons="auto"
|
||||
classes={{ root: classes.tabRoot, scroller: classes.tabScroller }}
|
||||
>
|
||||
{languagesTabs}
|
||||
</Tabs>
|
||||
</Container>
|
||||
</div>
|
||||
|
||||
|
@ -9,7 +9,12 @@ import Chip from '@material-ui/core/Chip';
|
||||
import { Link as RouterLink } from 'react-router-dom';
|
||||
import { Link } from '@material-ui/core';
|
||||
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) => ({
|
||||
searchHit: {
|
||||
@ -24,18 +29,33 @@ const useStyles = makeStyles((theme) => ({
|
||||
marginRight: theme.spacing(1),
|
||||
marginTop: theme.spacing(2),
|
||||
},
|
||||
languageChip: {
|
||||
coveredLanguageChip: {
|
||||
marginRight: theme.spacing(1),
|
||||
marginTop: theme.spacing(2),
|
||||
backgroundColor: '#4c9bd6',
|
||||
backgroundColor: blue,
|
||||
'&:hover, &:focus': {
|
||||
backgroundColor: '#25699D'
|
||||
backgroundColor: darkerBlue
|
||||
},
|
||||
},
|
||||
unimplementedMarker: {
|
||||
targetedLanguageChip: {
|
||||
marginRight: theme.spacing(1),
|
||||
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) {
|
||||
const { isLanguageCovered } = useRuleCoverage();
|
||||
const classes = useStyles();
|
||||
|
||||
const coveredLanguages: JSX.Element[] = [];
|
||||
const targetedLanguages: JSX.Element[] = [];
|
||||
|
||||
const actualLanguages = props.data.languages.filter(language => language !== 'default');
|
||||
const languages = actualLanguages.map(lang => (
|
||||
<Link component={RouterLink} to={`/${props.data.id}/${lang}`} style={{ textDecoration: 'none' }}>
|
||||
<Chip
|
||||
classes={{root: classes.languageChip}}
|
||||
label={lang}
|
||||
color="primary"
|
||||
clickable
|
||||
/>
|
||||
</Link>
|
||||
));
|
||||
actualLanguages.forEach(lang => {
|
||||
const covered = isLanguageCovered(lang, props.data.all_keys);
|
||||
const chip = <Link component={RouterLink} to={`/${props.data.id}/${lang}`} style={{ textDecoration: 'none' }}>
|
||||
<Chip
|
||||
classes={{root: covered ? classes.coveredLanguageChip : classes.targetedLanguageChip }}
|
||||
label={lang}
|
||||
color="primary"
|
||||
clickable
|
||||
/>
|
||||
</Link>;
|
||||
(covered ? coveredLanguages : targetedLanguages).push(chip);
|
||||
});
|
||||
const titles = props.data.titles.map(title => (
|
||||
<Typography variant="body1" component="p" gutterBottom>
|
||||
{title}
|
||||
</Typography>
|
||||
));
|
||||
let unimplementedMarker = <></>;
|
||||
if (props.data.prUrl) {
|
||||
unimplementedMarker = <Chip classes={{root: classes.unimplementedMarker}} label="Not implemented" color="secondary" />
|
||||
}
|
||||
|
||||
const coveredBlock = coveredLanguages.length === 0 ? <></>
|
||||
: <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 (
|
||||
<Card variant="outlined" classes={{root: classes.searchHit}}>
|
||||
<CardContent>
|
||||
@ -72,12 +108,10 @@ export function SearchHit(props: SearchHitProps) {
|
||||
<Link component={RouterLink} to={`/${props.data.id}`}>
|
||||
<div> Rule {props.data.id} </div>
|
||||
</Link>
|
||||
{unimplementedMarker}
|
||||
</Typography>
|
||||
{titles}
|
||||
<Typography variant="body2" component="p" classes={{root: classes.language}}>
|
||||
{languages}
|
||||
</Typography>
|
||||
{coveredBlock}
|
||||
{targetedBlock}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
|
@ -227,9 +227,6 @@ export const SearchPage = () => {
|
||||
<Box className={classes.resultsCount}>
|
||||
<Typography variant="subtitle1">Number of rules found: {numberOfHits}</Typography>
|
||||
</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>
|
||||
{resultsDisplay}
|
||||
<Pagination count={totalPages} page={pageNumber} siblingCount={2}
|
||||
|
@ -21,7 +21,7 @@ export default function TopBar() {
|
||||
SonarSource Rule Specifications
|
||||
</Typography>
|
||||
<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>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
|
@ -2,51 +2,42 @@ import { useFetch } from './useFetch';
|
||||
|
||||
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 [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() {
|
||||
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) {
|
||||
function ruleCoverageForSonarpediaKeys(languageKeys: string[], ruleKeys: string[], mapper: any) {
|
||||
if (coveredRulesError) {
|
||||
return 'Failed Loading';
|
||||
}
|
||||
@ -57,11 +48,10 @@ export function useRuleCoverage() {
|
||||
throw new Error('coveredRules is empty');
|
||||
}
|
||||
const result: any[] = [];
|
||||
sonarpediaKeys.forEach(sonarpediaKey => {
|
||||
languageKeys.forEach(language => {
|
||||
ruleKeys.forEach(ruleKey => {
|
||||
if (sonarpediaKey in coveredRules &&
|
||||
ruleKey in coveredRules[sonarpediaKey]) {
|
||||
result.push(mapper(sonarpediaKey, coveredRules[sonarpediaKey][ruleKey]))
|
||||
if (language in coveredRules && ruleKey in coveredRules[language]) {
|
||||
result.push(mapper(language, coveredRules[language][ruleKey]))
|
||||
}
|
||||
});
|
||||
});
|
||||
@ -81,8 +71,25 @@ export function useRuleCoverage() {
|
||||
}
|
||||
|
||||
function allLangsRuleCoverage(ruleKeys: string[], mapper: any) {
|
||||
const allLanguageKeys = Array.from(languageToSonarpedia.values()).flat();
|
||||
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