Migrate frontend to typescript
This commit is contained in:
parent
5766963cc1
commit
badbc08602
2
frontend/.gitignore
vendored
2
frontend/.gitignore
vendored
@ -21,3 +21,5 @@
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
.eslintcache
|
@ -1,3 +1,5 @@
|
||||
# Getting Started with Create React App
|
||||
|
||||
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
||||
|
||||
## Available Scripts
|
||||
@ -6,23 +8,23 @@ In the project directory, you can run:
|
||||
|
||||
### `npm start`
|
||||
|
||||
Runs the app in the development mode.<br />
|
||||
Runs the app in the development mode.\
|
||||
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
|
||||
|
||||
The page will reload if you make edits.<br />
|
||||
The page will reload if you make edits.\
|
||||
You will also see any lint errors in the console.
|
||||
|
||||
### `npm test`
|
||||
|
||||
Launches the test runner in the interactive watch mode.<br />
|
||||
Launches the test runner in the interactive watch mode.\
|
||||
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
||||
|
||||
### `npm run build`
|
||||
|
||||
Builds the app for production to the `build` folder.<br />
|
||||
Builds the app for production to the `build` folder.\
|
||||
It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||
|
||||
The build is minified and the filenames include the hashes.<br />
|
||||
The build is minified and the filenames include the hashes.\
|
||||
Your app is ready to be deployed!
|
||||
|
||||
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
||||
@ -42,27 +44,3 @@ You don’t have to ever use `eject`. The curated feature set is suitable for sm
|
||||
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
||||
|
||||
To learn React, check out the [React documentation](https://reactjs.org/).
|
||||
|
||||
### Code Splitting
|
||||
|
||||
This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting
|
||||
|
||||
### Analyzing the Bundle Size
|
||||
|
||||
This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size
|
||||
|
||||
### Making a Progressive Web App
|
||||
|
||||
This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app
|
||||
|
||||
### Advanced Configuration
|
||||
|
||||
This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration
|
||||
|
||||
### Deployment
|
||||
|
||||
This section has moved here: https://facebook.github.io/create-react-app/docs/deployment
|
||||
|
||||
### `npm run build` fails to minify
|
||||
|
||||
This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify
|
||||
|
11932
frontend/package-lock.json
generated
11932
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,23 +1,27 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"name": "rspec",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"homepage": "http://sonarsource.github.io/rspec",
|
||||
"dependencies": {
|
||||
"@material-ui/core": "^4.10.2",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "^4.0.0-alpha.56",
|
||||
"@testing-library/jest-dom": "^4.2.4",
|
||||
"@testing-library/react": "^9.5.0",
|
||||
"@testing-library/user-event": "^7.2.1",
|
||||
"asciidoctor": "^2.2.0",
|
||||
"jsdom": "^16.2.2",
|
||||
"lunr": "^2.3.8",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"@material-ui/core": "^4.11.3",
|
||||
"@material-ui/icons": "^4.11.2",
|
||||
"@material-ui/lab": "^4.0.0-alpha.57",
|
||||
"@testing-library/jest-dom": "^5.11.9",
|
||||
"@testing-library/react": "^11.2.3",
|
||||
"@testing-library/user-event": "^12.6.2",
|
||||
"@types/jest": "^26.0.20",
|
||||
"@types/node": "^12.19.15",
|
||||
"@types/react": "^16.14.2",
|
||||
"@types/react-dom": "^16.9.10",
|
||||
"asciidoctor": "^2.2.1",
|
||||
"lunr": "^2.3.9",
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-scripts": "3.4.1",
|
||||
"string-strip-html": "^8.0.1"
|
||||
"react-scripts": "4.0.1",
|
||||
"string-strip-html": "^8.0.1",
|
||||
"typescript": "^4.1.3",
|
||||
"web-vitals": "^0.2.4"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
@ -29,7 +33,10 @@
|
||||
"deploy": "gh-pages -d build"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "react-app"
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
@ -42,5 +49,9 @@
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/lunr": "^2.3.3",
|
||||
"@types/react-router-dom": "^5.1.7"
|
||||
}
|
||||
}
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.8 KiB |
@ -9,11 +9,6 @@
|
||||
name="description"
|
||||
content="Web site created using create-react-app"
|
||||
/>
|
||||
<meta
|
||||
name="viewport"
|
||||
content="minimum-scale=1, initial-scale=1, width=device-width"
|
||||
/> <!-- Required by Material UI -->
|
||||
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
|
@ -12,6 +12,7 @@ import { useHistory } from "react-router-dom";
|
||||
|
||||
import { useRuleCoverage } from './utils/useRuleCoverage';
|
||||
import { useFetch } from './utils/useFetch';
|
||||
import { RuleMetadata } from './types';
|
||||
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
@ -41,11 +42,11 @@ const useStyles = makeStyles((theme) => ({
|
||||
justifyContent: "center"
|
||||
},
|
||||
tabScroller: {
|
||||
flexGrow: "0"
|
||||
flexGrow: 0
|
||||
}
|
||||
}));
|
||||
|
||||
const languageToJiraProject = {
|
||||
const languageToJiraProject = new Map(Object.entries({
|
||||
"PYTHON": "SONARPY",
|
||||
"ABAP": "SONARABAP",
|
||||
"CFAMILY": "CPP",
|
||||
@ -66,9 +67,9 @@ const languageToJiraProject = {
|
||||
"TSQL": "SONARTSQL",
|
||||
"VB6": "SONARVBSIX",
|
||||
"XML": "SONARXML",
|
||||
};
|
||||
}));
|
||||
|
||||
const languageToGithubProject = {
|
||||
const languageToGithubProject = new Map(Object.entries({
|
||||
"ABAP": "sonar-abap",
|
||||
"CSHARP": "sonar-dotnet",
|
||||
"VBNET": "sonar-dotnet",
|
||||
@ -94,15 +95,15 @@ const languageToGithubProject = {
|
||||
"Swift": "sonar-swift",
|
||||
"T-SQL": "sonar-tsql",
|
||||
"XML": "sonar-xml",
|
||||
}
|
||||
}));
|
||||
|
||||
|
||||
export function RulePage(props) {
|
||||
export function RulePage(props: any) {
|
||||
const ruleid = props.match.params.ruleid;
|
||||
const language = props.match.params.language;
|
||||
|
||||
const history = useHistory();
|
||||
function handleLanguageChange(event, lang) {
|
||||
function handleLanguageChange(event: any, lang: string) {
|
||||
history.push(`/${ruleid}/${lang}`);
|
||||
}
|
||||
|
||||
@ -112,22 +113,22 @@ export function RulePage(props) {
|
||||
let metadataUrl = process.env.PUBLIC_URL + '/rules/' + ruleid + "/" + language + "-metadata.json";
|
||||
let editOnGithubUrl = 'https://github.com/SonarSource/rspec/tree/master/rules/' + ruleid + '/' + language;
|
||||
|
||||
let [descHTML, descError, descIsLoading] = useFetch(descUrl, null, false);
|
||||
let [metadataJSON, metadataError, metadataIsLoading] = useFetch(metadataUrl, null, true);
|
||||
let [descHTML, descError, descIsLoading] = useFetch<string>(descUrl, false);
|
||||
let [metadataJSON, metadataError, metadataIsLoading] = useFetch<RuleMetadata>(metadataUrl);
|
||||
|
||||
const ruleCoverage = useRuleCoverage();
|
||||
let coverage = "Loading...";
|
||||
let coverage: any = "Loading...";
|
||||
|
||||
let title = "Loading..."
|
||||
let metadataJSONString;
|
||||
let languagesTabs = null;
|
||||
if (!metadataIsLoading && !metadataError) {
|
||||
if (metadataJSON && !metadataIsLoading && !metadataError) {
|
||||
title = metadataJSON.title
|
||||
metadataJSON.all_languages.sort()
|
||||
languagesTabs = metadataJSON.all_languages.map(lang => <Tab label={lang} value={lang}/>)
|
||||
metadataJSONString = JSON.stringify(metadataJSON, null, 2);
|
||||
|
||||
coverage = ruleCoverage(language, metadataJSON.allKeys, (key, version) => {
|
||||
coverage = ruleCoverage(language, metadataJSON.allKeys, (key: any, version: any) => {
|
||||
return (
|
||||
<li>{key}: {version}</li>
|
||||
)
|
||||
@ -135,7 +136,7 @@ export function RulePage(props) {
|
||||
}
|
||||
|
||||
let description = <div>Loading...</div>;
|
||||
if (!descIsLoading && !descError) {
|
||||
if (descHTML !== null && !descIsLoading && !descError) {
|
||||
description = <div>
|
||||
<div dangerouslySetInnerHTML={{__html: descHTML}}/>
|
||||
<hr />
|
||||
@ -147,8 +148,8 @@ export function RulePage(props) {
|
||||
const ruleNumber = ruleid.substring(1)
|
||||
|
||||
const upperCaseLanguage = language.toUpperCase();
|
||||
const jiraProject = languageToJiraProject[upperCaseLanguage];
|
||||
const githubProject = languageToGithubProject[upperCaseLanguage];
|
||||
const jiraProject = languageToJiraProject.get(upperCaseLanguage);
|
||||
const githubProject = languageToGithubProject.get(upperCaseLanguage);
|
||||
|
||||
let ticketsLink;
|
||||
if (upperCaseLanguage in languageToJiraProject) {
|
||||
@ -193,14 +194,14 @@ export function RulePage(props) {
|
||||
|
||||
<Container maxWidth="md">
|
||||
<Typography variant="h3" classes={{root: classes.title}}>{title}</Typography>
|
||||
<Box classes={{root: classes.coverage}}>
|
||||
<Box className={classes.coverage}>
|
||||
<Typography variant="h4" >Covered Since</Typography>
|
||||
<ul>
|
||||
{coverage}
|
||||
</ul>
|
||||
</Box>
|
||||
|
||||
<Box classes={{root: classes.coverage}}>
|
||||
<Box className={classes.coverage}>
|
||||
<Typography variant="h4" >Related Tickets and Pull Requests</Typography>
|
||||
<ul>
|
||||
{ticketsLink}
|
||||
@ -210,7 +211,7 @@ export function RulePage(props) {
|
||||
</ul>
|
||||
</Box>
|
||||
|
||||
<Box classes={{root: classes.description}}>
|
||||
<Box>
|
||||
<Typography variant="h4">Description</Typography>
|
||||
<Typography className={classes.description}>
|
||||
{description}
|
@ -8,6 +8,7 @@ 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';
|
||||
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
@ -24,7 +25,11 @@ const useStyles = makeStyles((theme) => ({
|
||||
}
|
||||
}));
|
||||
|
||||
export function SearchHit(props) {
|
||||
type SearchHitProps = {
|
||||
data: IndexedRule
|
||||
}
|
||||
|
||||
export function SearchHit(props: SearchHitProps) {
|
||||
const classes = useStyles();
|
||||
const languages = props.data.languages.map(lang => (
|
||||
<Chip
|
||||
@ -34,7 +39,7 @@ export function SearchHit(props) {
|
||||
/>
|
||||
));
|
||||
const titles = props.data.titles.split('\n').map(title => (
|
||||
<Typography className={{root: classes.title}} variant="body1" component="p" gutterBottom>
|
||||
<Typography variant="body1" component="p" gutterBottom>
|
||||
{title}
|
||||
</Typography>
|
||||
));
|
||||
@ -46,7 +51,7 @@ export function SearchHit(props) {
|
||||
Rule {props.data.id}
|
||||
</Typography>
|
||||
{titles}
|
||||
<Typography variant="body2" component="p" classes={{root: classes.languages}}>
|
||||
<Typography variant="body2" component="p" classes={{root: classes.language}}>
|
||||
{languages}
|
||||
</Typography>
|
||||
</CardContent>
|
@ -11,6 +11,7 @@ import Box from '@material-ui/core/Box';
|
||||
import useStyles from './SearchPage.style';
|
||||
import { useSearch } from './utils/useSearch';
|
||||
import {
|
||||
SearchParamSetter,
|
||||
useLocationSearch,
|
||||
useLocationSearchState
|
||||
} from './utils/routing';
|
||||
@ -23,23 +24,28 @@ export const SearchPage = () => {
|
||||
const [query, setQuery] = useLocationSearchState('query', '');
|
||||
|
||||
const [ruleType, setRuleType] = useLocationSearchState('types', 'ALL');
|
||||
const allRuleTypes = {'BUG': 'Bug', 'CODE_SMELL': 'Code Smell', 'SECURITY_HOTSPOT': 'Security Hotspot', 'VULNERABILITY': 'Vulnerability'};
|
||||
const allRuleTypes: Record<string,string> = {
|
||||
'BUG': 'Bug',
|
||||
'CODE_SMELL': 'Code Smell',
|
||||
'SECURITY_HOTSPOT': 'Security Hotspot',
|
||||
'VULNERABILITY': 'Vulnerability'
|
||||
};
|
||||
|
||||
const [ruleTags, setRuleTags] = useLocationSearchState('tags', [], value => value ? value.split(',') : []);
|
||||
const [ruleTags, setRuleTags] = useLocationSearchState<string[]>('tags', [], value => value ? value.split(',') : []);
|
||||
const allRuleTags = ["confusing", 'pitfall', 'clumsy', 'junit', 'tests']; // TODO: generate this list
|
||||
|
||||
const [pageNumber, setPageNumber] = useLocationSearchState('page', 1, parseInt);
|
||||
const [, setLocationSearch] = useLocationSearch();
|
||||
const {setLocationSearch} = useLocationSearch();
|
||||
|
||||
|
||||
const [results, numberOfHits, error, resultsAreLoading] = useSearch(query,
|
||||
const {results, numberOfHits, error, loading} = useSearch(query,
|
||||
ruleType === "ALL" ? null : ruleType,
|
||||
ruleTags,
|
||||
pageSize, pageNumber);
|
||||
const totalPages = Math.ceil(numberOfHits/pageSize);
|
||||
const totalPages = numberOfHits ? Math.ceil(numberOfHits/pageSize) : 0;
|
||||
|
||||
let resultsDisplay="No rule found...";
|
||||
if (resultsAreLoading) {
|
||||
let resultsDisplay: string|JSX.Element[] = "No rule found...";
|
||||
if (loading) {
|
||||
resultsDisplay = "Searching";
|
||||
}
|
||||
else if (results.length > 0) {
|
||||
@ -50,11 +56,13 @@ export const SearchPage = () => {
|
||||
)
|
||||
}
|
||||
|
||||
const paramSetters = {types: setRuleType, tags: setRuleTags, query: setQuery};
|
||||
function handleUpdate(field) {
|
||||
return function(event) {
|
||||
const paramSetters: Record<string, SearchParamSetter<any>> = {types: setRuleType, tags: setRuleTags, query: setQuery};
|
||||
function handleUpdate(field: string) {
|
||||
return function(event: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) {
|
||||
if (pageNumber > 1) {
|
||||
const uriSearch = {query: query, types: ruleType, tags: ruleTags, page: 1};
|
||||
const uriSearch: Record<string, any> = {
|
||||
query: query, types: ruleType, tags: ruleTags, page: 1
|
||||
};
|
||||
uriSearch[field] = event.target.value;
|
||||
setLocationSearch(uriSearch);
|
||||
} else {
|
||||
@ -84,7 +92,7 @@ export const SearchPage = () => {
|
||||
variant="outlined"
|
||||
value={query}
|
||||
onChange={handleUpdate("query")}
|
||||
error={error}
|
||||
error={!!error}
|
||||
helperText={error}
|
||||
/>
|
||||
</Grid>
|
||||
@ -114,15 +122,15 @@ export const SearchPage = () => {
|
||||
fullWidth
|
||||
SelectProps={{
|
||||
multiple: true,
|
||||
renderValue: (selected: any) => {
|
||||
return selected.join(', ');
|
||||
}
|
||||
}}
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
label="Rule Tags"
|
||||
value={ruleTags}
|
||||
onChange={handleUpdate("tags")}
|
||||
renderValue={(selected) => {
|
||||
return selected.join(', ');
|
||||
}}
|
||||
>
|
||||
{allRuleTags.map((ruleType) => (
|
||||
<MenuItem key={ruleType} value={ruleType}>
|
@ -1,17 +0,0 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import './index.css';
|
||||
import App from './App';
|
||||
import * as serviceWorker from './serviceWorker';
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
document.getElementById('root')
|
||||
);
|
||||
|
||||
// If you want your app to work offline and load faster, you can change
|
||||
// unregister() to register() below. Note this comes with some pitfalls.
|
||||
// Learn more about service workers: https://bit.ly/CRA-PWA
|
||||
serviceWorker.unregister();
|
11
frontend/src/index.tsx
Normal file
11
frontend/src/index.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import './index.css';
|
||||
import App from './App';
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
document.getElementById('root')
|
||||
);
|
1
frontend/src/react-app-env.d.ts
vendored
Normal file
1
frontend/src/react-app-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
/// <reference types="react-scripts" />
|
@ -1,141 +0,0 @@
|
||||
// This optional code is used to register a service worker.
|
||||
// register() is not called by default.
|
||||
|
||||
// This lets the app load faster on subsequent visits in production, and gives
|
||||
// it offline capabilities. However, it also means that developers (and users)
|
||||
// will only see deployed updates on subsequent visits to a page, after all the
|
||||
// existing tabs open on the page have been closed, since previously cached
|
||||
// resources are updated in the background.
|
||||
|
||||
// To learn more about the benefits of this model and instructions on how to
|
||||
// opt-in, read https://bit.ly/CRA-PWA
|
||||
|
||||
const isLocalhost = Boolean(
|
||||
window.location.hostname === 'localhost' ||
|
||||
// [::1] is the IPv6 localhost address.
|
||||
window.location.hostname === '[::1]' ||
|
||||
// 127.0.0.0/8 are considered localhost for IPv4.
|
||||
window.location.hostname.match(
|
||||
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
|
||||
)
|
||||
);
|
||||
|
||||
export function register(config) {
|
||||
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
|
||||
// The URL constructor is available in all browsers that support SW.
|
||||
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
|
||||
if (publicUrl.origin !== window.location.origin) {
|
||||
// Our service worker won't work if PUBLIC_URL is on a different origin
|
||||
// from what our page is served on. This might happen if a CDN is used to
|
||||
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
|
||||
return;
|
||||
}
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
|
||||
|
||||
if (isLocalhost) {
|
||||
// This is running on localhost. Let's check if a service worker still exists or not.
|
||||
checkValidServiceWorker(swUrl, config);
|
||||
|
||||
// Add some additional logging to localhost, pointing developers to the
|
||||
// service worker/PWA documentation.
|
||||
navigator.serviceWorker.ready.then(() => {
|
||||
console.log(
|
||||
'This web app is being served cache-first by a service ' +
|
||||
'worker. To learn more, visit https://bit.ly/CRA-PWA'
|
||||
);
|
||||
});
|
||||
} else {
|
||||
// Is not localhost. Just register service worker
|
||||
registerValidSW(swUrl, config);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function registerValidSW(swUrl, config) {
|
||||
navigator.serviceWorker
|
||||
.register(swUrl)
|
||||
.then(registration => {
|
||||
registration.onupdatefound = () => {
|
||||
const installingWorker = registration.installing;
|
||||
if (installingWorker == null) {
|
||||
return;
|
||||
}
|
||||
installingWorker.onstatechange = () => {
|
||||
if (installingWorker.state === 'installed') {
|
||||
if (navigator.serviceWorker.controller) {
|
||||
// At this point, the updated precached content has been fetched,
|
||||
// but the previous service worker will still serve the older
|
||||
// content until all client tabs are closed.
|
||||
console.log(
|
||||
'New content is available and will be used when all ' +
|
||||
'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
|
||||
);
|
||||
|
||||
// Execute callback
|
||||
if (config && config.onUpdate) {
|
||||
config.onUpdate(registration);
|
||||
}
|
||||
} else {
|
||||
// At this point, everything has been precached.
|
||||
// It's the perfect time to display a
|
||||
// "Content is cached for offline use." message.
|
||||
console.log('Content is cached for offline use.');
|
||||
|
||||
// Execute callback
|
||||
if (config && config.onSuccess) {
|
||||
config.onSuccess(registration);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error during service worker registration:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function checkValidServiceWorker(swUrl, config) {
|
||||
// Check if the service worker can be found. If it can't reload the page.
|
||||
fetch(swUrl, {
|
||||
headers: { 'Service-Worker': 'script' },
|
||||
})
|
||||
.then(response => {
|
||||
// Ensure service worker exists, and that we really are getting a JS file.
|
||||
const contentType = response.headers.get('content-type');
|
||||
if (
|
||||
response.status === 404 ||
|
||||
(contentType != null && contentType.indexOf('javascript') === -1)
|
||||
) {
|
||||
// No service worker found. Probably a different app. Reload the page.
|
||||
navigator.serviceWorker.ready.then(registration => {
|
||||
registration.unregister().then(() => {
|
||||
window.location.reload();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// Service worker found. Proceed as normal.
|
||||
registerValidSW(swUrl, config);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
console.log(
|
||||
'No internet connection found. App is running in offline mode.'
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function unregister() {
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.ready
|
||||
.then(registration => {
|
||||
registration.unregister();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error.message);
|
||||
});
|
||||
}
|
||||
}
|
14
frontend/src/types/IndexStore.ts
Normal file
14
frontend/src/types/IndexStore.ts
Normal file
@ -0,0 +1,14 @@
|
||||
export interface IndexedRule {
|
||||
id: number;
|
||||
languages: string[];
|
||||
// FIXME: type, defaultSeverity should never be null but the index generation has a bug
|
||||
type: 'BUG'|'CODE_SMELL'|'VULNERABILITY'|'SECURITY_HOTSPOT'|null;
|
||||
defaultSeverity: 'Blocker'|'Critical'|'Major'|'Minor'|'Info'|null,
|
||||
// FIXME: titles should be a list instead of being a concatenation of titles.
|
||||
titles: string,
|
||||
tags: string[],
|
||||
// FIXME: quality profiles seem to always be empty
|
||||
qualityProfiles: string[]
|
||||
}
|
||||
|
||||
export type IndexStore = Record<string, IndexedRule>
|
5
frontend/src/types/RuleMetadata.ts
Normal file
5
frontend/src/types/RuleMetadata.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export default interface RuleMetadata {
|
||||
title: string,
|
||||
all_languages: string[],
|
||||
allKeys: string[]
|
||||
}
|
3
frontend/src/types/index.ts
Normal file
3
frontend/src/types/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import RuleMetadata from './RuleMetadata';
|
||||
|
||||
export type { RuleMetadata };
|
@ -6,7 +6,7 @@ export function useLocationSearch() {
|
||||
const location = useLocation();
|
||||
const history = useHistory();
|
||||
|
||||
function setLocationSearch(searchParams, push=true) {
|
||||
function setLocationSearch(searchParams: Record<string, string>, push=true) {
|
||||
const search = new URLSearchParams(location.search);
|
||||
|
||||
for (const [key, value] of Object.entries(searchParams)) {
|
||||
@ -20,11 +20,26 @@ export function useLocationSearch() {
|
||||
}
|
||||
}
|
||||
|
||||
return [new URLSearchParams(location.search), setLocationSearch];
|
||||
return {searchParams: new URLSearchParams(location.search), setLocationSearch};
|
||||
}
|
||||
|
||||
export function useLocationSearchState(name, defaultValue, paramToState=value=>value ? value: defaultValue, stateToParam=value=>value ? value.toString(): defaultValue) {
|
||||
const [state, setState] = useState(defaultValue);
|
||||
interface Serializable {
|
||||
toString: () => string
|
||||
}
|
||||
|
||||
export type SearchParamSetter<ValueType> = (value: ValueType, { push, skipURI }?: {
|
||||
push?: boolean | undefined;
|
||||
skipURI?: boolean | undefined;
|
||||
}) => void;
|
||||
|
||||
export function useLocationSearchState<ValueType extends Serializable = string>(
|
||||
name: string,
|
||||
defaultValue: ValueType,
|
||||
// Default paramToState function works if ValueType is string.
|
||||
paramToState=(param: any) => param !== undefined ? param: defaultValue,
|
||||
stateToParam=(state: ValueType) => state !== undefined ? state.toString(): defaultValue.toString()
|
||||
): [ValueType, SearchParamSetter<ValueType>] {
|
||||
const [state, setState] = useState<ValueType>(defaultValue);
|
||||
const location = useLocation();
|
||||
const history = useHistory();
|
||||
|
||||
@ -33,14 +48,14 @@ export function useLocationSearchState(name, defaultValue, paramToState=value=>v
|
||||
if (search.has(name) && search.get(name) !== stateToParam(state)) {
|
||||
setState(paramToState(search.get(name)));
|
||||
} else if (!search.has(name) && stateToParam(state) !== stateToParam(defaultValue)) {
|
||||
setState(defaultValue);
|
||||
setState(paramToState(defaultValue));
|
||||
}
|
||||
}, [name, defaultValue, paramToState, stateToParam, state, location, history]);
|
||||
|
||||
function setSearchParam(value, {push=true, skipURI=false} = {}) {
|
||||
function setSearchParam(value: ValueType, {push=true, skipURI=false} = {}) {
|
||||
const search = new URLSearchParams(location.search);
|
||||
|
||||
search.set(name, value);
|
||||
search.set(name, stateToParam(value));
|
||||
setState(value);
|
||||
if (push) {
|
||||
history.push(`${location.pathname}?${search.toString()}`);
|
@ -1,7 +1,11 @@
|
||||
import React from 'react';
|
||||
|
||||
export function useFetch(url, options=null, parseJSON=true) {
|
||||
const [response, setResponse] = React.useState(null);
|
||||
export function useFetch<FetchedType>(
|
||||
url: string,
|
||||
parseJSON=true,
|
||||
options?: Record<string, any>
|
||||
): [FetchedType|null, any, boolean] {
|
||||
const [response, setResponse] = React.useState<FetchedType|null>(null);
|
||||
const [error, setError] = React.useState(null);
|
||||
const [isLoading, setIsLoading] = React.useState(true);
|
||||
|
@ -1,10 +1,12 @@
|
||||
import { useFetch } from './useFetch';
|
||||
|
||||
type RuleCoverage = Record<string, Record<string, string>>;
|
||||
|
||||
export function useRuleCoverage() {
|
||||
|
||||
const coveredRulesUrl = `${process.env.PUBLIC_URL}/covered_rules.json`;
|
||||
const [coveredRules, coveredRulesError, coveredRulesIsLoading] = useFetch(coveredRulesUrl);
|
||||
const languageToSonarpedia = {
|
||||
const [coveredRules, coveredRulesError, coveredRulesIsLoading] = useFetch<RuleCoverage>(coveredRulesUrl);
|
||||
const languageToSonarpedia = new Map<string, string[]>(Object.entries({
|
||||
'abap': ['ABAP'],
|
||||
'apex': ['APEX'],
|
||||
'cfamily': ['CPP', 'C', 'OBJC'],
|
||||
@ -29,18 +31,25 @@ export function useRuleCoverage() {
|
||||
'vb6': ['VB'],
|
||||
'WEB': ['WEB'],
|
||||
'xml': ['XML']
|
||||
};
|
||||
function ruleCoverage(language, ruleKeys, mapper) {
|
||||
}));
|
||||
function ruleCoverage(language: string, ruleKeys: string[], mapper: any) {
|
||||
if (coveredRulesError) {
|
||||
return 'Failed Loading';
|
||||
}
|
||||
if (coveredRulesIsLoading) {
|
||||
return 'Loading';
|
||||
}
|
||||
if (!coveredRules) {
|
||||
throw new Error('coveredRules is empty');
|
||||
}
|
||||
// return "FIXME"
|
||||
const result = [];
|
||||
const result: any[] = [];
|
||||
// const keys = coveredRules.keys;
|
||||
languageToSonarpedia[language].forEach(sonarpediaKey => {
|
||||
const languageKeys = languageToSonarpedia.get(language);
|
||||
if (!languageKeys) {
|
||||
throw new Error(`Unknown key ${language}`)
|
||||
}
|
||||
languageKeys.forEach(sonarpediaKey => {
|
||||
ruleKeys.forEach(ruleKey => {
|
||||
if (ruleKey in coveredRules[sonarpediaKey]) {
|
||||
result.push(mapper(sonarpediaKey, coveredRules[sonarpediaKey][ruleKey]))
|
@ -3,23 +3,24 @@ import React, { useState } from 'react';
|
||||
import * as lunr from 'lunr'
|
||||
|
||||
import { useFetch } from './useFetch';
|
||||
import { IndexedRule, IndexStore } from '../types/IndexStore';
|
||||
|
||||
export function useSearch(query, ruleType, ruleTags, pageSize, pageNumber) {
|
||||
export function useSearch(query: string, ruleType: string|null, ruleTags: string[], pageSize: number, pageNumber: number) {
|
||||
let indexDataUrl = `${process.env.PUBLIC_URL}/rules/rule-index.json`;
|
||||
let storeDataUrl = `${process.env.PUBLIC_URL}/rules/rule-index-store.json`;
|
||||
|
||||
const [indexData, indexDataError, indexDataIsLoading] = useFetch(indexDataUrl);
|
||||
const [storeData, storeDataError, storeDataIsLoading] = useFetch(storeDataUrl);
|
||||
const [index, setIndex] = useState(null);
|
||||
const [indexData, indexDataError, indexDataIsLoading] = useFetch<object>(indexDataUrl);
|
||||
const [storeData, storeDataError, storeDataIsLoading] = useFetch<IndexStore>(storeDataUrl);
|
||||
const [index, setIndex] = useState<lunr.Index|null>(null);
|
||||
|
||||
const [results, setResults] = useState([]);
|
||||
const [numberOfHits, setNumberOfHits] = useState(null);
|
||||
const [error, setError] = useState(null);
|
||||
const [resultsAreloading, setResultsAreLoading] = useState(true);
|
||||
const [results, setResults] = useState<IndexedRule[]>([]);
|
||||
const [numberOfHits, setNumberOfHits] = useState<number|null>(null);
|
||||
const [error, setError] = useState<string|null>(null);
|
||||
const [loading, setResultsAreLoading] = useState(true);
|
||||
|
||||
React.useEffect(() => {
|
||||
console.log(`trying to load index`);
|
||||
if (!indexDataIsLoading && !indexDataError) {
|
||||
if (indexData && !indexDataIsLoading && !indexDataError) {
|
||||
console.log("Loading Index");
|
||||
setIndex(lunr.Index.load(indexData));
|
||||
}
|
||||
@ -28,7 +29,7 @@ export function useSearch(query, ruleType, ruleTags, pageSize, pageNumber) {
|
||||
React.useEffect(() => {
|
||||
console.log(`trying to run query`);
|
||||
if (index != null && !storeDataIsLoading && !storeDataError) {
|
||||
let hits = []
|
||||
let hits: lunr.Index.Result[] = []
|
||||
setError(null);
|
||||
try {
|
||||
// We use index.query instead if index.search in order to fully
|
||||
@ -65,12 +66,14 @@ export function useSearch(query, ruleType, ruleTags, pageSize, pageNumber) {
|
||||
throw exception;
|
||||
}
|
||||
}
|
||||
if (storeData) {
|
||||
setNumberOfHits(hits.length)
|
||||
const pageResults = hits.slice(pageSize*(pageNumber - 1), pageSize*(pageNumber));
|
||||
setResults(pageResults.map(({ ref }) => storeData[ref]));
|
||||
setResultsAreLoading(false);
|
||||
}
|
||||
}
|
||||
}, [query, ruleType, ruleTags, pageSize, pageNumber, storeData, storeDataIsLoading, storeDataError, index]);
|
||||
|
||||
return [results, numberOfHits, error, resultsAreloading];
|
||||
return {results, numberOfHits, error, loading};
|
||||
}
|
26
frontend/tsconfig.json
Normal file
26
frontend/tsconfig.json
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user