diff --git a/lab/webapp/dist/App.css b/lab/webapp/dist/App.css index c8b656ca8..3b0bcaf7b 100644 --- a/lab/webapp/dist/App.css +++ b/lab/webapp/dist/App.css @@ -38,6 +38,9 @@ body { /* file upload form styling */ .pennai .file-upload-segment { max-width: 630px; + /* add 'overflow: auto' so the floated child + buttons stay contained within the segment */ + overflow: auto; } .pennai .file-upload-form-dataset-input { width: 40%; @@ -63,21 +66,43 @@ body { .pennai .file-upload-help-icon { padding-left: 0; } -.pennai .file-upload-ordinal-text-area { - width: 90% +.pennai .file-upload-header { + margin-top: 1em; } -.pennai .file-upload-sortable-list { +.pennai .file-upload-pseudo-dialog-button { + size: "small"; + float: right; +} +.pennai .file-upload-feature-text-info{ + color:rgba(255, 255, 255, 0.9); + background-color:rgba(17, 17, 17, 0.9); +} +.pennai .file-upload-feature-text-input{ + width: 100%; + color:rgba(255, 255, 255, 0.9); + background-color:rgba(17, 17, 17, 0.9); + resize: none; +} +.pennai .file-upload-centered-div { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + min-height: 80vh; +} +.file-upload-sortable-list { list-style-type: none; margin: auto; width: 80%; padding: 10px 5px; } -.pennai .file-upload-sortable-list-item { +.file-upload-sortable-list-item { /*border-style: solid;*/ border: 1px solid; text-align: center; padding: 2px; border-color: rgb(84,200,255); + color: rgba(255, 255, 255, 0.9); border-radius: 4px; } /* This class is for the list item that moves @@ -89,20 +114,9 @@ the element isn't visible at all. See https://github.com/clauderic/react-sortable-hoc/issues/87 */ .file-upload-sortable-list-item-helper { - border: 2px solid; - text-align: center; - /* something happens to the padding compared to the regular item defined above, - and the text is aligned at bottom of border instead of middle. - But these settings aren't effecting things. Huh. - */ - padding-top: 0 !important; - padding-bottom: 5px !important; - border-color: yellow; /* rgb(84,200,255);*/ - border-radius: 4px; z-index: 10; /*important*/ - color: rgba(255,255,255,.9); - list-style-type: none; /*remove the bullet*/ - } + list-style-type: none; +} .pennai .file-upload-table { line-height: 1; } @@ -426,10 +440,13 @@ See https://github.com/clauderic/react-sortable-hoc/issues/87 * The bug is in v1.3.1 and v2.0.0. * Note that there's no way to reference a color value from the .ui class * that defines the inverted color w/out adding a css preprocessor */ - .pennai .inverted-dropdown-search .ui.search.dropdown { +.pennai .inverted-dropdown-search .ui.search.dropdown { color: rgba(255,255,255,0.9); + border: 2px solid rgb(84,200,255); + border-radius: 4px; + padding: 0.4em 1em; } - .pennai .inverted-dropdown-search .ui.search.dropdown > input.search { +.pennai .inverted-dropdown-search .ui.search.dropdown > input.search { color: rgba(255,255,255,0.9); } /* Manual fix for color of dropdown text in an inline form.field dropdown control @@ -437,6 +454,9 @@ See https://github.com/clauderic/react-sortable-hoc/issues/87 */ .pennai .inverted-dropdown-inline .ui.dropdown { color: rgba(255,255,255,0.9); + border: 2px solid rgb(84,200,255); + border-radius: 4px; + padding: 0.4em 1em; } .gauge { diff --git a/lab/webapp/src/components/FileUpload/index.js b/lab/webapp/src/components/FileUpload/index.js index e82c076ee..c82837b24 100644 --- a/lab/webapp/src/components/FileUpload/index.js +++ b/lab/webapp/src/components/FileUpload/index.js @@ -73,6 +73,7 @@ class FileUpload extends Component { this.state = this.initState; // enter info in text fields + this.resetState = this.resetState.bind(this); this.handleDepColDropdown = this.handleDepColDropdown.bind(this); this.handleCatFeaturesUserTextOnChange = this.handleCatFeaturesUserTextOnChange.bind(this); this.handleCatFeaturesUserTextBlur = this.handleCatFeaturesUserTextBlur.bind(this); @@ -93,7 +94,7 @@ class FileUpload extends Component { this.handleOrdinalSortDragRelease = this.handleOrdinalSortDragRelease.bind(this); this.handleOrdinalRankClick = this.handleOrdinalRankClick.bind(this); this.handleOrdinalSortAccept = this.handleOrdinalSortAccept.bind(this); - this.handleOrdinalSortCancel = this.handleOrdinalSortCancel.bind(this); + this.handleOrdinalRankCancel = this.handleOrdinalRankCancel.bind(this); this.getUniqueValuesForFeature = this.getUniqueValuesForFeature.bind(this); this.getFeatureDefaultType = this.getFeatureDefaultType.bind(this); this.ordinalFeaturesClearToDefault = this.ordinalFeaturesClearToDefault.bind(this); @@ -110,6 +111,12 @@ class FileUpload extends Component { this.initFeatureTypeDefaults = this.initFeatureTypeDefaults.bind(this); this.getDependentColumn = this.getDependentColumn.bind(this); this.getElapsedTime = this.getElapsedTime.bind(this); + this.getOridinalRankingDialog = this.getOridinalRankingDialog.bind(this); + this.getOrdinalFeaturesUserTextModal = this.getOrdinalFeaturesUserTextModal.bind(this); + this.getCatFeaturesUserTextModal = this.getCatFeaturesUserTextModal.bind(this); + this.handleKeyDown = this.handleKeyDown.bind(this); + this.enableKeyDownHandler = this.enableKeyDownHandler.bind(this); + this.initDependentColumn = this.initDependentColumn.bind(this); //this.cleanedInput = this.cleanedInput.bind(this) @@ -150,6 +157,10 @@ class FileUpload extends Component { selectedFile: null, /** Flag tells us when a file is being loaded and processed for preview. */ processingFileForPreview: false, + /** The dataset object for preview, read from file */ + datasetPreview: null, + /** Show the model error dialog */ + showErrorModal: false, /** {array} String-array holding the type for each feature, in same index order as features within the data. * For assignment, use the gettors: * featureTypeNumeric, featureTypeCategorical, featureTypeOrdinal, featureTypeDependent } @@ -190,11 +201,45 @@ class FileUpload extends Component { } } + /** Reset the state to its default, clearing any loaded data. */ + resetState() { + this.setState(this.initState); + } + /** * React lifecycle method, when component loads into html dom, 'reset' state */ componentDidMount() { - this.setState(this.initState); //Not sure why this is called here + this.resetState(); //Not sure why this is called here + //Add listener for keystrokes so we can process esc key globablly. + this.enableKeyDownHandler(true); + } + + componentWillUnmount(){ + //Supposedly if we don't remove the listener we can get memory leaks + this.enableKeyDownHandler(false); + } + + enableKeyDownHandler(enable){ + if(enable){ + //Do this with a delay because otherwise any events (e.g. from modal error dialog) + // meant to be handled by a proper modal dialog will also be handled by this handler, + // which we don't want. + setTimeout(() => {document.addEventListener("keydown", this.handleKeyDown, false);}, 100); + } + else { + document.removeEventListener("keydown", this.handleKeyDown, false); + } + } + + /** Special document-wide key event handler */ + handleKeyDown(event){ + //handle escape key to close our pseudo-modal dialogs. + if(event.keyCode === 27 ) { + this.handleOrdinalRankCancel(); + this.handleOrdinalFeaturesUserTextCancel(); + this.handleCatFeaturesUserTextCancel(); + } } /** Helper routine for debugging. Get elapsed time in sec from @@ -243,15 +288,23 @@ class FileUpload extends Component { /** Process the passed string and update Categorical feature settings. * Assumes the input string has been validated. - * Will override any settings made via feature-type dropdowns selectors, + * Will override any settings made via feature-type dropdowns selectors + * by setting any current Categorical features to their default type, * EXCEPT that any fields that are auto-detected as type Categorical will - * stay as type Categorical even if not listed in the user string. + * stay as type Categorical even if not listed in the input string. */ catFeaturesUserTextIngest(input) { - let cats = input.split(','); - cats.forEach( (feature) => { - this.setFeatureType(feature.trim(), this.featureTypeCategorical); + //First take all current cat features and set them to their auto-defaults. + //This is necessary for condition when user unassigns a cat feature via new input. + this.getCatFeatures().forEach( feature => { + this.setFeatureType(feature, this.getFeatureDefaultType(feature)); }) + //Now set cat features from the input string + if(input != "") { + input.split(',').forEach( (feature) => { + this.setFeatureType(feature.trim(), this.featureTypeCategorical); + }) + } } /** Handler for accepting button to accept categorical feature user text element. @@ -262,15 +315,7 @@ class FileUpload extends Component { * @returns {void} - no return value */ handleCatFeaturesUserTextAccept(e) { - //If empty string, just populate with current categorical features - if(this.state.catFeaturesUserText.trim() === "") { - this.setState({ - catFeaturesUserText: this.getCatFeatures().join(), - catFeaturesUserTextModalOpen: false, - }) - return; - } - //Validate the whole text + //Validate the whole text. Handles empty strings cleanly. let result = this.catFeaturesUserTextValidateAndExpand(this.state.catFeaturesUserText); if( result.success ) { this.catFeaturesUserTextIngest(result.expanded); @@ -363,7 +408,7 @@ handleCatFeaturesUserTextCancel() { //Check that dependent column is valid if (!this.validateFeatureName(depCol)) { - return { errorResp: "Please assign a Dependent Feature Column." }; + return { errorResp: "Please assign a Target Column." }; } // Ordinal features. @@ -434,6 +479,9 @@ handleCatFeaturesUserTextCancel() { this.ordinalFeaturesClearToDefault(); //Init the store of default feature types this.initFeatureTypeDefaults(); + //Init the default dependent column. + //Do this before setAllFeatureTypes + this.initDependentColumn(); //Init the feature type assignments this.setAllFeatureTypes('autoDefault'); //Clear @@ -462,10 +510,8 @@ handleCatFeaturesUserTextCancel() { //Store the result this.setState({datasetPreview: result}); - if(this.isDevBuild) { - console.log( this.getElapsedTime() + " - setState complete. "); - } if(this.isDevBuild){ + console.log( this.getElapsedTime() + " - setState complete. "); console.log("Calling initDatasetPreview... "); this.getElapsedTime(); } @@ -494,16 +540,16 @@ handleCatFeaturesUserTextCancel() { try { if(this.isDevBuild) { this.getElapsedTime(); //resets the timer - console.log("=== Calling Papa.parse... "); + console.log("=== DevBuild output - Calling Papa.parse... "); } Papa.parse(uploadFile, papaConfig); } catch(error) { console.error('Error generating preview for selected file:', error); this.setState({ - selectedFile: undefined, + selectedFile: null, datasetPreview: null, - openErrorModal: false, + showErrorModal: false, processingFileForPreview: false }); this.showErrorModal("Error With File", JSON.stringify(error)); @@ -516,7 +562,7 @@ handleCatFeaturesUserTextCancel() { this.setState({ selectedFile: files[0], datasetPreview: null, - openErrorModal: false, + showErrorModal: false, processingFileForPreview: true }); @@ -525,16 +571,13 @@ handleCatFeaturesUserTextCancel() { this.setState({ selectedFile: null, datasetPreview: null, - openErrorModal: true + showErrorModal: true, + processingFileForPreview: false }); } } else { // reset state as fallback - this.setState({ - selectedFile: null, - datasetPreview: null, - openErrorModal: false - }); + this.resetState(); } } @@ -653,6 +696,10 @@ handleCatFeaturesUserTextCancel() { * @returns {null} */ initFeatureTypeDefaults() { + if(this.state.datasetPreview == null) { + console.log('ERROR - FileUpload.initFeatureTypeDefaults: datasetPreview is null'); + return; + } let newDefaults = {}; //First init all to type Numeric this.state.datasetPreview.meta.fields.forEach( (feature) => { @@ -671,6 +718,28 @@ handleCatFeaturesUserTextCancel() { this.setState({ featureTypeDefaults: newDefaults }); } + /** + * Initialize the dependent feature and set UI. + * Looks for columns with particular headers, and if none found, + * use the last column as dependent. + * Make sure this gets called before the first call to setAllFeatureTypes, but + * only gets called during init so user can override via UI. + */ + initDependentColumn() { + if(this.state.datasetPreview == null) { + console.log('ERROR - FileUpload.initDependentColumn: datasetPreview is null'); + return; + } + let fields = this.state.datasetPreview.meta.fields; + for( let i=0; i < fields.length; i++) { + let feature = this.state.datasetPreview.meta.fields[i]; + if(feature.toLowerCase() === 'class' || feature.toLowerCase() === 'target' || i === (fields.length-1)){ + this.setFeatureType(feature, this.featureTypeDependent); + break; + } + } + } + /** * For the passed feature, get the default type for it based on automatic * feature-type assignment algorithm. @@ -692,6 +761,10 @@ handleCatFeaturesUserTextCancel() { * where 'autoDefault' will set each feature type based on analysis of each feature's values */ setAllFeatureTypes(type) { + if(this.state.datasetPreview == null) { + console.log('ERROR - FileUpload.setAllFeatureTypes: datasetPreview is null'); + return; + } //Batch the setState calls that happen in setFeatureType so they don't re-render each time. //Some discussion here: https://medium.com/swlh/react-state-batch-update-b1b61bd28cd2 //setFeatureType calls setState() each time it's called, and this triggers a re-render @@ -833,8 +906,8 @@ handleCatFeaturesUserTextCancel() { }); } - /** Handle user canceling the ordinal sort/ranking */ - handleOrdinalSortCancel() { + /** Handle user canceling the ordinal sort/ranking and modal text-input */ + handleOrdinalRankCancel() { this.setState({ ordinalFeatureToRank: undefined, ordinalFeatureToRankValues: [] @@ -904,7 +977,7 @@ handleCatFeaturesUserTextCancel() { } //Make sure the feature name is not assigned as the dependent column if( this.getDependentColumn() === ordObj.feature ) { - return {success: false, message: "Feature '" + ordObj.feature + "' is currently assigned as the Dependent Column."}; + return {success: false, message: "Feature '" + ordObj.feature + "' is currently assigned as the Target Column."}; } //The remaining items are the unique values if( ordObj.values === undefined || ordObj.values.length === 0) { @@ -970,105 +1043,28 @@ handleCatFeaturesUserTextCancel() { * @retuns {JSX} Return JSX with button for field type Ordinal, otherwise null for no button. */ getDataTableOrdinalRankButton(feature) { - //Helper method for sortable list component - // https://github.com/clauderic/react-sortable-hoc - // https://clauderic.github.io/react-sortable-hoc/#/basic-configuration/multiple-lists?_k=7ghtqv - const SortableItem = SortableElement(({value}) =>
  • {value}
  • ); - //Helper method for sortable list component - const SortableList = SortableContainer(({items}) => { - return ( - - ); - }); - - //If we're currently ranking this ordinal feature, show the sortable list - // - if(this.state.ordinalFeatureToRank === feature) - return ( - //This puts the sortable list right in the cell. Awkward but it works. - - - } - open={this.state.ordinalFeaturesUserTextModalOpen} - onOpen={() => this.setState({ordinalFeaturesUserTextModalOpen: true})} - onClose={() => this.handleOrdinalFeaturesUserTextCancel()} - closeOnDimmerClick={false} - closeOnEscape={true} - disabled={this.state.ordinalFeatureToRank !== undefined} - > - Ordinal Feature Input - -

    For each ordinal feature, enter one comma-separated line with the following format (this overrides selections in the Dataset Preview):
    -  [feature name],[1st unique value],[2nd unique value],...

    -

    For example:
    -  month,jan,feb,mar,apr,may,jun,jul,aug,sep,oct,nov,dec
    -  day,mon,tue,wed,thu,fri,sat,sun

    -

    To populate this text box with all features and their unique values, close this window and use the button to set all feature types as ordinal.

    -
    -
    -
    - -
    - - } - open={this.state.catFeaturesUserTextModalOpen} - onOpen={() => this.setState({catFeaturesUserTextModalOpen: true})} - onClose={() => this.handleCatFeaturesUserTextCancel()} - closeOnDimmerClick={false} - closeOnEscape={true} - > - Categorical Feature Input - -

    Enter a comma-separated list to specify which features are Categorical.
    - This will override selections in the Dataset Preview.

    -

    For example:
    -  sex,eye_color,hair_color,disease_state -

    -

    Ranges - you can specify features using ranges. Each feature name in a range is converted to a column number within the data, - and the range is expanded using the column numbers. For example, working from the example above in which - we assume the features are present in the data in the same order as listed, entering
    -  sex-disease_state

    - would expand to
    -  sex,eye_color,hair_color,disease_state -

    -
    -
    -
    - -
    - -