Migrate frontend to typescript

This commit is contained in:
Nicolas Harraudeau 2021-01-26 22:10:28 +01:00 committed by Arseniy Zaostrovnykh
parent 5766963cc1
commit badbc08602
28 changed files with 7365 additions and 5080 deletions

2
frontend/.gitignore vendored
View File

@ -21,3 +21,5 @@
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.eslintcache

View File

@ -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 dont 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

File diff suppressed because it is too large Load Diff

View File

@ -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

View File

@ -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

View File

@ -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}

View File

@ -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>

View File

@ -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}>

View File

@ -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
View 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
View File

@ -0,0 +1 @@
/// <reference types="react-scripts" />

View File

@ -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);
});
}
}

View 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>

View File

@ -0,0 +1,5 @@
export default interface RuleMetadata {
title: string,
all_languages: string[],
allKeys: string[]
}

View File

@ -0,0 +1,3 @@
import RuleMetadata from './RuleMetadata';
export type { RuleMetadata };

View File

@ -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()}`);

View File

@ -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);

View File

@ -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]))

View File

@ -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
View 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"
]
}