diff --git a/.eslintrc.js b/.eslintrc.js index 8b42a5f9..d00f8866 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -6,12 +6,14 @@ module.exports = { "globals": { }, "plugins": [ + "@typescript-eslint", "jsx-a11y", "react-hooks" ], "extends": [ 'plugin:react/recommended', "eslint:recommended", + "plugin:@typescript-eslint/recommended", "plugin:jsx-a11y/recommended", "plugin:react-hooks/recommended" ], @@ -25,7 +27,7 @@ module.exports = { "version": "detect" } }, - "parser": "@babel/eslint-parser", + "parser": "@typescript-eslint/parser", "parserOptions": { "sourceType": "module", "ecmaFeatures": { @@ -40,9 +42,10 @@ module.exports = { "dot-location": ["error", "property"], "eol-last": "error", eqeqeq: "error", - "jsx-quotes": "error", // autofixable + "jsx-quotes": "error", "keyword-spacing": "error", - "no-array-constructor": "error", + "no-array-constructor": "off", + "@typescript-eslint/no-array-constructor": "error", "no-console": "off", "no-duplicate-imports": "error", "no-empty": "off", @@ -52,31 +55,34 @@ module.exports = { "no-implicit-globals": "error", "no-new-object": "error", "no-trailing-spaces": "error", - "no-undef": "error", - "no-unused-vars": ["error", { args: "none" }], + "no-undef": "off", + "no-unused-vars": "off", + "@typescript-eslint/no-unused-vars": ["error", { args: "none" }], + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-require-imports": "off", "no-useless-escape": "off", "no-with": "error", "object-curly-spacing": "off", "react/react-in-jsx-scope": "off", "react/button-has-type": "error", "react/display-name": "off", - "react/jsx-closing-bracket-location": "error", // autofixable - "react/jsx-curly-spacing": "error", // autofixable + "react/jsx-closing-bracket-location": "error", + "react/jsx-curly-spacing": "error", "react/jsx-first-prop-new-line": ["error", "multiline"], - "react/jsx-indent-props": ["error", 2], // autofixable + "react/jsx-indent-props": ["error", 2], "react/jsx-key": "off", "react/jsx-no-target-blank": "off", - "react/jsx-wrap-multilines": "error", // autofixable + "react/jsx-wrap-multilines": "error", "react/no-danger": "error", "react/no-find-dom-node": "off", "react/no-render-return-value": "off", "react/no-string-refs": "off", "react/no-unescaped-entities": "off", + "react/prop-types": "off", "react/self-closing-comp": "error", - semi: "off", // enforced by babel/semi + semi: "off", "space-before-blocks": "error", - strict: "error", + strict: "off", "jsx-a11y/no-onchange": 0 - } }; diff --git a/babel.config.json b/babel.config.json index fcde8385..1e588588 100644 --- a/babel.config.json +++ b/babel.config.json @@ -1,6 +1,7 @@ { "presets": [ ["@babel/preset-env", {"loose": true}], - ["@babel/preset-react", {"runtime": "automatic"}] + ["@babel/preset-react", {"runtime": "automatic"}], + "@babel/preset-typescript" ] } diff --git a/package.json b/package.json index e2b84902..487421d4 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,13 @@ "url": "git+ssh://git@github.com/code-dot-org/ml-playground.git" }, "jest": { + "moduleFileExtensions": [ + "ts", + "tsx", + "js", + "jsx", + "json" + ], "moduleNameMapper": { ".+\\.(bin|jpg|jpeg|png|mp3|ogg|wav|gif)$": "identity-obj-proxy", "^@public(.*)$": "/public/$1" @@ -23,7 +30,7 @@ "build": "webpack --mode production", "start": "yarn run dev", "dev": "webpack-dev-server --mode development --static public --host 0.0.0.0 --allowed-hosts all", - "lint": "eslint --ext .js,.jsx src", + "lint": "eslint --ext .ts,.tsx src", "test": "yarn run lint && jest", "test:unit": "jest ./test/unit/*.js", "preversion": "yarn install && yarn run test", @@ -35,6 +42,11 @@ "@babel/eslint-parser": "^7.28.6", "@babel/preset-env": "^7.29.2", "@babel/preset-react": "^7.28.5", + "@babel/preset-typescript": "^7.28.5", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@typescript-eslint/eslint-plugin": "^8.58.1", + "@typescript-eslint/parser": "^8.58.1", "babel-jest": "29", "babel-loader": "^10.1.1", "copy-webpack-plugin": "11", @@ -55,6 +67,8 @@ "react-redux": "9", "redux": "^4.0.5", "style-loader": "^4.0.0", + "ts-loader": "^9.5.7", + "typescript": "^6.0.2", "webpack": "5", "webpack-bundle-analyzer": "^3.6.0", "webpack-cli": "5", @@ -67,9 +81,9 @@ "public/*.json" ], "dependencies": { - "@fortawesome/fontawesome-svg-core": "^1.2.25", - "@fortawesome/free-solid-svg-icons": "^5.11.2", - "@fortawesome/react-fontawesome": "^0.1.7", + "@fortawesome/fontawesome-svg-core": "^7.2.0", + "@fortawesome/free-solid-svg-icons": "^7.2.0", + "@fortawesome/react-fontawesome": "^3.3.0", "chart.js": "^2.9.4", "react-chartjs-2": "^2.11.1", "reselect": "^4.0.0" diff --git a/src/App.js b/src/App.tsx similarity index 69% rename from src/App.js rename to src/App.tsx index 95ece78f..e2498e48 100644 --- a/src/App.js +++ b/src/App.tsx @@ -1,4 +1,4 @@ -import PropTypes from "prop-types"; +import React from "react"; import SelectDataset from "./UIComponents/SelectDataset"; import DataDisplay from "./UIComponents/DataDisplay"; import ColumnInspector from "./UIComponents/ColumnInspector"; @@ -14,17 +14,33 @@ import { connect } from "react-redux"; import { getPanelButtons, setCurrentPanel, - getTrainedModelDataToSave + getTrainedModelDataToSave, + RootState } from "./redux"; import { isSaveComplete, shouldDisplaySaveStatus } from "./helpers/navigationValidation"; +import { PrevNextButtons, ModelDataToSave, SaveResponseData } from "./types"; +import { Dispatch } from "redux"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faSpinner } from "@fortawesome/free-solid-svg-icons"; import I18n from "./i18n"; -function PanelButtons({ +interface PanelButtonsProps { + panelButtons: PrevNextButtons; + currentPanel: string; + setCurrentPanel: (panel: string) => void; + onContinue: () => void; + startSaveTrainedModel: (dataToSave: ModelDataToSave) => void; + dataToSave: ModelDataToSave; + saveStatus: string; + saveResponseData: SaveResponseData | undefined; + isSaveComplete: (saveStatus: string) => boolean; + shouldDisplaySaveStatus: (saveStatus: string) => boolean; +} + +const PanelButtons = ({ panelButtons, currentPanel, setCurrentPanel, @@ -35,26 +51,26 @@ function PanelButtons({ saveResponseData, isSaveComplete: isSaveCompleteProp, shouldDisplaySaveStatus: shouldDisplaySaveStatusProp -}) { +}: PanelButtonsProps) => { const onClickPrev = () => { - setCurrentPanel(panelButtons.prev.panel); + setCurrentPanel(panelButtons.prev!.panel); }; const onClickNext = () => { - if (["continue", "finish"].includes(panelButtons.next.panel)) { + if (["continue", "finish"].includes(panelButtons.next!.panel)) { onContinue(); } else if (currentPanel === "saveModel") { startSaveTrainedModel(dataToSave); } else { - setCurrentPanel(panelButtons.next.panel); + setCurrentPanel(panelButtons.next!.panel); } }; - const localizedSaveMessage = saveStatus => { - return I18n.t(`saveStatus_${saveStatus}`); + const localizedSaveMessage = (saveStatus: string): string => { + return I18n.t(`saveStatus_${saveStatus}`) ?? ""; }; - const saveResponseDataMessage = saveResponseData => { + const saveResponseDataMessage = (saveResponseData: SaveResponseData | undefined): string | undefined => { // The list of known error types from share_filtering.rb. const errorTypes = ["email", "address", "phone", "profanity"]; @@ -62,7 +78,7 @@ function PanelButtons({ return undefined; } - const index = errorTypes.indexOf(saveResponseData.type); + const index = errorTypes.indexOf(saveResponseData.type || ""); if (index !== -1) { return `(${index})`; } else { @@ -70,13 +86,13 @@ function PanelButtons({ } }; - let loadSaveStatus = isSaveCompleteProp(saveStatus) ? ( + const loadSaveStatus = isSaveCompleteProp(saveStatus) ? ( localizedSaveMessage(saveStatus) ) : ( ); - let loadSaveResponseData = isSaveCompleteProp(saveStatus) + const loadSaveResponseData = isSaveCompleteProp(saveStatus) ? saveResponseDataMessage(saveResponseData) : undefined; @@ -132,58 +148,61 @@ function PanelButtons({ )} ); -} - -PanelButtons.propTypes = { - panelButtons: PropTypes.object, - currentPanel: PropTypes.string, - setCurrentPanel: PropTypes.func, - onContinue: PropTypes.func, - startSaveTrainedModel: PropTypes.func, - dataToSave: PropTypes.object, - saveStatus: PropTypes.string, - saveResponseData: PropTypes.object, - isSaveComplete: PropTypes.func, - shouldDisplaySaveStatus: PropTypes.func }; -const BodyContainer = props => ( -
{props.children}
-); +interface BodyContainerProps { + children: React.ReactNode; +} -BodyContainer.propTypes = { - children: PropTypes.node +const BodyContainer = ({ children }: BodyContainerProps) => { + return
{children}
; }; -const ContainerLeft = props => ( -
- {props.children} -
-); +interface ContainerLeftProps { + children: React.ReactNode; +} -ContainerLeft.propTypes = { - children: PropTypes.node +const ContainerLeft = ({ children }: ContainerLeftProps) => { + return ( +
+ {children} +
+ ); }; -const ContainerRight = props => ( -
- {props.children} -
-); +interface ContainerRightProps { + children: React.ReactNode; +} -ContainerRight.propTypes = { - children: PropTypes.node +const ContainerRight = ({ children }: ContainerRightProps) => { + return ( +
+ {children} +
+ ); }; -const ContainerFullWidth = props => ( -
{props.children}
-); +interface ContainerFullWidthProps { + children: React.ReactNode; +} -ContainerFullWidth.propTypes = { - children: PropTypes.node +const ContainerFullWidth = ({ children }: ContainerFullWidthProps) => { + return
{children}
; }; -function App({ +interface AppProps { + panelButtons: PrevNextButtons; + currentPanel: string; + setCurrentPanel: (panel: string) => void; + onContinue: () => void; + resultsPhase: number | undefined; + startSaveTrainedModel: (dataToSave: ModelDataToSave) => void; + dataToSave: ModelDataToSave; + saveStatus: string; + saveResponseData: SaveResponseData | undefined; +} + +const App = ({ panelButtons, currentPanel, setCurrentPanel, @@ -193,7 +212,7 @@ function App({ dataToSave, saveStatus, saveResponseData -}) { +}: AppProps) => { return (
{currentPanel === "selectDataset" && ( @@ -278,24 +297,10 @@ function App({ />
); -} - -App.propTypes = { - panelButtons: PropTypes.object, - currentPanel: PropTypes.string, - setCurrentPanel: PropTypes.func, - onContinue: PropTypes.func, - resultsPhase: PropTypes.number, - startSaveTrainedModel: PropTypes.func, - dataToSave: PropTypes.object, - saveStatus: PropTypes.string, - saveResponseData: PropTypes.object, - isSaveComplete: PropTypes.func, - shouldDisplaySaveStatus: PropTypes.func }; export default connect( - state => ({ + (state: RootState) => ({ panelButtons: getPanelButtons(state), currentPanel: state.currentPanel, resultsPhase: state.resultsPhase, @@ -303,15 +308,9 @@ export default connect( saveStatus: state.saveStatus, saveResponseData: state.saveResponseData }), - dispatch => ({ - setCurrentPanel(panel) { + (dispatch: Dispatch) => ({ + setCurrentPanel(panel: string) { dispatch(setCurrentPanel(panel)); - }, - isSaveComplete(state) { - dispatch(isSaveComplete(state)); - }, - shouldDisplaySaveStatus(state) { - dispatch(shouldDisplaySaveStatus(state)); } }) )(App); diff --git a/src/UIComponents/AddFeatureButton.jsx b/src/UIComponents/AddFeatureButton.tsx similarity index 55% rename from src/UIComponents/AddFeatureButton.jsx rename to src/UIComponents/AddFeatureButton.tsx index cd0dc475..7cc25cf5 100644 --- a/src/UIComponents/AddFeatureButton.jsx +++ b/src/UIComponents/AddFeatureButton.tsx @@ -1,12 +1,16 @@ /* React component to handle selecting columns as features. */ -import PropTypes from "prop-types"; import { connect } from "react-redux"; +import { RootState, addSelectedFeature } from "../redux"; import { styles } from "../constants"; -import { addSelectedFeature } from "../redux"; import I18n from "../i18n"; -function AddFeatureButton({ column, addSelectedFeature }) { - const addFeature = (event, column) => { +interface AddFeatureButtonProps { + column?: string; + addSelectedFeature: (column: string) => void; +} + +const AddFeatureButton = ({ column, addSelectedFeature }: AddFeatureButtonProps) => { + const addFeature = (event: React.MouseEvent, column: string) => { addSelectedFeature(column); event.preventDefault(); }; @@ -15,23 +19,18 @@ function AddFeatureButton({ column, addSelectedFeature }) { ); -} - -AddFeatureButton.propTypes = { - column: PropTypes.string, - addSelectedFeature: PropTypes.func.isRequired }; export default connect( - state => ({}), + (state: RootState) => ({}), dispatch => ({ - addSelectedFeature(column) { + addSelectedFeature(column: string) { dispatch(addSelectedFeature(column)); } }) diff --git a/src/UIComponents/AnimationDescriptions.jsx b/src/UIComponents/AnimationDescriptions.tsx similarity index 74% rename from src/UIComponents/AnimationDescriptions.jsx rename to src/UIComponents/AnimationDescriptions.tsx index 18fc2244..cd4429eb 100644 --- a/src/UIComponents/AnimationDescriptions.jsx +++ b/src/UIComponents/AnimationDescriptions.tsx @@ -1,8 +1,11 @@ /* React component to handle animation descriptions for screen readers. */ -import PropTypes from "prop-types"; import I18n from "../i18n"; -function AnimationDescription({ description }) { +interface AnimationDescriptionProps { + description?: string; +} + +const AnimationDescription = ({ description }: AnimationDescriptionProps) => { return (
@@ -14,26 +17,22 @@ function AnimationDescription({ description }) {
); -} - -AnimationDescription.propTypes = { - description: PropTypes.string }; -function TrainingAnimationDescription() { +const TrainingAnimationDescription = () => { return ( ); -} +}; -function TestingAnimationDescription() { +const TestingAnimationDescription = () => { return ( ); -} +}; export { TrainingAnimationDescription, TestingAnimationDescription }; diff --git a/src/UIComponents/ColumnDataTypeDropdown.jsx b/src/UIComponents/ColumnDataTypeDropdown.tsx similarity index 54% rename from src/UIComponents/ColumnDataTypeDropdown.jsx rename to src/UIComponents/ColumnDataTypeDropdown.tsx index fe4eb5c6..b8b87644 100644 --- a/src/UIComponents/ColumnDataTypeDropdown.jsx +++ b/src/UIComponents/ColumnDataTypeDropdown.tsx @@ -1,12 +1,17 @@ /* React component to handle setting datatype for selected columns. */ -import PropTypes from "prop-types"; import { connect } from "react-redux"; -import { setColumnsByDataType } from "../redux"; -import { ColumnTypes } from "../constants.js"; +import { RootState, setColumnsByDataType } from "../redux"; +import { ColumnTypes } from "../constants"; import I18n from "../i18n"; -function ColumnDataTypeDropdown({ columnId, currentDataType, setColumnsByDataType }) { - const handleChangeDataType = (event, feature) => { +interface ColumnDataTypeDropdownProps { + columnId?: string; + currentDataType?: string; + setColumnsByDataType: (column: string, dataType: string) => void; +} + +const ColumnDataTypeDropdown = ({ columnId, currentDataType, setColumnsByDataType }: ColumnDataTypeDropdownProps) => { + const handleChangeDataType = (event: React.ChangeEvent, feature: string) => { event.preventDefault(); setColumnsByDataType(feature, event.target.value); }; @@ -15,7 +20,7 @@ function ColumnDataTypeDropdown({ columnId, currentDataType, setColumnsByDataTyp
); -} - -ColumnDataTypeDropdown.propTypes = { - columnId: PropTypes.string, - currentDataType: PropTypes.oneOf(Object.values(ColumnTypes)), - setColumnsByDataType: PropTypes.func.isRequired }; export default connect( - state => ({}), + (state: RootState) => ({}), dispatch => ({ - setColumnsByDataType(column, dataType) { + setColumnsByDataType(column: string, dataType: string) { dispatch(setColumnsByDataType(column, dataType)); } }) diff --git a/src/UIComponents/ColumnDetailsCategorical.jsx b/src/UIComponents/ColumnDetailsCategorical.tsx similarity index 85% rename from src/UIComponents/ColumnDetailsCategorical.jsx rename to src/UIComponents/ColumnDetailsCategorical.tsx index b3721477..0b9ae0b6 100644 --- a/src/UIComponents/ColumnDetailsCategorical.jsx +++ b/src/UIComponents/ColumnDetailsCategorical.tsx @@ -1,12 +1,17 @@ /* React component to handle showing details of categorical columns. */ import { connect } from "react-redux"; +import { RootState } from "../redux"; import { colors, styles } from "../constants"; import { Bar } from "react-chartjs-2"; -import { categeoricalColumnDetailsShape } from "./shapes"; import { getCategoricalColumnDetails } from "../selectors/currentColumnSelectors"; import I18n from "../i18n"; +import { CategoricalColumnDetails } from "../types"; + +interface ColumnDetailsCategoricalProps { + columnDetails: CategoricalColumnDetails; +} const chartOptions = { scales: { @@ -22,7 +27,7 @@ const chartOptions = { maintainAspectRatio: false }; -function ColumnDetailsCategorical({ columnDetails }) { +const ColumnDetailsCategorical = ({ columnDetails }: ColumnDetailsCategoricalProps) => { const { id, uniqueOptions, frequencies } = columnDetails; const labels = uniqueOptions && Object.values(uniqueOptions); const barData = { @@ -65,14 +70,11 @@ function ColumnDetailsCategorical({ columnDetails }) { ); -} - -ColumnDetailsCategorical.propTypes = { - columnDetails: categeoricalColumnDetailsShape }; export default connect( - state => ({ + (state: RootState) => ({ columnDetails: getCategoricalColumnDetails(state) - }) + }), + {} )(ColumnDetailsCategorical); diff --git a/src/UIComponents/ColumnDetailsNumerical.jsx b/src/UIComponents/ColumnDetailsNumerical.tsx similarity index 77% rename from src/UIComponents/ColumnDetailsNumerical.jsx rename to src/UIComponents/ColumnDetailsNumerical.tsx index 3762294d..49c94191 100644 --- a/src/UIComponents/ColumnDetailsNumerical.jsx +++ b/src/UIComponents/ColumnDetailsNumerical.tsx @@ -1,11 +1,16 @@ /* React component to handle showing details of numerical columns. */ import { connect } from "react-redux"; +import { RootState } from "../redux"; import { styles } from "../constants"; import { getNumericalColumnDetails } from "../selectors/currentColumnSelectors"; -import { numericalColumnDetailsShape } from "./shapes" import I18n from "../i18n"; +import { NumericalColumnDetails } from "../types"; -function ColumnDetailsNumerical({ columnDetails }) { +interface ColumnDetailsNumericalProps { + columnDetails: NumericalColumnDetails; +} + +const ColumnDetailsNumerical = ({ columnDetails }: ColumnDetailsNumericalProps) => { const { extrema, containsOnlyNumbers } = columnDetails; return ( @@ -25,14 +30,11 @@ function ColumnDetailsNumerical({ columnDetails }) { )} ); -} - -ColumnDetailsNumerical.propTypes = { - columnDetails: numericalColumnDetailsShape }; export default connect( - state => ({ + (state: RootState) => ({ columnDetails: getNumericalColumnDetails(state) - }) + }), + {} )(ColumnDetailsNumerical); diff --git a/src/UIComponents/ColumnInspector.jsx b/src/UIComponents/ColumnInspector.tsx similarity index 85% rename from src/UIComponents/ColumnInspector.jsx rename to src/UIComponents/ColumnInspector.tsx index f412c3b2..5dbcc444 100644 --- a/src/UIComponents/ColumnInspector.jsx +++ b/src/UIComponents/ColumnInspector.tsx @@ -2,10 +2,10 @@ React component to handle displaying details, including data visualizations, for selected columns. */ -import PropTypes from "prop-types"; import { connect } from "react-redux"; +import { RootState } from "../redux"; import { getCurrentColumnDetails } from "../selectors/currentColumnSelectors"; -import { styles, ColumnTypes } from "../constants.js"; +import { styles, ColumnTypes } from "../constants"; import ScatterPlot from "./ScatterPlot"; import CrossTab from "./CrossTab"; import ScrollableContent from "./ScrollableContent"; @@ -15,11 +15,17 @@ import ColumnDataTypeDropdown from "./ColumnDataTypeDropdown"; import AddFeatureButton from "./AddFeatureButton"; import SelectLabelButton from "./SelectLabelButton"; import UniqueOptionsWarning from "./UniqueOptionsWarning"; -import { currentColumnInspectorShape } from "./shapes"; import I18n from "../i18n"; -import { getLocalizedColumnName } from "../helpers/columnDetails.js"; +import { getLocalizedColumnName } from "../helpers/columnDetails"; +import { CurrentColumnInspector } from "../types"; -function ColumnInspector({ currentColumnDetails, currentPanel, datasetId }) { +interface ColumnInspectorProps { + currentColumnDetails: CurrentColumnInspector | undefined; + currentPanel: string; + datasetId: string | undefined; +} + +const ColumnInspector = ({ currentColumnDetails, currentPanel, datasetId }: ColumnInspectorProps) => { const selectingFeatures = currentPanel === "dataDisplayFeatures"; const selectingLabel = currentPanel === "dataDisplayLabel"; @@ -33,7 +39,7 @@ function ColumnInspector({ currentColumnDetails, currentPanel, datasetId }) { } const localizedDataType = I18n.t(`columnType_${currentColumnDetails.dataType}`) - const localizedColumnName = getLocalizedColumnName(datasetId, currentColumnDetails.id); + const localizedColumnName = getLocalizedColumnName(datasetId!, currentColumnDetails.id); return ( currentColumnDetails && ( @@ -83,18 +89,13 @@ function ColumnInspector({ currentColumnDetails, currentPanel, datasetId }) { ) ); -} - -ColumnInspector.propTypes = { - currentColumnDetails: currentColumnInspectorShape, - currentPanel: PropTypes.string, - datasetId: PropTypes.string }; export default connect( - state => ({ + (state: RootState) => ({ currentColumnDetails: getCurrentColumnDetails(state), currentPanel: state.currentPanel, datasetId: state.metadata && state.metadata.name - }) + }), + {} )(ColumnInspector); diff --git a/src/UIComponents/CrossTab.jsx b/src/UIComponents/CrossTab.tsx similarity index 87% rename from src/UIComponents/CrossTab.jsx rename to src/UIComponents/CrossTab.tsx index 4aac9e99..e2b67082 100644 --- a/src/UIComponents/CrossTab.jsx +++ b/src/UIComponents/CrossTab.tsx @@ -5,16 +5,21 @@ */ import { useCallback } from "react"; import { connect } from "react-redux"; +import { RootState } from "../redux"; import { getCrossTabData } from "../selectors/visualizationSelectors"; -import { styles } from "../constants.js"; +import { styles } from "../constants"; import ScrollableContent from "./ScrollableContent"; -import { crossTabDataShape } from "./shapes"; import I18n from "../i18n"; +import { CrossTabData } from "../types"; -function CrossTab({ crossTabData }) { - const getCellStyle = useCallback((percent) => { +interface CrossTabProps { + crossTabData: CrossTabData | null; +} + +const CrossTab = ({ crossTabData }: CrossTabProps) => { + const getCellStyle = useCallback((percent: number) => { return { - ...styles["crossTabCell" + Math.round(percent / 20)], + ...(styles as Record)["crossTabCell" + Math.round(percent / 20)], ...styles.crossTabTableCell }; }, []); @@ -108,10 +113,10 @@ function CrossTab({ crossTabData }) { - {result.labelPercents[uniqueLabelValue] || 0}% + {result.labelPercents?.[uniqueLabelValue] || 0}% ); } @@ -126,12 +131,8 @@ function CrossTab({ crossTabData }) { )} ); -} - -CrossTab.propTypes = { - crossTabData: crossTabDataShape }; -export default connect(state => ({ +export default connect((state: RootState) => ({ crossTabData: getCrossTabData(state) }))(CrossTab); diff --git a/src/UIComponents/DataCard.jsx b/src/UIComponents/DataCard.tsx similarity index 73% rename from src/UIComponents/DataCard.jsx rename to src/UIComponents/DataCard.tsx index eb002f59..a0b81a51 100644 --- a/src/UIComponents/DataCard.jsx +++ b/src/UIComponents/DataCard.tsx @@ -1,24 +1,32 @@ /* React component to show information about the currently-selected data set. */ -import PropTypes from "prop-types"; import { connect } from "react-redux"; -import { styles } from "../constants.js"; +import { RootState } from "../redux"; +import { styles } from "../constants"; import ScrollableContent from "./ScrollableContent"; -import { metadataShape, datasetDetailsShape } from "./shapes.js"; import I18n from "../i18n"; import { getDatasetDetails } from "../helpers/datasetDetails"; +import { Metadata, DatasetDetails } from "../types"; -function DataCard({ name, metadata, datasetDetails, dataLength, removedRowsCount }) { +interface DataCardProps { + name?: string; + metadata?: Metadata; + datasetDetails?: DatasetDetails; + dataLength?: number; + removedRowsCount?: number; +} + +const DataCard = ({ name, metadata, datasetDetails, dataLength, removedRowsCount }: DataCardProps) => { const card = metadata && metadata.card; const dataLengthLimit = 20000; - if (dataLength > dataLengthLimit) { + if (dataLength! > dataLengthLimit) { window.alert( I18n.t("dataCardWarningLargeDataset", {"rowCount": dataLengthLimit}) ); } const removedRowsMsg = - removedRowsCount > 0 + removedRowsCount! > 0 ? I18n.t("dataCardRemovedRows", {"rowCount": removedRowsCount}) : null; @@ -29,12 +37,12 @@ function DataCard({ name, metadata, datasetDetails, dataLength, removedRowsCount {card && (
-
{datasetDetails.description}
+
{datasetDetails!.description}
{I18n.t("dataCardSource")}
- {metadata.card.source} + {metadata!.card!.source}
@@ -44,35 +52,35 @@ function DataCard({ name, metadata, datasetDetails, dataLength, removedRowsCount {dataLength}
- {metadata.card.lastUpdated && ( + {metadata!.card!.lastUpdated && (
{I18n.t("dataCardLastUpdated")}
- {metadata.card.lastUpdated} + {metadata!.card!.lastUpdated}
)} - {metadata.card.context.potentialUses && ( + {metadata!.card!.context?.potentialUses && (
{I18n.t("dataCardPotentialUses")}
- {datasetDetails.potentialUses} + {datasetDetails!.potentialUses}
)} - {metadata.card.context.potentialMisuses && ( + {metadata!.card!.context?.potentialMisuses && (
{I18n.t("dataCardPotentialMisuses")}
- {datasetDetails.potentialMisuses} + {datasetDetails!.potentialMisuses}
)}
)} - {!card && dataLength > 0 && ( + {!card && dataLength! > 0 && (

@@ -93,17 +101,9 @@ function DataCard({ name, metadata, datasetDetails, dataLength, removedRowsCount
) ); -} - -DataCard.propTypes = { - name: PropTypes.string, - metadata: metadataShape, - datasetDetails: datasetDetailsShape, - dataLength: PropTypes.number, - removedRowsCount: PropTypes.number }; -export default connect(state => ({ +export default connect((state: RootState) => ({ name: state.name, metadata: state.metadata, datasetDetails: getDatasetDetails(state), diff --git a/src/UIComponents/DataDisplay.jsx b/src/UIComponents/DataDisplay.tsx similarity index 75% rename from src/UIComponents/DataDisplay.jsx rename to src/UIComponents/DataDisplay.tsx index 3bf97a8a..2078116e 100644 --- a/src/UIComponents/DataDisplay.jsx +++ b/src/UIComponents/DataDisplay.tsx @@ -1,17 +1,22 @@ /* React component to handle displaying imported data. */ -import PropTypes from "prop-types"; import { connect } from "react-redux"; +import { RootState } from "../redux"; import Statement from "./Statement"; import DataTable from "./DataTable"; import { styles } from "../constants"; import I18n from "../i18n"; +import { DataRow } from "../types"; -function DataDisplay({ data }) { - if (data.length === 0) { +interface DataDisplayProps { + data?: DataRow[]; +} + +const DataDisplay = ({ data }: DataDisplayProps) => { + if (data!.length === 0) { return null; } - const rowCount = data.length; + const rowCount = data!.length; const rowLimit = 100; const rowCountMessage = (rowCount <= rowLimit) ? I18n.t("dataDisplayRowCount", {"rowCount": rowCount}) : @@ -27,14 +32,10 @@ function DataDisplay({ data }) {
); -} - -DataDisplay.propTypes = { - data: PropTypes.array }; export default connect( - state => ({ + (state: RootState) => ({ data: state.data }) )(DataDisplay); diff --git a/src/UIComponents/DataTable.jsx b/src/UIComponents/DataTable.tsx similarity index 71% rename from src/UIComponents/DataTable.jsx rename to src/UIComponents/DataTable.tsx index 1d3a357c..595b77c5 100644 --- a/src/UIComponents/DataTable.jsx +++ b/src/UIComponents/DataTable.tsx @@ -1,11 +1,30 @@ /* React component to handle displaying imported data. */ -import PropTypes from "prop-types"; import { connect } from "react-redux"; -import { getTableData, setCurrentColumn, setHighlightColumn } from "../redux"; +import { getTableData, setCurrentColumn, setHighlightColumn, RootState } from "../redux"; +import { Dispatch } from "redux"; import { styles } from "../constants"; -import { getLocalizedColumnName } from "../helpers/columnDetails.js"; +import { getLocalizedColumnName } from "../helpers/columnDetails"; +import { DataRow } from "../types"; + +interface DataTableProps { + currentPanel: string; + data: DataRow[] | null; + datasetId: string | undefined; + labelColumn: string | undefined; + selectedFeatures: string[]; + setCurrentColumn: (column: string | undefined) => void; + setHighlightColumn: (column: string | undefined) => void; + currentColumn: string | undefined; + highlightColumn: string | undefined; + reducedColumns?: boolean; + singleRow?: number | undefined; + startingRow?: number | undefined; + noLabel?: boolean; + hideLabel?: boolean; + useResultsData?: boolean; +} -function DataTable({ +const DataTable = ({ currentPanel, data, datasetId, @@ -20,8 +39,8 @@ function DataTable({ startingRow, noLabel, hideLabel -}) { - const getColumnHeaderStyle = key => { +}: DataTableProps) => { + const getColumnHeaderStyle = (key: string) => { let style; if (key === labelColumn) { @@ -36,7 +55,7 @@ function DataTable({ return { ...styles.dataDisplayHeader, ...pointerStyle, ...style }; }; - const getColumnCellStyle = key => { + const getColumnCellStyle = (key: string) => { let style; if (hideLabel && labelColumn === key) { @@ -60,7 +79,7 @@ function DataTable({ const getColumns = () => { if (reducedColumns) { - return Object.keys(data[0]) + return Object.keys(data![0]) .filter(key => { return ( (!noLabel && labelColumn === key) || @@ -72,34 +91,34 @@ function DataTable({ }); } - return Object.keys(data[0]); + return Object.keys(data![0]); }; const getRows = () => { if (singleRow !== undefined) { return [ - data[ - Math.min(singleRow, data.length - 1) + data![ + Math.min(singleRow, data!.length - 1) ] ]; } else { - return data.slice(0, 100); + return data!.slice(0, 100); } }; - const handleSetCurrentColumn = columnId => { + const handleSetCurrentColumn = (columnId: string) => { if (!reducedColumns) { setCurrentColumnProp(columnId); } }; - const handleSetHighlightColumn = columnId => { + const handleSetHighlightColumn = (columnId: string | undefined) => { if (!reducedColumns) { setHighlightColumnProp(columnId); } }; - if (data.length === 0) { + if (!data || data.length === 0) { return null; } @@ -117,7 +136,7 @@ function DataTable({ onMouseEnter={() => handleSetHighlightColumn(columnId)} onMouseLeave={() => handleSetHighlightColumn(undefined)} > - {getLocalizedColumnName(datasetId, columnId)} + {getLocalizedColumnName(datasetId!, columnId)} ); })} @@ -153,28 +172,11 @@ function DataTable({ ); -} - -DataTable.propTypes = { - currentPanel: PropTypes.string, - data: PropTypes.array, - datasetId: PropTypes.string, - labelColumn: PropTypes.string, - selectedFeatures: PropTypes.array, - setCurrentColumn: PropTypes.func, - setHighlightColumn: PropTypes.func, - currentColumn: PropTypes.string, - highlightColumn: PropTypes.string, - reducedColumns: PropTypes.bool, - singleRow: PropTypes.number, - startingRow: PropTypes.number, - noLabel: PropTypes.bool, - hideLabel: PropTypes.bool }; export default connect( - (state, props) => ({ - data: getTableData(state, props.useResultsData), + (state: RootState, props: { useResultsData?: boolean }) => ({ + data: getTableData(state, !!props.useResultsData), datasetId: state.metadata && state.metadata.name, labelColumn: state.labelColumn, selectedFeatures: state.selectedFeatures, @@ -182,12 +184,12 @@ export default connect( highlightColumn: state.highlightColumn, currentPanel: state.currentPanel }), - dispatch => ({ - setCurrentColumn(column) { - dispatch(setCurrentColumn(column)); + (dispatch: Dispatch) => ({ + setCurrentColumn(column: string | undefined) { + dispatch(setCurrentColumn(column as string)); }, - setHighlightColumn(column) { - dispatch(setHighlightColumn(column)); + setHighlightColumn(column: string | undefined) { + dispatch(setHighlightColumn(column as string)); } }) )(DataTable); diff --git a/src/UIComponents/GenerateResults.jsx b/src/UIComponents/GenerateResults.tsx similarity index 86% rename from src/UIComponents/GenerateResults.jsx rename to src/UIComponents/GenerateResults.tsx index 03fcf9e2..611f9065 100644 --- a/src/UIComponents/GenerateResults.jsx +++ b/src/UIComponents/GenerateResults.tsx @@ -1,8 +1,7 @@ /* React component to handle training. */ -import PropTypes from "prop-types"; import { useState, useEffect, useRef, useCallback } from "react"; import { connect } from "react-redux"; -import { getTableData, readyToTrain } from "../redux"; +import { getTableData, readyToTrain, RootState } from "../redux"; import { styles, getFadeOpacity } from "../constants"; import aiBotHead from "@public/images/ai-bot/ai-bot-head.png"; import aiBotBody from "@public/images/ai-bot/ai-bot-body.png"; @@ -11,29 +10,48 @@ import background from "@public/images/results-background-light.jpg"; import DataTable from "./DataTable"; import { TestingAnimationDescription } from "./AnimationDescriptions"; import I18n from "../i18n"; +import { DataRow } from "../types"; const framesPerCycle = 80; const maxNumItems = 7; -function GenerateResults({ data, readyToTrain, labelColumn, selectedFeatures, instructionsOverlayActive }) { +interface GenerateResultsProps { + data: DataRow[] | null; + readyToTrain: boolean; + labelColumn: string | undefined; + selectedFeatures: string[]; + instructionsOverlayActive: boolean; +} + +const GenerateResults = ({ data, readyToTrain, labelColumn, selectedFeatures, instructionsOverlayActive }: GenerateResultsProps) => { const [frame, setFrame] = useState(0); const [, setFinished] = useState(false); const frameRef = useRef(0); const instructionsOverlayActiveRef = useRef(instructionsOverlayActive); + const dataRef = useRef(data); - // Keep ref in sync with prop + // Keep refs in sync with props useEffect(() => { instructionsOverlayActiveRef.current = instructionsOverlayActive; }, [instructionsOverlayActive]); + useEffect(() => { + dataRef.current = data; + }, [data]); + const getNumItems = useCallback(() => { - return Math.min(maxNumItems, data.length); - }, [data.length]); + return Math.min(maxNumItems, data!.length); + }, [data]); useEffect(() => { const animationTimer = setInterval(() => { + const currentData = dataRef.current; + if (!currentData) { + return; + } + const currentFrame = frameRef.current; - const numItems = Math.min(maxNumItems, data.length); + const numItems = Math.min(maxNumItems, currentData.length); const animationStep = Math.floor(currentFrame / framesPerCycle); if (animationStep >= numItems) { @@ -49,10 +67,10 @@ function GenerateResults({ data, readyToTrain, labelColumn, selectedFeatures, in return () => { clearInterval(animationTimer); }; - }, [data.length]); + }, []); // eslint-disable-line react-hooks/exhaustive-deps const getShowItemsFadingOut = () => { - return data.length > maxNumItems; + return data!.length > maxNumItems; }; const getAnimationSubstep = () => { @@ -190,17 +208,9 @@ function GenerateResults({ data, readyToTrain, labelColumn, selectedFeatures, in ); -} - -GenerateResults.propTypes = { - data: PropTypes.array, - readyToTrain: PropTypes.bool, - labelColumn: PropTypes.string, - selectedFeatures: PropTypes.array, - instructionsOverlayActive: PropTypes.bool }; -export default connect(state => ({ +export default connect((state: RootState) => ({ data: getTableData(state, true), readyToTrain: readyToTrain(state), labelColumn: state.labelColumn, diff --git a/src/UIComponents/ModelCard.jsx b/src/UIComponents/ModelCard.tsx similarity index 84% rename from src/UIComponents/ModelCard.jsx rename to src/UIComponents/ModelCard.tsx index d0c8a156..220ad5b9 100644 --- a/src/UIComponents/ModelCard.jsx +++ b/src/UIComponents/ModelCard.tsx @@ -1,21 +1,29 @@ /* React component to handle displaying the model card. */ -import PropTypes from "prop-types"; import { connect } from "react-redux"; import { styles } from "../constants"; -import { getLabelToSave, getFeaturesToSave } from "../redux"; +import { getLabelToSave, getFeaturesToSave, RootState } from "../redux"; import { getPercentCorrect } from "../helpers/accuracy"; import { getDatasetDetails } from "../helpers/datasetDetails"; import Statement from "./Statement"; import aiBotBorder from "@public/images/ai-bot/ai-bot-border.png"; -import { datasetDetailsShape, trainedModelDetailsShape, modelCardColumnShape } from "./shapes"; import I18n from "../i18n"; -import { getLocalizedColumnName } from "../helpers/columnDetails.js"; +import { getLocalizedColumnName } from "../helpers/columnDetails"; +import { ModelCardColumn, TrainedModelDetailsSave, DatasetDetails } from "../types"; -function ModelCard({ trainedModelDetails, selectedFeatures, percentCorrect, label, features, datasetDetails }) { +interface ModelCardProps { + trainedModelDetails: TrainedModelDetailsSave; + selectedFeatures: string[]; + percentCorrect: string; + label: ModelCardColumn; + features: ModelCardColumn[]; + datasetDetails: DatasetDetails; +} + +const ModelCard = ({ trainedModelDetails, selectedFeatures, percentCorrect, label, features, datasetDetails }: ModelCardProps) => { console.log("trainedModelDetails", trainedModelDetails) - const localizedLabel = getLocalizedColumnName(datasetDetails.name, label.id); + const localizedLabel = getLocalizedColumnName(datasetDetails.name!, label!.id!); const localizedFeatures = - selectedFeatures.map(feature => getLocalizedColumnName(datasetDetails.name, feature)); + selectedFeatures!.map(feature => getLocalizedColumnName(datasetDetails.name!, feature)); const predictionStatement = I18n.t("predictionStatement", {"output": localizedLabel, "inputs": localizedFeatures.join(", ")}) return ( @@ -90,21 +98,21 @@ function ModelCard({ trainedModelDetails, selectedFeatures, percentCorrect, labe
{I18n.t("modelCardLabel")}

{localizedLabel}

- {label.description &&

{label.description}

} - {!label.values && ( + {label!.description &&

{label!.description}

} + {!label!.values && (

{I18n.t("modelCardPossibleValues")}
- {I18n.t("modelCardPossibleValuesMinimum")} {label.min} + {I18n.t("modelCardPossibleValuesMinimum")} {label!.min}
- {I18n.t("modelCardPossibleValuesMaximum")} {label.max} + {I18n.t("modelCardPossibleValuesMaximum")} {label!.max}

)} - {label.values && ( + {label!.values && (

{I18n.t("modelCardPossibleValues")}
- {label.values.join(" ")} + {label!.values.join(" ")}

)}
@@ -112,8 +120,8 @@ function ModelCard({ trainedModelDetails, selectedFeatures, percentCorrect, labe
{I18n.t("modelCardFeatures")}
- {features.length > 0 && - features.map((feature, index) => { + {features!.length > 0 && + features!.map((feature, index) => { return (

{feature.id}

@@ -142,18 +150,9 @@ function ModelCard({ trainedModelDetails, selectedFeatures, percentCorrect, labe
); -} - -ModelCard.propTypes = { - trainedModelDetails: trainedModelDetailsShape, - selectedFeatures: PropTypes.arrayOf(modelCardColumnShape), - percentCorrect: PropTypes.string, - label: modelCardColumnShape, - features: PropTypes.arrayOf(PropTypes.string), - datasetDetails: datasetDetailsShape }; -export default connect(state => ({ +export default connect((state: RootState) => ({ trainedModelDetails: state.trainedModelDetails, selectedFeatures: state.selectedFeatures, percentCorrect: getPercentCorrect(state), diff --git a/src/UIComponents/Predict.jsx b/src/UIComponents/Predict.tsx similarity index 75% rename from src/UIComponents/Predict.jsx rename to src/UIComponents/Predict.tsx index 3d75bf9a..fec59438 100644 --- a/src/UIComponents/Predict.jsx +++ b/src/UIComponents/Predict.tsx @@ -1,9 +1,10 @@ /* React component to handle predicting and displaying predictions. */ -import PropTypes from "prop-types"; +import React from "react"; import { connect } from "react-redux"; -import { store } from "../index.js"; +import { store } from "../index"; import train from "../train"; -import { setTestData, getPredictAvailable } from "../redux"; +import { setTestData, getPredictAvailable, RootState } from "../redux"; +import { Dispatch } from "redux"; import { getConvertedPredictedLabel } from "../helpers/valueConversion"; import { getSelectedCategoricalFeatures, @@ -15,9 +16,22 @@ import { styles } from "../constants"; import aiBotBorder from "@public/images/ai-bot/ai-bot-border.png"; import ScrollableContent from "./ScrollableContent"; import I18n from "../i18n"; -import { getLocalizedColumnName } from "../helpers/columnDetails.js"; +import { getLocalizedColumnName } from "../helpers/columnDetails"; -function Predict({ +interface PredictProps { + labelColumn: string | undefined; + selectedCategoricalFeatures: string[]; + selectedNumericalFeatures: string[]; + uniqueOptionsByColumn: Record; + testData: Record; + setTestData: (feature: string, value: string | number) => void; + predictedLabel: string | number; + getPredictAvailable: boolean; + extremaByColumn: Record; + datasetId: string | undefined; +} + +const Predict = ({ labelColumn, selectedCategoricalFeatures, selectedNumericalFeatures, @@ -28,8 +42,8 @@ function Predict({ getPredictAvailable: predictAvailable, extremaByColumn, datasetId -}) { - const handleChange = (event, feature) => { +}: PredictProps) => { + const handleChange = (event: React.ChangeEvent, feature: string) => { setTestData(feature, event.target.value); }; @@ -43,13 +57,13 @@ function Predict({
{selectedNumericalFeatures.map((feature, index) => { - let min = extremaByColumn[feature].min.toFixed(2); - let max = extremaByColumn[feature].max.toFixed(2); + const min = extremaByColumn[feature].min.toFixed(2); + const max = extremaByColumn[feature].max.toFixed(2); return (