Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new analysis button to all analyses component in map #459

Merged
merged 21 commits into from
Oct 19, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
003ff2b
Add new analysis button to all analyses component in map
chowington Aug 30, 2023
7e4ca66
Merge branch 'main' into 425-add-new-analysis-button-in-map
chowington Aug 30, 2023
5a9d838
Clean up
chowington Aug 30, 2023
7ab90e6
Move the timeout before redirecting to the dialog component
chowington Aug 30, 2023
fb1d385
Delete comment
chowington Aug 30, 2023
c723caf
Merge branch 'main' into 425-add-new-analysis-button-in-map
chowington Sep 7, 2023
8e3e876
Use subsettingclient from props instead of hook
chowington Sep 7, 2023
966c84b
Merge branch 'main' into 425-add-new-analysis-button-in-map
chowington Sep 11, 2023
31293ab
Render button in panel instead of AllAnalyses
chowington Sep 12, 2023
57ff3f6
Remove remaining new analysis logic from AllAnalyses
chowington Sep 12, 2023
0be83b0
Clean up MapAnalysis
chowington Sep 12, 2023
e26be00
More cleanup
chowington Sep 12, 2023
d06f2da
Merge branch 'main' into 425-add-new-analysis-button-in-map
chowington Sep 12, 2023
e637576
Merge branch 'main' into 425-add-new-analysis-button-in-map
chowington Sep 18, 2023
309e786
Center plus icon in button
chowington Sep 19, 2023
768391c
Merge branch 'main' into 425-add-new-analysis-button-in-map
chowington Sep 25, 2023
df07727
WIP fix for map not rendering new analysis
chowington Sep 25, 2023
8071c9f
Merge branch 'main' into 425-add-new-analysis-button-in-map
chowington Oct 2, 2023
371ca5b
Fix failing to rerender on new analysis bug
chowington Oct 2, 2023
5f9188e
Merge branch 'main' into 425-add-new-analysis-button-in-map
chowington Oct 19, 2023
644b302
Remove uppercase textTransform on button
chowington Oct 19, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1014,13 +1014,13 @@ function MapAnalysisImpl(props: ImplProps) {
>
<AllAnalyses
analysisClient={analysisClient}
activeAnalysisId={
analysisState={
isSavedAnalysis(analysisState.analysis)
? analysisState.analysis.analysisId
? analysisState
: undefined
}
subsettingClient={subsettingClient}
studyId={getStudyId(studyRecord)}
studyRecord={studyRecord}
showLoginForm={showLoginForm}
/>
</div>
Expand Down
194 changes: 125 additions & 69 deletions packages/libs/eda/src/lib/workspace/AllAnalyses.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { orderBy } from 'lodash';
import Path from 'path';
import { Link, useHistory, useLocation } from 'react-router-dom';
import { Link, useHistory, useLocation, useRouteMatch } from 'react-router-dom';

import {
Button,
Expand Down Expand Up @@ -35,21 +35,27 @@ import { OverflowingTextCell } from '@veupathdb/wdk-client/lib/Views/Strategy/Ov

import {
AnalysisClient,
AnalysisState,
AnalysisSummary,
DEFAULT_ANALYSIS_NAME,
StudyRecord,
useAnalysisList,
usePinnedAnalyses,
} from '../core';
import SubsettingClient from '../core/api/SubsettingClient';
import { getAnalysisId } from '../core/utils/analysis';
import { useDebounce } from '../core/hooks/debouncing';
import { useWdkStudyRecords } from '../core/hooks/study';
import {
ANALYSIS_NAME_MAX_LENGTH,
makeCurrentProvenanceString,
makeOnImportProvenanceString,
} from '../core/utils/analysis';
import { AnalysisNameDialog } from './AnalysisNameDialog';
import { convertISOToDisplayFormat } from '../core/utils/date-conversion';
import ShareFromAnalysesList from './sharing/ShareFromAnalysesList';
import { Checkbox, Toggle, colors } from '@veupathdb/coreui';
import { getStudyId } from '@veupathdb/study-data-access/lib/shared/studies';

interface AnalysisAndDataset {
analysis: AnalysisSummary & {
Expand All @@ -68,14 +74,15 @@ interface Props {
exampleAnalysesAuthor?: number;
/**
* When provided, the table is filtered to the study,
* and the study column is not displayed.
* and the study column is not displayed. This, along with analysisState, is
* necessary for a new analysis button to be shown
*/
studyId?: string | null;
studyRecord?: StudyRecord;
/**
* If the analysis with this ID is displayed,
* indicate it is "active"
* If there is an active analysis, indicate the active analysis in the table
* and show a new analysis button if studyRecord is also provided
*/
activeAnalysisId?: string;
analysisState?: AnalysisState;
/**
* Determines if the search term is stored as a query
* param in the url
Expand Down Expand Up @@ -111,15 +118,30 @@ export function AllAnalyses(props: Props) {
analysisClient,
exampleAnalysesAuthor,
showLoginForm,
studyId,
studyRecord,
synchronizeWithUrl,
updateDocumentTitle,
activeAnalysisId,
analysisState,
} = props;
const user = useWdkService((wdkService) => wdkService.getCurrentUser(), []);
const history = useHistory();
const location = useLocation();
const classes = useStyles();
const [isAnalysisNameDialogOpen, setIsAnalysisNameDialogOpen] =
useState(false);
const analysis = analysisState?.analysis;
const activeAnalysisId = getAnalysisId(analysis);
const studyId = studyRecord ? getStudyId(studyRecord) : null;
const { url } = useRouteMatch();
const redirectURL = studyId
? url.endsWith(studyId)
? `/workspace/${url}/new`
: Path.resolve(url, '../new')
: null;
const redirectToNewAnalysis = useCallback(
() => (redirectURL ? history.push(redirectURL) : null),
[history, redirectURL]
);

const searchTextQueryParam = useMemo(() => {
if (!synchronizeWithUrl) return '';
Expand Down Expand Up @@ -320,6 +342,24 @@ export function AllAnalyses(props: Props) {
selectedAnalyses.has(analysis.analysisId),
},
actions: [
{
element:
activeAnalysisId && redirectToNewAnalysis ? (
<Button
type="button"
startIcon={<Icon color="action" className="fa fa-plus" />}
onClick={
analysis && analysis.displayName === DEFAULT_ANALYSIS_NAME
? () => setIsAnalysisNameDialogOpen(true)
: redirectToNewAnalysis
}
>
Create new analysis
</Button>
) : (
<></>
),
},
{
element: (
<Button
Expand Down Expand Up @@ -640,73 +680,89 @@ export function AllAnalyses(props: Props) {
exampleAnalysesAuthor,
user,
studyId,
activeAnalysisId,
analysis,
redirectToNewAnalysis,
]
);

useSetDocumentTitle(updateDocumentTitle ? 'My Analyses' : document.title);

return (
<div className={classes.root}>
<ShareFromAnalysesList
analysis={
analysesAndDatasets?.find(
(potentialMatch) =>
potentialMatch.analysis.analysisId === selectedAnalysisId
)?.analysis
}
updateAnalysis={updateAnalysis}
visible={sharingModalVisible}
toggleVisible={setSharingModalVisible}
showLoginForm={showLoginForm}
/>

<h1>My Analyses</h1>
{(loading || datasets == null || analyses == null || user == null) && (
<Loading style={{ position: 'absolute', left: '50%', top: '1em' }} />
)}
{error && <ContentError>{error}</ContentError>}
{analyses && datasets && user ? (
<Mesa.Mesa state={tableState}>
<div
style={{
display: 'flex',
alignItems: 'center',
gap: '1ex',
}}
>
<TextField
variant="outlined"
size="small"
label="Search analyses"
inputProps={{ size: 50 }}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
aria-label="Clear search text"
onClick={() => setSearchText('')}
style={{
visibility:
debouncedSearchText.length > 0 ? 'visible' : 'hidden',
}}
edge="end"
size="small"
>
<CloseIcon />
</IconButton>
</InputAdornment>
),
<>
<div className={classes.root}>
<ShareFromAnalysesList
analysis={
analysesAndDatasets?.find(
(potentialMatch) =>
potentialMatch.analysis.analysisId === selectedAnalysisId
)?.analysis
}
updateAnalysis={updateAnalysis}
visible={sharingModalVisible}
toggleVisible={setSharingModalVisible}
showLoginForm={showLoginForm}
/>

<h1>My Analyses</h1>
{(loading || datasets == null || analyses == null || user == null) && (
<Loading style={{ position: 'absolute', left: '50%', top: '1em' }} />
)}
{error && <ContentError>{error}</ContentError>}
{analyses && datasets && user ? (
<Mesa.Mesa state={tableState}>
<div
style={{
display: 'flex',
alignItems: 'center',
gap: '1ex',
}}
value={searchText}
onChange={onFilterFieldChange}
/>
<span>
Showing {filteredAnalysesAndDatasets?.length} of {analyses.length}{' '}
analyses
</span>
</div>
</Mesa.Mesa>
) : null}
</div>
>
<TextField
variant="outlined"
size="small"
label="Search analyses"
inputProps={{ size: 50 }}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
aria-label="Clear search text"
onClick={() => setSearchText('')}
style={{
visibility:
debouncedSearchText.length > 0
? 'visible'
: 'hidden',
}}
edge="end"
size="small"
>
<CloseIcon />
</IconButton>
</InputAdornment>
),
}}
value={searchText}
onChange={onFilterFieldChange}
/>
<span>
Showing {filteredAnalysesAndDatasets?.length} of{' '}
{analyses.length} analyses
</span>
</div>
</Mesa.Mesa>
) : null}
</div>
{analysis && (
<AnalysisNameDialog
isOpen={isAnalysisNameDialogOpen}
setIsOpen={setIsAnalysisNameDialogOpen}
initialAnalysisName={analysis.displayName}
setAnalysisName={analysisState.setName}
redirectToNewAnalysis={redirectToNewAnalysis}
/>
)}
</>
);
}
30 changes: 20 additions & 10 deletions packages/libs/eda/src/lib/workspace/AnalysisNameDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,17 @@ export function AnalysisNameDialog({
redirectToNewAnalysis,
}: AnalysisNameDialogProps) {
const [inputText, setInputText] = useState(initialAnalysisName);
const [continueText, setContinueText] =
useState<'Continue' | 'Rename and continue'>('Continue');
const [nameIsValid, setNameIsValid] = useState(true);
const [disableButtons, setDisableButtons] = useState(false);

const handleTextChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const newText = event.target.value;
setInputText(newText);
setContinueText(
newText === initialAnalysisName ? 'Continue' : 'Rename and continue'
);
// Currently the only requirement is no empty name
newText.length > 0 ? setNameIsValid(true) : setNameIsValid(false);
};
Expand All @@ -44,11 +50,12 @@ export function AnalysisNameDialog({
setInputText(initialAnalysisName);
};

const handleContinue = async () => {
// TypeScript says this `await` has no effect, but it seems to be required
// for this function to finish before the page redirect
await setAnalysisName(inputText);
redirectToNewAnalysis();
const handleContinue = () => {
setDisableButtons(true);
setAnalysisName(inputText);
// The timeout for saving an analysis is 1 second,
// so wait a bit longer than that
setTimeout(redirectToNewAnalysis, 1200);
};

return (
Expand All @@ -74,20 +81,23 @@ export function AnalysisNameDialog({
error={!nameIsValid}
// Currently the only requirement is no empty name
helperText={nameIsValid ? ' ' : 'Name must not be blank'}
disabled={disableButtons}
/>
</DialogContent>
<DialogActions>
<Button onClick={handleCancel} color="secondary">
<Button
onClick={handleCancel}
color="secondary"
disabled={disableButtons}
>
Cancel
</Button>
<Button
onClick={handleContinue}
color="primary"
disabled={!nameIsValid}
disabled={!nameIsValid || disableButtons}
>
{inputText === initialAnalysisName
? 'Continue'
: 'Rename and continue'}
{continueText}
</Button>
</DialogActions>
</Dialog>
Expand Down