diff --git a/frontend/packages/labs/ailab/src/App.tsx b/frontend/packages/labs/ailab/src/App.tsx index 60002b92ae9c2..48a462544b306 100644 --- a/frontend/packages/labs/ailab/src/App.tsx +++ b/frontend/packages/labs/ailab/src/App.tsx @@ -1,22 +1,16 @@ import {faSpinner} from '@fortawesome/free-solid-svg-icons'; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import type React from 'react'; -import {connect} from 'react-redux'; -import type {Dispatch} from 'redux'; import {styles} from './constants'; import { isSaveComplete, shouldDisplaySaveStatus, } from './helpers/navigationValidation'; +import {shallowEqual, useAppDispatch, useAppSelector} from './hooks'; import I18n from './i18n'; -import type {RootState} from './redux'; -import { - getPanelButtons, - setCurrentPanel, - getTrainedModelDataToSave, -} from './redux'; -import type {PrevNextButtons, ModelDataToSave, SaveResponseData} from './types'; +import {getPanelButtons, setCurrentPanel} from './redux'; +import type {PrevNextButtons, SaveResponseData} from './types'; import ColumnInspector from './UIComponents/ColumnInspector'; import DataCard from './UIComponents/DataCard'; import DataDisplay from './UIComponents/DataDisplay'; @@ -33,8 +27,7 @@ interface PanelButtonsProps { currentPanel: string; setCurrentPanel: (panel: string) => void; onContinue: () => void; - startSaveTrainedModel: (dataToSave: ModelDataToSave) => void; - dataToSave: ModelDataToSave; + startSaveTrainedModel: () => void; saveStatus: string; saveResponseData: SaveResponseData | undefined; isSaveComplete: (saveStatus: string) => boolean; @@ -47,7 +40,6 @@ const PanelButtons = ({ setCurrentPanel, onContinue, startSaveTrainedModel, - dataToSave, saveStatus, saveResponseData, isSaveComplete: isSaveCompleteProp, @@ -64,7 +56,7 @@ const PanelButtons = ({ if (['continue', 'finish'].includes(panelButtons.next.panel)) { onContinue(); } else if (currentPanel === 'saveModel') { - startSaveTrainedModel(dataToSave); + startSaveTrainedModel(); } else { setCurrentPanel(panelButtons.next.panel); } @@ -198,28 +190,24 @@ const ContainerFullWidth = ({children}: ContainerFullWidthProps) => { }; 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; + startSaveTrainedModel: () => void; } -const App = ({ - panelButtons, - currentPanel, - setCurrentPanel, - onContinue, - resultsPhase, - startSaveTrainedModel, - dataToSave, - saveStatus, - saveResponseData, -}: AppProps) => { +// getPanelButtons builds a fresh {prev, next} each call (and reads I18n for +// button text), so compare its two sub-objects to avoid rerendering on a new +// reference with unchanged contents. +const panelButtonsEqual = (a: PrevNextButtons, b: PrevNextButtons) => + shallowEqual(a.prev, b.prev) && shallowEqual(a.next, b.next); + +const App = ({onContinue, startSaveTrainedModel}: AppProps) => { + const dispatch = useAppDispatch(); + const panelButtons = useAppSelector(getPanelButtons, panelButtonsEqual); + const currentPanel = useAppSelector(state => state.currentPanel); + const resultsPhase = useAppSelector(state => state.resultsPhase); + const saveStatus = useAppSelector(state => state.saveStatus); + const saveResponseData = useAppSelector(state => state.saveResponseData); + return (
{currentPanel === 'selectDataset' && ( @@ -293,10 +281,9 @@ const App = ({ dispatch(setCurrentPanel(panel))} onContinue={onContinue} startSaveTrainedModel={startSaveTrainedModel} - dataToSave={dataToSave} saveStatus={saveStatus} saveResponseData={saveResponseData} isSaveComplete={isSaveComplete} @@ -306,18 +293,4 @@ const App = ({ ); }; -export default connect( - (state: RootState) => ({ - panelButtons: getPanelButtons(state), - currentPanel: state.currentPanel, - resultsPhase: state.resultsPhase, - dataToSave: getTrainedModelDataToSave(state), - saveStatus: state.saveStatus, - saveResponseData: state.saveResponseData, - }), - (dispatch: Dispatch) => ({ - setCurrentPanel(panel: string) { - dispatch(setCurrentPanel(panel)); - }, - }), -)(App); +export default App; diff --git a/frontend/packages/labs/ailab/src/UIComponents/AddFeatureButton.tsx b/frontend/packages/labs/ailab/src/UIComponents/AddFeatureButton.tsx index f5f662a17d561..94a55283d56e0 100644 --- a/frontend/packages/labs/ailab/src/UIComponents/AddFeatureButton.tsx +++ b/frontend/packages/labs/ailab/src/UIComponents/AddFeatureButton.tsx @@ -1,21 +1,18 @@ /* React component to handle selecting columns as features. */ -import {connect} from 'react-redux'; - import {styles} from '../constants'; +import {useAppDispatch} from '../hooks'; import I18n from '../i18n'; import {addSelectedFeature} from '../redux'; interface AddFeatureButtonProps { column?: string; - addSelectedFeature: (column: string) => void; } -const AddFeatureButton = ({ - column, - addSelectedFeature, -}: AddFeatureButtonProps) => { +const AddFeatureButton = ({column}: AddFeatureButtonProps) => { + const dispatch = useAppDispatch(); + const addFeature = (event: React.MouseEvent, column: string) => { - addSelectedFeature(column); + dispatch(addSelectedFeature(column)); event.preventDefault(); }; @@ -31,8 +28,4 @@ const AddFeatureButton = ({ ); }; -export default connect(null, dispatch => ({ - addSelectedFeature(column: string) { - dispatch(addSelectedFeature(column)); - }, -}))(AddFeatureButton); +export default AddFeatureButton; diff --git a/frontend/packages/labs/ailab/src/UIComponents/ColumnDataTypeDropdown.tsx b/frontend/packages/labs/ailab/src/UIComponents/ColumnDataTypeDropdown.tsx index 0bd393414eab2..51f28d224d972 100644 --- a/frontend/packages/labs/ailab/src/UIComponents/ColumnDataTypeDropdown.tsx +++ b/frontend/packages/labs/ailab/src/UIComponents/ColumnDataTypeDropdown.tsx @@ -1,27 +1,26 @@ /* React component to handle setting datatype for selected columns. */ -import {connect} from 'react-redux'; - import {ColumnTypes} from '../constants'; +import {useAppDispatch} from '../hooks'; import I18n from '../i18n'; import {setColumnsByDataType} from '../redux'; interface ColumnDataTypeDropdownProps { columnId?: string; currentDataType?: string; - setColumnsByDataType: (column: string, dataType: string) => void; } const ColumnDataTypeDropdown = ({ columnId, currentDataType, - setColumnsByDataType, }: ColumnDataTypeDropdownProps) => { + const dispatch = useAppDispatch(); + const handleChangeDataType = ( event: React.ChangeEvent, feature: string, ) => { event.preventDefault(); - setColumnsByDataType(feature, event.target.value); + dispatch(setColumnsByDataType(feature, event.target.value)); }; return ( @@ -42,8 +41,4 @@ const ColumnDataTypeDropdown = ({ ); }; -export default connect(null, dispatch => ({ - setColumnsByDataType(column: string, dataType: string) { - dispatch(setColumnsByDataType(column, dataType)); - }, -}))(ColumnDataTypeDropdown); +export default ColumnDataTypeDropdown; diff --git a/frontend/packages/labs/ailab/src/UIComponents/ColumnDetailsCategorical.tsx b/frontend/packages/labs/ailab/src/UIComponents/ColumnDetailsCategorical.tsx index 58ddfd8412f5c..f60d74515536e 100644 --- a/frontend/packages/labs/ailab/src/UIComponents/ColumnDetailsCategorical.tsx +++ b/frontend/packages/labs/ailab/src/UIComponents/ColumnDetailsCategorical.tsx @@ -1,18 +1,11 @@ /* React component to handle showing details of categorical columns. */ import {Bar} from 'react-chartjs-2'; -import {connect} from 'react-redux'; import {colors, styles} from '../constants'; import {getLocalizedValue} from '../helpers/valueDetails'; +import {useAppSelector} from '../hooks'; import I18n from '../i18n'; -import type {RootState} from '../redux'; import {getCategoricalColumnDetails} from '../selectors/currentColumnSelectors'; -import type {CategoricalColumnDetails} from '../types'; - -interface ColumnDetailsCategoricalProps { - columnDetails: CategoricalColumnDetails; - datasetId: string; -} const chartOptions = { scales: { @@ -28,10 +21,10 @@ const chartOptions = { maintainAspectRatio: false, }; -const ColumnDetailsCategorical = ({ - columnDetails, - datasetId, -}: ColumnDetailsCategoricalProps) => { +const ColumnDetailsCategorical = () => { + const columnDetails = useAppSelector(getCategoricalColumnDetails); + const datasetId = useAppSelector(state => state.metadata?.name || 'unknown'); + const {id, uniqueOptions, frequencies} = columnDetails; const labels = uniqueOptions && Object.values(uniqueOptions); const localizedLabels = labels.map(option => @@ -74,10 +67,4 @@ const ColumnDetailsCategorical = ({ ); }; -export default connect( - (state: RootState) => ({ - columnDetails: getCategoricalColumnDetails(state), - datasetId: state.metadata?.name || 'unknown', - }), - {}, -)(ColumnDetailsCategorical); +export default ColumnDetailsCategorical; diff --git a/frontend/packages/labs/ailab/src/UIComponents/ColumnDetailsNumerical.tsx b/frontend/packages/labs/ailab/src/UIComponents/ColumnDetailsNumerical.tsx index 48d37547a8233..677cf2b48ab95 100644 --- a/frontend/packages/labs/ailab/src/UIComponents/ColumnDetailsNumerical.tsx +++ b/frontend/packages/labs/ailab/src/UIComponents/ColumnDetailsNumerical.tsx @@ -1,19 +1,12 @@ /* React component to handle showing details of numerical columns. */ -import {connect} from 'react-redux'; - import {styles} from '../constants'; +import {useAppSelector} from '../hooks'; import I18n from '../i18n'; -import type {RootState} from '../redux'; import {getNumericalColumnDetails} from '../selectors/currentColumnSelectors'; -import type {NumericalColumnDetails} from '../types'; -interface ColumnDetailsNumericalProps { - columnDetails: NumericalColumnDetails; -} +const ColumnDetailsNumerical = () => { + const columnDetails = useAppSelector(getNumericalColumnDetails); -const ColumnDetailsNumerical = ({ - columnDetails, -}: ColumnDetailsNumericalProps) => { const {extrema, containsOnlyNumbers} = columnDetails; return ( @@ -35,9 +28,4 @@ const ColumnDetailsNumerical = ({ ); }; -export default connect( - (state: RootState) => ({ - columnDetails: getNumericalColumnDetails(state), - }), - {}, -)(ColumnDetailsNumerical); +export default ColumnDetailsNumerical; diff --git a/frontend/packages/labs/ailab/src/UIComponents/ColumnInspector.tsx b/frontend/packages/labs/ailab/src/UIComponents/ColumnInspector.tsx index 9df6b8d2c8b77..96b9fd02f207f 100644 --- a/frontend/packages/labs/ailab/src/UIComponents/ColumnInspector.tsx +++ b/frontend/packages/labs/ailab/src/UIComponents/ColumnInspector.tsx @@ -2,14 +2,11 @@ React component to handle displaying details, including data visualizations, for selected columns. */ -import {connect} from 'react-redux'; - import {styles, ColumnTypes} from '../constants'; import {getLocalizedColumnName} from '../helpers/columnDetails'; +import {useAppSelector} from '../hooks'; import I18n from '../i18n'; -import type {RootState} from '../redux'; import {getCurrentColumnDetails} from '../selectors/currentColumnSelectors'; -import type {CurrentColumnInspector} from '../types'; import AddFeatureButton from './AddFeatureButton'; import ColumnDataTypeDropdown from './ColumnDataTypeDropdown'; @@ -21,17 +18,12 @@ import ScrollableContent from './ScrollableContent'; import SelectLabelButton from './SelectLabelButton'; import UniqueOptionsWarning from './UniqueOptionsWarning'; -interface ColumnInspectorProps { - currentColumnDetails: CurrentColumnInspector | undefined; - currentPanel: string; - datasetId: string | undefined; -} - -const ColumnInspector = ({ - currentColumnDetails, - currentPanel, - datasetId, -}: ColumnInspectorProps) => { +const ColumnInspector = () => { + const currentColumnDetails = useAppSelector(getCurrentColumnDetails); + const currentPanel = useAppSelector(state => state.currentPanel); + const datasetId = useAppSelector( + state => state.metadata && state.metadata.name, + ); const selectingFeatures = currentPanel === 'dataDisplayFeatures'; const selectingLabel = currentPanel === 'dataDisplayLabel'; @@ -106,11 +98,4 @@ const ColumnInspector = ({ ); }; -export default connect( - (state: RootState) => ({ - currentColumnDetails: getCurrentColumnDetails(state), - currentPanel: state.currentPanel, - datasetId: state.metadata && state.metadata.name, - }), - {}, -)(ColumnInspector); +export default ColumnInspector; diff --git a/frontend/packages/labs/ailab/src/UIComponents/CrossTab.tsx b/frontend/packages/labs/ailab/src/UIComponents/CrossTab.tsx index 68f4bd61db0c4..f930f1ee33fa4 100644 --- a/frontend/packages/labs/ailab/src/UIComponents/CrossTab.tsx +++ b/frontend/packages/labs/ailab/src/UIComponents/CrossTab.tsx @@ -4,23 +4,18 @@ label values, with a heatmap style applied. */ import {useCallback} from 'react'; -import {connect} from 'react-redux'; import {styles} from '../constants'; import {getLocalizedValue} from '../helpers/valueDetails'; +import {useAppSelector} from '../hooks'; import I18n from '../i18n'; -import type {RootState} from '../redux'; import {getCrossTabData} from '../selectors/visualizationSelectors'; -import type {CrossTabData} from '../types'; import ScrollableContent from './ScrollableContent'; -interface CrossTabProps { - crossTabData: CrossTabData | null; - datasetId: string; -} - -const CrossTab = ({crossTabData, datasetId}: CrossTabProps) => { +const CrossTab = () => { + const crossTabData = useAppSelector(getCrossTabData); + const datasetId = useAppSelector(state => state.metadata?.name || 'unknown'); const getCellStyle = useCallback((percent: number) => { return { ...(styles as Record)[ @@ -139,7 +134,4 @@ const CrossTab = ({crossTabData, datasetId}: CrossTabProps) => { ); }; -export default connect((state: RootState) => ({ - crossTabData: getCrossTabData(state), - datasetId: state.metadata?.name || 'unknown', -}))(CrossTab); +export default CrossTab; diff --git a/frontend/packages/labs/ailab/src/UIComponents/DataCard.tsx b/frontend/packages/labs/ailab/src/UIComponents/DataCard.tsx index a6cdd089cfaa8..958c3479ea5ac 100644 --- a/frontend/packages/labs/ailab/src/UIComponents/DataCard.tsx +++ b/frontend/packages/labs/ailab/src/UIComponents/DataCard.tsx @@ -1,40 +1,29 @@ /* React component to show information about the currently-selected data set. */ -import {connect} from 'react-redux'; - import {styles} from '../constants'; import {getDatasetDetails} from '../helpers/datasetDetails'; +import {shallowEqual, useAppSelector} from '../hooks'; import I18n from '../i18n'; -import type {RootState} from '../redux'; -import type {Metadata, DatasetDetails} from '../types'; import ScrollableContent from './ScrollableContent'; -interface DataCardProps { - name?: string; - metadata: Metadata; - datasetDetails: DatasetDetails; - dataLength?: number; - removedRowsCount?: number; -} +const DataCard = () => { + const name = useAppSelector(state => state.name); + const metadata = useAppSelector(state => state.metadata); + const datasetDetails = useAppSelector(getDatasetDetails, shallowEqual); + const dataLength = useAppSelector(state => state.data.length); + const removedRowsCount = useAppSelector(state => state.removedRowsCount); -const DataCard = ({ - name, - metadata, - datasetDetails, - dataLength, - removedRowsCount, -}: DataCardProps) => { const card = 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; @@ -94,7 +83,7 @@ const DataCard = ({ )}
)} - {!card && dataLength! > 0 && ( + {!card && dataLength > 0 && (

@@ -117,10 +106,4 @@ const DataCard = ({ ); }; -export default connect((state: RootState) => ({ - name: state.name, - metadata: state.metadata, - datasetDetails: getDatasetDetails(state), - dataLength: state.data.length, - removedRowsCount: state.removedRowsCount, -}))(DataCard); +export default DataCard; diff --git a/frontend/packages/labs/ailab/src/UIComponents/DataDisplay.tsx b/frontend/packages/labs/ailab/src/UIComponents/DataDisplay.tsx index ae65c7ec3dbf1..bea18a560b74b 100644 --- a/frontend/packages/labs/ailab/src/UIComponents/DataDisplay.tsx +++ b/frontend/packages/labs/ailab/src/UIComponents/DataDisplay.tsx @@ -1,19 +1,14 @@ /* React component to handle displaying imported data. */ -import {connect} from 'react-redux'; - import {styles} from '../constants'; +import {useAppSelector} from '../hooks'; import I18n from '../i18n'; -import type {RootState} from '../redux'; -import type {DataRow} from '../types'; import DataTable from './DataTable'; import Statement from './Statement'; -interface DataDisplayProps { - data: DataRow[]; -} +const DataDisplay = () => { + const data = useAppSelector(state => state.data); -const DataDisplay = ({data}: DataDisplayProps) => { if (data.length === 0) { return null; } @@ -38,6 +33,4 @@ const DataDisplay = ({data}: DataDisplayProps) => { ); }; -export default connect((state: RootState) => ({ - data: state.data, -}))(DataDisplay); +export default DataDisplay; diff --git a/frontend/packages/labs/ailab/src/UIComponents/DataTable.tsx b/frontend/packages/labs/ailab/src/UIComponents/DataTable.tsx index a69039716f01a..201c4bf164a5f 100644 --- a/frontend/packages/labs/ailab/src/UIComponents/DataTable.tsx +++ b/frontend/packages/labs/ailab/src/UIComponents/DataTable.tsx @@ -1,24 +1,11 @@ /* React component to handle displaying imported data. */ -import {connect} from 'react-redux'; -import type {Dispatch} from 'redux'; - import {styles} from '../constants'; import {getLocalizedColumnName} from '../helpers/columnDetails'; import {getLocalizedValue} from '../helpers/valueDetails'; -import type {RootState} from '../redux'; +import {deepEqual, useAppDispatch, useAppSelector} from '../hooks'; import {getTableData, setCurrentColumn, setHighlightColumn} from '../redux'; -import type {DataRow} from '../types'; interface DataTableProps { - currentPanel: string; - data: DataRow[]; - datasetId: string; - labelColumn: string; - selectedFeatures: string[]; - setCurrentColumn: (column?: string) => void; - setHighlightColumn: (column?: string) => void; - currentColumn?: string; - highlightColumn?: string; reducedColumns?: boolean; singleRow?: number; startingRow?: number; @@ -28,21 +15,29 @@ interface DataTableProps { } const DataTable = ({ - currentPanel, - data, - datasetId, - labelColumn, - selectedFeatures, - setCurrentColumn: setCurrentColumnProp, - setHighlightColumn: setHighlightColumnProp, - currentColumn, - highlightColumn, reducedColumns, singleRow, startingRow, noLabel, hideLabel, + useResultsData, }: DataTableProps) => { + const dispatch = useAppDispatch(); + const data = useAppSelector( + state => getTableData(state, !!useResultsData), + deepEqual, + ); + const datasetId = useAppSelector(state => state.metadata?.name || 'unknown'); + const labelColumn = useAppSelector(state => state.labelColumn || 'unknown'); + const selectedFeatures = useAppSelector(state => state.selectedFeatures); + const currentColumn = useAppSelector(state => state.currentColumn); + const highlightColumn = useAppSelector(state => state.highlightColumn); + const currentPanel = useAppSelector(state => state.currentPanel); + + const setCurrentColumnProp = (column: string | undefined) => + dispatch(setCurrentColumn(column as string)); + const setHighlightColumnProp = (column: string | undefined) => + dispatch(setHighlightColumn(column as string)); const getColumnHeaderStyle = (key: string) => { let style; @@ -172,22 +167,4 @@ const DataTable = ({ ); }; -export default connect( - (state: RootState, props: {useResultsData?: boolean}) => ({ - data: getTableData(state, !!props.useResultsData), - datasetId: state.metadata?.name || 'unknown', - labelColumn: state.labelColumn || 'unknown', - selectedFeatures: state.selectedFeatures, - currentColumn: state.currentColumn, - highlightColumn: state.highlightColumn, - currentPanel: state.currentPanel, - }), - (dispatch: Dispatch) => ({ - setCurrentColumn(column: string | undefined) { - dispatch(setCurrentColumn(column as string)); - }, - setHighlightColumn(column: string | undefined) { - dispatch(setHighlightColumn(column as string)); - }, - }), -)(DataTable); +export default DataTable; diff --git a/frontend/packages/labs/ailab/src/UIComponents/GenerateResults.tsx b/frontend/packages/labs/ailab/src/UIComponents/GenerateResults.tsx index ca8eefc12ecf6..016815e80fc46 100644 --- a/frontend/packages/labs/ailab/src/UIComponents/GenerateResults.tsx +++ b/frontend/packages/labs/ailab/src/UIComponents/GenerateResults.tsx @@ -1,13 +1,11 @@ /* React component to handle training. */ import {useState, useEffect, useRef, useCallback} from 'react'; -import {connect} from 'react-redux'; import {imageUrl} from '../assetPath'; import {styles, getFadeOpacity} from '../constants'; +import {deepEqual, useAppSelector} from '../hooks'; import I18n from '../i18n'; -import type {RootState} from '../redux'; import {getTableData} from '../redux'; -import type {DataRow} from '../types'; import {TestingAnimationDescription} from './AnimationDescriptions'; import DataTable from './DataTable'; @@ -15,15 +13,11 @@ import DataTable from './DataTable'; const framesPerCycle = 80; const maxNumItems = 7; -interface GenerateResultsProps { - data: DataRow[]; - instructionsOverlayActive: boolean; -} - -const GenerateResults = ({ - data, - instructionsOverlayActive, -}: GenerateResultsProps) => { +const GenerateResults = () => { + const data = useAppSelector(state => getTableData(state, true), deepEqual); + const instructionsOverlayActive = useAppSelector( + state => state.instructionsOverlayActive, + ); const [frame, setFrame] = useState(0); const [, setFinished] = useState(false); const frameRef = useRef(0); @@ -206,7 +200,4 @@ const GenerateResults = ({ ); }; -export default connect((state: RootState) => ({ - data: getTableData(state, true), - instructionsOverlayActive: state.instructionsOverlayActive, -}))(GenerateResults); +export default GenerateResults; diff --git a/frontend/packages/labs/ailab/src/UIComponents/ModelCard.tsx b/frontend/packages/labs/ailab/src/UIComponents/ModelCard.tsx index e7c750d4edfae..3dbd9d2193011 100644 --- a/frontend/packages/labs/ailab/src/UIComponents/ModelCard.tsx +++ b/frontend/packages/labs/ailab/src/UIComponents/ModelCard.tsx @@ -1,42 +1,26 @@ /* React component to handle displaying the model card. */ -import {connect} from 'react-redux'; - import {imageUrl} from '../assetPath'; import {styles} from '../constants'; import {getPercentCorrect} from '../helpers/accuracy'; import {getLocalizedColumnName} from '../helpers/columnDetails'; import {getDatasetDetails} from '../helpers/datasetDetails'; import {getLocalizedValue} from '../helpers/valueDetails'; +import {deepEqual, shallowEqual, useAppSelector} from '../hooks'; import I18n from '../i18n'; -import type {RootState} from '../redux'; import {getLabelToSave, getFeaturesToSave} from '../redux'; -import type { - ModelCardColumn, - TrainedModelDetailsSave, - DatasetDetails, -} from '../types'; import Statement from './Statement'; -interface ModelCardProps { - trainedModelDetails: TrainedModelDetailsSave; - selectedFeatures: string[]; - percentCorrect: string; - label: ModelCardColumn; - features: ModelCardColumn[]; - datasetDetails: DatasetDetails; - datasetId: string; -} - -const ModelCard = ({ - trainedModelDetails, - selectedFeatures, - percentCorrect, - label, - features, - datasetDetails, - datasetId, -}: ModelCardProps) => { +const ModelCard = () => { + const trainedModelDetails = useAppSelector( + state => state.trainedModelDetails, + ); + const selectedFeatures = useAppSelector(state => state.selectedFeatures); + const percentCorrect = useAppSelector(getPercentCorrect); + const label = useAppSelector(getLabelToSave, deepEqual); + const features = useAppSelector(getFeaturesToSave, deepEqual); + const datasetDetails = useAppSelector(getDatasetDetails, shallowEqual); + const datasetId = useAppSelector(state => state.metadata?.name || 'unknown'); const localizedLabel = getLocalizedColumnName(datasetDetails.name, label.id); const localizedFeatures = selectedFeatures.map(feature => getLocalizedColumnName(datasetDetails.name, feature), @@ -181,12 +165,4 @@ const ModelCard = ({ ); }; -export default connect((state: RootState) => ({ - trainedModelDetails: state.trainedModelDetails, - selectedFeatures: state.selectedFeatures, - percentCorrect: getPercentCorrect(state), - label: getLabelToSave(state), - features: getFeaturesToSave(state), - datasetDetails: getDatasetDetails(state), - datasetId: state.metadata?.name || 'unknown', -}))(ModelCard); +export default ModelCard; diff --git a/frontend/packages/labs/ailab/src/UIComponents/Predict.tsx b/frontend/packages/labs/ailab/src/UIComponents/Predict.tsx index 65ddc142d4f16..ed9937b293a5e 100644 --- a/frontend/packages/labs/ailab/src/UIComponents/Predict.tsx +++ b/frontend/packages/labs/ailab/src/UIComponents/Predict.tsx @@ -1,15 +1,13 @@ /* React component to handle predicting and displaying predictions. */ import type React from 'react'; -import {connect} from 'react-redux'; -import type {Dispatch} from 'redux'; import {imageUrl} from '../assetPath'; import {styles} from '../constants'; import {getLocalizedColumnName} from '../helpers/columnDetails'; import {getConvertedPredictedLabel} from '../helpers/valueConversion'; import {getLocalizedValue} from '../helpers/valueDetails'; +import {useAppDispatch, useAppSelector} from '../hooks'; import I18n from '../i18n'; -import type {RootState} from '../redux'; import {setTestData, getPredictAvailable} from '../redux'; import { getSelectedCategoricalFeatures, @@ -22,36 +20,27 @@ import train from '../train'; import ScrollableContent from './ScrollableContent'; -interface PredictProps { - labelColumn: string; - selectedCategoricalFeatures: string[]; - selectedNumericalFeatures: string[]; - uniqueOptionsByColumn: Record; - testData: Record; - setTestData: (feature: string, value: string | number) => void; - predictedLabel: string | number; - getPredictAvailable: boolean; - extremaByColumn: Record; - datasetId: string; -} +const Predict = () => { + const dispatch = useAppDispatch(); + const testData = useAppSelector(state => state.testData); + const predictedLabel = useAppSelector(getConvertedPredictedLabel); + const labelColumn = useAppSelector(state => state.labelColumn || 'unknown'); + const selectedNumericalFeatures = useAppSelector( + getSelectedNumericalFeatures, + ); + const selectedCategoricalFeatures = useAppSelector( + getSelectedCategoricalFeatures, + ); + const uniqueOptionsByColumn = useAppSelector(getUniqueOptionsByColumn); + const predictAvailable = useAppSelector(getPredictAvailable); + const extremaByColumn = useAppSelector(getExtremaByColumn); + const datasetId = useAppSelector(state => state.metadata?.name || 'unknown'); -const Predict = ({ - labelColumn, - selectedCategoricalFeatures, - selectedNumericalFeatures, - uniqueOptionsByColumn, - testData, - setTestData, - predictedLabel, - getPredictAvailable: predictAvailable, - extremaByColumn, - datasetId, -}: PredictProps) => { const handleChange = ( event: React.ChangeEvent, feature: string, ) => { - setTestData(feature, event.target.value); + dispatch(setTestData(feature, event.target.value)); }; const onClickPredict = () => { @@ -145,21 +134,4 @@ const Predict = ({ ); }; -export default connect( - (state: RootState) => ({ - testData: state.testData, - predictedLabel: getConvertedPredictedLabel(state), - labelColumn: state.labelColumn || 'unknown', - selectedNumericalFeatures: getSelectedNumericalFeatures(state), - selectedCategoricalFeatures: getSelectedCategoricalFeatures(state), - uniqueOptionsByColumn: getUniqueOptionsByColumn(state), - getPredictAvailable: getPredictAvailable(state), - extremaByColumn: getExtremaByColumn(state), - datasetId: state.metadata?.name || 'unknown', - }), - (dispatch: Dispatch) => ({ - setTestData(feature: string, value: string | number) { - dispatch(setTestData(feature, value)); - }, - }), -)(Predict); +export default Predict; diff --git a/frontend/packages/labs/ailab/src/UIComponents/Results.tsx b/frontend/packages/labs/ailab/src/UIComponents/Results.tsx index f134d25b95a90..d12aedcb0e65e 100644 --- a/frontend/packages/labs/ailab/src/UIComponents/Results.tsx +++ b/frontend/packages/labs/ailab/src/UIComponents/Results.tsx @@ -1,42 +1,31 @@ /* React component to handle displaying accuracy results. */ import {useEffect, useCallback} from 'react'; -import {connect} from 'react-redux'; -import type {Dispatch} from 'redux'; import {styles} from '../constants'; +import {useAppDispatch, useAppSelector} from '../hooks'; import I18n from '../i18n'; -import type {RootState} from '../redux'; import {setShowResultsDetails, setResultsPhase} from '../redux'; -import type {HistoricResult} from '../types'; import ResultsDetails from './ResultsDetails'; import ScrollableContent from './ScrollableContent'; import {UnconnectedStatement} from './Statement'; -interface ResultsProps { - historicResults: HistoricResult[]; - showResultsDetails: boolean; - setShowResultsDetails: (show: boolean) => void; - setResultsPhase: (phase: number) => void; -} +const Results = () => { + const historicResults = useAppSelector(state => state.historicResults); + const showResultsDetails = useAppSelector(state => state.showResultsDetails); + const dispatch = useAppDispatch(); -const Results = ({ - historicResults, - showResultsDetails, - setShowResultsDetails, - setResultsPhase, -}: ResultsProps) => { useEffect(() => { - setResultsPhase(0); + dispatch(setResultsPhase(0)); const timer = setTimeout(() => { - setResultsPhase(1); + dispatch(setResultsPhase(1)); }, 1000); return () => clearTimeout(timer); - }, [setResultsPhase]); + }, [dispatch]); const showDetails = useCallback(() => { - setShowResultsDetails(true); - }, [setShowResultsDetails]); + dispatch(setShowResultsDetails(true)); + }, [dispatch]); return (
@@ -101,17 +90,4 @@ const Results = ({ ); }; -export default connect( - (state: RootState) => ({ - historicResults: state.historicResults, - showResultsDetails: state.showResultsDetails, - }), - (dispatch: Dispatch) => ({ - setResultsPhase(phase: number) { - dispatch(setResultsPhase(phase)); - }, - setShowResultsDetails(show: boolean) { - dispatch(setShowResultsDetails(show)); - }, - }), -)(Results); +export default Results; diff --git a/frontend/packages/labs/ailab/src/UIComponents/ResultsDetails.tsx b/frontend/packages/labs/ailab/src/UIComponents/ResultsDetails.tsx index 834ed1249e82c..9a64c331c6738 100644 --- a/frontend/packages/labs/ailab/src/UIComponents/ResultsDetails.tsx +++ b/frontend/packages/labs/ailab/src/UIComponents/ResultsDetails.tsx @@ -2,8 +2,6 @@ import {faTimes} from '@fortawesome/free-solid-svg-icons'; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import {useCallback} from 'react'; -import {connect} from 'react-redux'; -import type {Dispatch} from 'redux'; import {ResultsGrades, styles} from '../constants'; import { @@ -11,31 +9,22 @@ import { getCorrectResults, getIncorrectResults, } from '../helpers/accuracy'; -import type {RootState} from '../redux'; +import {useAppDispatch, useAppSelector} from '../hooks'; import {setShowResultsDetails} from '../redux'; -import type {ResultsData} from '../types'; import ResultsTable from './ResultsTable'; import ResultsToggle from './ResultsToggle'; -interface ResultsDetailsProps { - resultsTab: string; - percentCorrect: string; - setShowResultsDetails: (show: boolean) => void; - correctResults: ResultsData; - incorrectResults: ResultsData; -} +const ResultsDetails = () => { + const dispatch = useAppDispatch(); + const resultsTab = useAppSelector(state => state.resultsTab); + const percentCorrect = useAppSelector(getPercentCorrect); + const correctResults = useAppSelector(getCorrectResults); + const incorrectResults = useAppSelector(getIncorrectResults); -const ResultsDetails = ({ - resultsTab, - percentCorrect, - setShowResultsDetails, - correctResults, - incorrectResults, -}: ResultsDetailsProps) => { const onClose = useCallback(() => { - setShowResultsDetails(false); - }, [setShowResultsDetails]); + dispatch(setShowResultsDetails(false)); + }, [dispatch]); const results = resultsTab === ResultsGrades.CORRECT ? correctResults : incorrectResults; @@ -59,16 +48,4 @@ const ResultsDetails = ({ ); }; -export default connect( - (state: RootState) => ({ - resultsTab: state.resultsTab, - percentCorrect: getPercentCorrect(state), - correctResults: getCorrectResults(state), - incorrectResults: getIncorrectResults(state), - }), - (dispatch: Dispatch) => ({ - setShowResultsDetails(show: boolean) { - dispatch(setShowResultsDetails(show)); - }, - }), -)(ResultsDetails); +export default ResultsDetails; diff --git a/frontend/packages/labs/ailab/src/UIComponents/ResultsTable.tsx b/frontend/packages/labs/ailab/src/UIComponents/ResultsTable.tsx index 7145bef524bc1..e13d9eedbf84b 100644 --- a/frontend/packages/labs/ailab/src/UIComponents/ResultsTable.tsx +++ b/frontend/packages/labs/ailab/src/UIComponents/ResultsTable.tsx @@ -1,35 +1,31 @@ /* React component to handle displaying test data and A.I. Bot's guesses. */ import {useCallback} from 'react'; -import {connect} from 'react-redux'; -import type {Dispatch} from 'redux'; import {styles, colors, REGRESSION_ERROR_TOLERANCE} from '../constants'; import {getLocalizedColumnName, isRegression} from '../helpers/columnDetails'; import {getLocalizedValue} from '../helpers/valueDetails'; +import {useAppDispatch, useAppSelector} from '../hooks'; import I18n from '../i18n'; -import type {RootState} from '../redux'; import {setResultsHighlightRow} from '../redux'; import type {ResultsData} from '../types'; interface ResultsTableProps { - selectedFeatures: string[]; - labelColumn: string; results: ResultsData; - isRegression: boolean; - setResultsHighlightRow: (row: number | undefined) => void; - resultsHighlightRow: number | undefined; - datasetId: string; } -const ResultsTable = ({ - selectedFeatures, - labelColumn, - results, - isRegression: isRegressionMode, - setResultsHighlightRow, - resultsHighlightRow, - datasetId, -}: ResultsTableProps) => { +const ResultsTable = ({results}: ResultsTableProps) => { + const dispatch = useAppDispatch(); + const selectedFeatures = useAppSelector(state => state.selectedFeatures); + const labelColumn = useAppSelector(state => state.labelColumn || 'unknown'); + const isRegressionMode = useAppSelector(isRegression); + const resultsHighlightRow = useAppSelector( + state => state.resultsHighlightRow, + ); + const datasetId = useAppSelector(state => state.metadata?.name || 'unknown'); + + const highlightRow = (row: number | undefined) => + dispatch(setResultsHighlightRow(row as number)); + const getRowCellStyle = useCallback( (index: number) => { return { @@ -122,8 +118,8 @@ const ResultsTable = ({ return ( setResultsHighlightRow(index)} - onMouseLeave={() => setResultsHighlightRow(undefined)} + onMouseEnter={() => highlightRow(index)} + onMouseLeave={() => highlightRow(undefined)} > {examples.map((example, i) => { return ( @@ -151,17 +147,4 @@ const ResultsTable = ({ ); }; -export default connect( - (state: RootState) => ({ - selectedFeatures: state.selectedFeatures, - labelColumn: state.labelColumn || 'unknown', - isRegression: isRegression(state), - resultsHighlightRow: state.resultsHighlightRow, - datasetId: state.metadata?.name || 'unknown', - }), - (dispatch: Dispatch) => ({ - setResultsHighlightRow(column: number | undefined) { - dispatch(setResultsHighlightRow(column as number)); - }, - }), -)(ResultsTable); +export default ResultsTable; diff --git a/frontend/packages/labs/ailab/src/UIComponents/ResultsToggle.tsx b/frontend/packages/labs/ailab/src/UIComponents/ResultsToggle.tsx index 65f6f667bd89e..2201e164234e8 100644 --- a/frontend/packages/labs/ailab/src/UIComponents/ResultsToggle.tsx +++ b/frontend/packages/labs/ailab/src/UIComponents/ResultsToggle.tsx @@ -1,19 +1,16 @@ /* React component to handle toggling between correct/incorrect test results */ import {faTimes, faCheck} from '@fortawesome/free-solid-svg-icons'; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; -import {connect} from 'react-redux'; import {ResultsGrades, styles} from '../constants'; +import {useAppDispatch, useAppSelector} from '../hooks'; import I18n from '../i18n'; -import type {RootState} from '../redux'; import {setResultsTab} from '../redux'; -interface ResultsToggleProps { - resultsTab?: string; - setResultsTab?: (key: string) => void; -} +const ResultsToggle = () => { + const resultsTab = useAppSelector(state => state.resultsTab); + const dispatch = useAppDispatch(); -const ResultsToggle = ({resultsTab, setResultsTab}: ResultsToggleProps) => { const getTogglePillStyle = (key: string) => { let style; if (key === resultsTab) { @@ -46,8 +43,8 @@ const ResultsToggle = ({resultsTab, setResultsTab}: ResultsToggleProps) => {
setResultsTab!(tab.key)} - onKeyDown={() => setResultsTab!(tab.key)} + onClick={() => dispatch(setResultsTab(tab.key))} + onKeyDown={() => dispatch(setResultsTab(tab.key))} role="button" tabIndex={0} > @@ -60,13 +57,4 @@ const ResultsToggle = ({resultsTab, setResultsTab}: ResultsToggleProps) => { ); }; -export default connect( - (state: RootState) => ({ - resultsTab: state.resultsTab, - }), - dispatch => ({ - setResultsTab(key: string) { - dispatch(setResultsTab(key)); - }, - }), -)(ResultsToggle); +export default ResultsToggle; diff --git a/frontend/packages/labs/ailab/src/UIComponents/SaveModel.tsx b/frontend/packages/labs/ailab/src/UIComponents/SaveModel.tsx index 0d6ac3e44f5ed..7983c53885428 100644 --- a/frontend/packages/labs/ailab/src/UIComponents/SaveModel.tsx +++ b/frontend/packages/labs/ailab/src/UIComponents/SaveModel.tsx @@ -1,8 +1,6 @@ /* React component to handle saving a trained model. */ import type React from 'react'; import {useState} from 'react'; -import {connect} from 'react-redux'; -import type {Dispatch} from 'redux'; import {styles, ModelNameMaxLength} from '../constants'; import {getLocalizedColumnName} from '../helpers/columnDetails'; @@ -10,35 +8,24 @@ import { getDatasetDescription, isUserUploadedDataset, } from '../helpers/datasetDetails'; +import {useAppDispatch, useAppSelector} from '../hooks'; import I18n from '../i18n'; -import type {RootState} from '../redux'; import {setTrainedModelDetail} from '../redux'; import {getSelectedColumnsDescriptions} from '../selectors'; import ScrollableContent from './ScrollableContent'; import Statement from './Statement'; -interface SaveModelProps { - setTrainedModelDetail: ( - field: string, - value: string, - isColumn: boolean, - ) => void; - labelColumn: string | undefined; - columnDescriptions: {id: string; description: string | null}[]; - dataDescription: string | undefined; - isUserUploadedDataset: boolean; - datasetId: string | undefined; -} +const SaveModel = () => { + const dispatch = useAppDispatch(); + const labelColumn = useAppSelector(state => state.labelColumn); + const columnDescriptions = useAppSelector(getSelectedColumnsDescriptions); + const dataDescription = useAppSelector(getDatasetDescription); + const isUserUploaded = useAppSelector(isUserUploadedDataset); + const datasetId = useAppSelector( + state => state.metadata && state.metadata.name, + ); -const SaveModel = ({ - setTrainedModelDetail, - labelColumn, - columnDescriptions, - dataDescription, - isUserUploadedDataset: isUserUploaded, - datasetId, -}: SaveModelProps) => { const [showColumnDescriptions, setShowColumnDescriptions] = useState(isUserUploaded); @@ -51,7 +38,7 @@ const SaveModel = ({ field: string, isColumn: boolean, ) => { - setTrainedModelDetail(field, event.target.value, isColumn); + dispatch(setTrainedModelDetail(field, event.target.value, isColumn)); }; const getColumnFields = () => { @@ -250,17 +237,4 @@ const SaveModel = ({ ); }; -export default connect( - (state: RootState) => ({ - labelColumn: state.labelColumn, - columnDescriptions: getSelectedColumnsDescriptions(state), - dataDescription: getDatasetDescription(state), - isUserUploadedDataset: isUserUploadedDataset(state), - datasetId: state.metadata && state.metadata.name, - }), - (dispatch: Dispatch) => ({ - setTrainedModelDetail(field: string, value: string, isColumn: boolean) { - dispatch(setTrainedModelDetail(field, value, isColumn)); - }, - }), -)(SaveModel); +export default SaveModel; diff --git a/frontend/packages/labs/ailab/src/UIComponents/ScatterPlot.tsx b/frontend/packages/labs/ailab/src/UIComponents/ScatterPlot.tsx index 4d79605799608..881b7ef505e91 100644 --- a/frontend/packages/labs/ailab/src/UIComponents/ScatterPlot.tsx +++ b/frontend/packages/labs/ailab/src/UIComponents/ScatterPlot.tsx @@ -1,9 +1,8 @@ import {Scatter} from 'react-chartjs-2'; -import {connect} from 'react-redux'; import {styles, colors} from '../constants'; +import {useAppSelector} from '../hooks'; import I18n from '../i18n'; -import type {RootState} from '../redux'; import {getScatterPlotData} from '../selectors/visualizationSelectors'; const scatterDataBase = { @@ -55,15 +54,9 @@ const chartOptionsBase = { maintainAspectRatio: false, }; -interface ScatterPlotProps { - scatterPlotData: { - coordinates: {x: number; y: number}[]; - feature: string; - label: string; - } | null; -} +const ScatterPlot = () => { + const scatterPlotData = useAppSelector(getScatterPlotData); -const ScatterPlot = ({scatterPlotData}: ScatterPlotProps) => { const scatterDataCombined = { ...scatterDataBase, }; @@ -93,9 +86,4 @@ const ScatterPlot = ({scatterPlotData}: ScatterPlotProps) => { ); }; -export default connect( - (state: RootState) => ({ - scatterPlotData: getScatterPlotData(state), - }), - {}, -)(ScatterPlot); +export default ScatterPlot; diff --git a/frontend/packages/labs/ailab/src/UIComponents/SelectDataset.tsx b/frontend/packages/labs/ailab/src/UIComponents/SelectDataset.tsx index 37db22db5da7c..2381838740dbb 100644 --- a/frontend/packages/labs/ailab/src/UIComponents/SelectDataset.tsx +++ b/frontend/packages/labs/ailab/src/UIComponents/SelectDataset.tsx @@ -1,50 +1,40 @@ /* React component to handle importing CSVs and pushing data to Redux store. */ import {useState} from 'react'; -import {connect} from 'react-redux'; -import type {Dispatch} from 'redux'; import {styles} from '../constants'; import {parseCSV, MIN_CSV_ROWS, MIN_CSV_COLUMNS} from '../csvReaderWrapper'; import {getDatasets, getAvailableDatasets} from '../datasetManifest'; +import {useAppDispatch, useAppSelector} from '../hooks'; import I18n from '../i18n'; import {parseJSON} from '../jsonReaderWrapper'; -import type {RootState} from '../redux'; import { - setSelectedName, - setSelectedCSV, - setSelectedJSON, - resetState, + setSelectedName as setSelectedNameAction, + setSelectedCSV as setSelectedCSVAction, + setSelectedJSON as setSelectedJSONAction, + resetState as resetStateAction, getSpecifiedDatasets, - setHighlightDataset, + setHighlightDataset as setHighlightDatasetAction, } from '../redux'; import ScrollableContent from './ScrollableContent'; -interface SelectDatasetProps { - setSelectedName: (name: string) => void; - setSelectedCSV: (csvfilePath: string | File) => void; - setSelectedJSON: (jsonfilePath: string) => void; - setHighlightDataset: (id: string | undefined) => void; - csvfile: string | object | undefined; - jsonfile: string | object | undefined; - resetState: () => void; - specifiedDatasets: string[] | undefined; - name: string | undefined; - highlightDataset: string | undefined; - invalidData: string | undefined; -} - -const SelectDataset = ({ - setSelectedName, - setSelectedCSV, - setSelectedJSON, - setHighlightDataset, - resetState, - specifiedDatasets, - name, - highlightDataset, - invalidData, -}: SelectDatasetProps) => { +const SelectDataset = () => { + const dispatch = useAppDispatch(); + const specifiedDatasets = useAppSelector(getSpecifiedDatasets); + const name = useAppSelector(state => state.name); + const highlightDataset = useAppSelector(state => state.highlightDataset); + const invalidData = useAppSelector(state => state.invalidData); + + const resetState = () => dispatch(resetStateAction()); + const setSelectedName = (datasetName: string) => + dispatch(setSelectedNameAction(datasetName)); + const setSelectedCSV = (csvfilePath: string | File) => + dispatch(setSelectedCSVAction(csvfilePath as string)); + const setSelectedJSON = (jsonfilePath: string) => + dispatch(setSelectedJSONAction(jsonfilePath)); + const setHighlightDataset = (id: string | undefined) => + dispatch(setHighlightDatasetAction(id as string)); + const [, setDownload] = useState(false); const handleDatasetClick = (id: string) => { @@ -161,30 +151,4 @@ const SelectDataset = ({ ); }; -export default connect( - (state: RootState) => ({ - csvfile: state.csvfile, - jsonfile: state.jsonfile, - specifiedDatasets: getSpecifiedDatasets(state), - name: state.name, - highlightDataset: state.highlightDataset, - invalidData: state.invalidData, - }), - (dispatch: Dispatch) => ({ - resetState() { - dispatch(resetState()); - }, - setSelectedName(name: string) { - dispatch(setSelectedName(name)); - }, - setSelectedCSV(csvfilePath: string | File) { - dispatch(setSelectedCSV(csvfilePath as string)); - }, - setSelectedJSON(jsonfilePath: string) { - dispatch(setSelectedJSON(jsonfilePath)); - }, - setHighlightDataset(id: string | undefined) { - dispatch(setHighlightDataset(id as string)); - }, - }), -)(SelectDataset); +export default SelectDataset; diff --git a/frontend/packages/labs/ailab/src/UIComponents/SelectLabelButton.tsx b/frontend/packages/labs/ailab/src/UIComponents/SelectLabelButton.tsx index 3862b6b07f7f8..00e53b9bcd4d2 100644 --- a/frontend/packages/labs/ailab/src/UIComponents/SelectLabelButton.tsx +++ b/frontend/packages/labs/ailab/src/UIComponents/SelectLabelButton.tsx @@ -1,21 +1,18 @@ /* React component to handle selecting a column as the label. */ -import {connect} from 'react-redux'; - import {styles} from '../constants'; +import {useAppDispatch} from '../hooks'; import I18n from '../i18n'; import {setLabelColumn} from '../redux'; interface SelectLabelButtonProps { column?: string; - setLabelColumn: (column: string) => void; } -const SelectLabelButton = ({ - column, - setLabelColumn, -}: SelectLabelButtonProps) => { +const SelectLabelButton = ({column}: SelectLabelButtonProps) => { + const dispatch = useAppDispatch(); + const setPredictColumn = (event: React.MouseEvent, column: string) => { - setLabelColumn(column); + dispatch(setLabelColumn(column)); event.preventDefault(); }; @@ -31,8 +28,4 @@ const SelectLabelButton = ({ ); }; -export default connect(null, dispatch => ({ - setLabelColumn(column: string) { - dispatch(setLabelColumn(column)); - }, -}))(SelectLabelButton); +export default SelectLabelButton; diff --git a/frontend/packages/labs/ailab/src/UIComponents/Statement.tsx b/frontend/packages/labs/ailab/src/UIComponents/Statement.tsx index e66383da250a4..0d2410f6e47f9 100644 --- a/frontend/packages/labs/ailab/src/UIComponents/Statement.tsx +++ b/frontend/packages/labs/ailab/src/UIComponents/Statement.tsx @@ -1,12 +1,11 @@ /* React component to display a statement about our model. */ import {faTimesCircle} from '@fortawesome/free-solid-svg-icons'; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; -import {connect} from 'react-redux'; import {styles} from '../constants'; import {getLocalizedColumnName} from '../helpers/columnDetails'; +import {useAppDispatch, useAppSelector} from '../hooks'; import I18n from '../i18n'; -import type {RootState} from '../redux'; import {setLabelColumn, removeSelectedFeature} from '../redux'; interface StatementProps { @@ -153,22 +152,32 @@ const Statement = ({ ); }; +// The presentational component, for callers that supply props directly (e.g. +// the historic-results list in Results). export const UnconnectedStatement = Statement; -export default connect( - (state: RootState) => ({ - shouldShow: state.data.length !== 0, - currentPanel: state.currentPanel, - labelColumn: state.labelColumn, - selectedFeatures: state.selectedFeatures, - datasetId: state.metadata && state.metadata.name, - }), - dispatch => ({ - setLabelColumn(labelColumn: string | null) { - dispatch(setLabelColumn(labelColumn as string)); - }, - removeSelectedFeature(labelColumn: string) { - dispatch(removeSelectedFeature(labelColumn)); - }, - }), -)(Statement); +// Store-connected container: the default export used in the live panels. +const ConnectedStatement = () => { + const dispatch = useAppDispatch(); + const shouldShow = useAppSelector(state => state.data.length !== 0); + const currentPanel = useAppSelector(state => state.currentPanel); + const labelColumn = useAppSelector(state => state.labelColumn); + const selectedFeatures = useAppSelector(state => state.selectedFeatures); + const datasetId = useAppSelector( + state => state.metadata && state.metadata.name, + ); + + return ( + dispatch(setLabelColumn(column as string))} + removeSelectedFeature={id => dispatch(removeSelectedFeature(id))} + /> + ); +}; + +export default ConnectedStatement; diff --git a/frontend/packages/labs/ailab/src/UIComponents/TrainModel.tsx b/frontend/packages/labs/ailab/src/UIComponents/TrainModel.tsx index e7e7a0e33b7da..a023cf6254d69 100644 --- a/frontend/packages/labs/ailab/src/UIComponents/TrainModel.tsx +++ b/frontend/packages/labs/ailab/src/UIComponents/TrainModel.tsx @@ -1,15 +1,13 @@ /* React component to handle training. */ import {useState, useEffect, useRef} from 'react'; -import {connect} from 'react-redux'; import {imageUrl} from '../assetPath'; import {styles, getFadeOpacity} from '../constants'; +import {useAppSelector} from '../hooks'; import I18n from '../i18n'; -import type {RootState} from '../redux'; import {getTableData} from '../redux'; import {store} from '../store'; import train from '../train'; -import type {DataRow} from '../types'; import {TrainingAnimationDescription} from './AnimationDescriptions'; import DataTable from './DataTable'; @@ -17,12 +15,11 @@ import DataTable from './DataTable'; const framesPerCycle = 80; const maxNumItems = 7; -interface TrainModelProps { - data: DataRow[]; - instructionsOverlayActive: boolean; -} - -const TrainModel = ({data, instructionsOverlayActive}: TrainModelProps) => { +const TrainModel = () => { + const data = useAppSelector(state => getTableData(state, false)); + const instructionsOverlayActive = useAppSelector( + state => state.instructionsOverlayActive, + ); const [frame, setFrame] = useState(0); const [headOpen, setHeadOpen] = useState(false); const [, setFinished] = useState(false); @@ -183,7 +180,4 @@ const TrainModel = ({data, instructionsOverlayActive}: TrainModelProps) => { ); }; -export default connect((state: RootState) => ({ - data: getTableData(state, false), - instructionsOverlayActive: state.instructionsOverlayActive, -}))(TrainModel); +export default TrainModel; diff --git a/frontend/packages/labs/ailab/src/UIComponents/UniqueOptionsWarning.tsx b/frontend/packages/labs/ailab/src/UIComponents/UniqueOptionsWarning.tsx index 250270c7f8f71..0455461cc2083 100644 --- a/frontend/packages/labs/ailab/src/UIComponents/UniqueOptionsWarning.tsx +++ b/frontend/packages/labs/ailab/src/UIComponents/UniqueOptionsWarning.tsx @@ -1,16 +1,12 @@ /* React component to handle showing warning for excessive unique options. */ -import {connect} from 'react-redux'; - import {styles, UNIQUE_OPTIONS_MAX} from '../constants'; +import {useAppSelector} from '../hooks'; import I18n from '../i18n'; -import type {RootState} from '../redux'; import {hasTooManyUniqueOptions} from '../selectors/currentColumnSelectors'; -interface UniqueOptionsWarningProps { - showWarning?: boolean; -} +const UniqueOptionsWarning = () => { + const showWarning = useAppSelector(hasTooManyUniqueOptions); -const UniqueOptionsWarning = ({showWarning}: UniqueOptionsWarningProps) => { if (!showWarning) { return null; } @@ -27,6 +23,4 @@ const UniqueOptionsWarning = ({showWarning}: UniqueOptionsWarningProps) => { ); }; -export default connect((state: RootState) => ({ - showWarning: hasTooManyUniqueOptions(state), -}))(UniqueOptionsWarning); +export default UniqueOptionsWarning; diff --git a/frontend/packages/labs/ailab/src/helpers/datasetDetails.ts b/frontend/packages/labs/ailab/src/helpers/datasetDetails.ts index a440191804290..4d3c715d82f41 100644 --- a/frontend/packages/labs/ailab/src/helpers/datasetDetails.ts +++ b/frontend/packages/labs/ailab/src/helpers/datasetDetails.ts @@ -4,6 +4,10 @@ import type {DatasetDetails} from '../types'; /* Helper functions for getting information about the selected dataset. */ +// Reads the I18n global, so it is intentionally NOT memoized on Redux state +// (a state-keyed cache would go stale across a locale change). Callers that +// select it should pass `shallowEqual` to useSelector — the result is a flat +// object, so that dedupes the reference and avoids unnecessary rerenders. export function getDatasetDetails(state: RootState): DatasetDetails { const datasetDetails: DatasetDetails = { name: state.metadata?.name || '', diff --git a/frontend/packages/labs/ailab/src/hooks.ts b/frontend/packages/labs/ailab/src/hooks.ts new file mode 100644 index 0000000000000..5b263c9860d94 --- /dev/null +++ b/frontend/packages/labs/ailab/src/hooks.ts @@ -0,0 +1,48 @@ +import { + type TypedUseSelectorHook, + shallowEqual, + useDispatch, + useSelector, +} from 'react-redux'; + +import type {RootState} from './redux'; + +// Typed react-redux hooks for use throughout the package, in place of connect(). +export const useAppSelector: TypedUseSelectorHook = useSelector; +export const useAppDispatch = useDispatch; + +// Re-exported for selectors that build a fresh object/array each call (e.g. +// derived shapes that also read non-Redux globals like I18n and so can't be +// memoized on state): pass it to useAppSelector to dedupe by value. +export {shallowEqual}; + +// Structural equality for useAppSelector, for selectors whose result is nested +// (arrays of objects, or objects with array fields) and so isn't deduped by +// shallowEqual. Like shallowEqual it lets a selector that rebuilds an equal +// value each call avoid spurious rerenders without being memoized on state. +export function deepEqual(a: unknown, b: unknown): boolean { + if (a === b) { + return true; + } + if ( + typeof a !== 'object' || + typeof b !== 'object' || + a === null || + b === null + ) { + return false; + } + const keysA = Object.keys(a); + const keysB = Object.keys(b); + if (keysA.length !== keysB.length) { + return false; + } + return keysA.every( + key => + Object.prototype.hasOwnProperty.call(b, key) && + deepEqual( + (a as Record)[key], + (b as Record)[key], + ), + ); +} diff --git a/frontend/packages/labs/ailab/src/index.tsx b/frontend/packages/labs/ailab/src/index.tsx index f60a2bfd0db31..896ae657aafcb 100644 --- a/frontend/packages/labs/ailab/src/index.tsx +++ b/frontend/packages/labs/ailab/src/index.tsx @@ -18,6 +18,7 @@ import { setReserveLocation, setInstructionsDismissed, setFirehoseMetricsLogger, + getTrainedModelDataToSave, } from './redux'; import {store} from './store'; import type {Mode, ModelDataToSave, SaveResponse} from './types'; @@ -36,7 +37,7 @@ let saveTrainedModel: | undefined = null; let onContinue: (() => void) | null | undefined = null; -interface InitAllOptions { +export interface InitAllOptions { i18n?: Record; mode?: Mode; onContinue?: () => void; @@ -123,7 +124,11 @@ const processMode = (mode: Mode | undefined): void => { }; // Do the asynchronous save of a model. -const startSaveTrainedModel = (dataToSave: ModelDataToSave): void => { +// TODO: In the RTK migration, fold this into a thunk action so the payload is +// composed from getState() in the thunk body and App can simply dispatch it, +// rather than the store being reached into here. +const startSaveTrainedModel = (): void => { + const dataToSave = getTrainedModelDataToSave(store.getState()); store.dispatch(setSaveStatus('started')); saveTrainedModel!(dataToSave, (response: SaveResponse) => { store.dispatch(setSaveStatus(response.status, response.data)); @@ -135,3 +140,6 @@ const startSaveTrainedModel = (dataToSave: ModelDataToSave): void => { } }); }; + +// Export a few types. +export {type SaveResponse, type ModelDataToSave} from './types'; diff --git a/frontend/packages/labs/ailab/src/redux.ts b/frontend/packages/labs/ailab/src/redux.ts index 27f0156651942..b739b9d7d036a 100644 --- a/frontend/packages/labs/ailab/src/redux.ts +++ b/frontend/packages/labs/ailab/src/redux.ts @@ -820,6 +820,9 @@ export function getPredictAvailable(state: RootState): boolean { ); } +// Builds {prev, next} and reads the I18n global for button text, so it is not +// memoized on Redux state. App selects it with a prev/next-aware equality fn so +// the new object reference doesn't trigger unnecessary rerenders. export function getPanelButtons(state: RootState): PrevNextButtons { return prevNextButtons(state); }