21642: Fix tab state misalignment by oursourcing tab state management
authorStephen Smith <stephen@curii.com>
Thu, 18 Apr 2024 20:27:01 +0000 (16:27 -0400)
committerStephen Smith <stephen@curii.com>
Thu, 18 Apr 2024 20:27:01 +0000 (16:27 -0400)
ConditionalTabs component ensures no state misalignments by displaying tab
content by indexing into the same array of tabs that are given to the tab bar

By grouping tab and contents, we can simply remove inactive tabs before trying
to figure out which contents to show, making the logic much easier.

Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen@curii.com>

services/workbench2/src/components/conditional-tabs/conditional-tabs.tsx [new file with mode: 0644]
services/workbench2/src/views/process-panel/process-io-card.tsx

diff --git a/services/workbench2/src/components/conditional-tabs/conditional-tabs.tsx b/services/workbench2/src/components/conditional-tabs/conditional-tabs.tsx
new file mode 100644 (file)
index 0000000..74f2ecf
--- /dev/null
@@ -0,0 +1,44 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import React, { ReactNode, useEffect, useState } from "react";
+import { Tabs, Tab } from "@material-ui/core";
+import { TabsProps } from "@material-ui/core/Tabs";
+
+type TabData = {
+    show: boolean;
+    label: string;
+    content: ReactNode;
+};
+
+type ConditionalTabsProps = {
+    tabs: TabData[];
+};
+
+export const ConditionalTabs = (props: Omit<TabsProps, 'value' | 'onChange'> & ConditionalTabsProps) => {
+    const [tabState, setTabState] = useState(0);
+    const visibleTabs = props.tabs.filter(tab => tab.show);
+    const activeTab = visibleTabs[tabState];
+    const visibleTabNames = visibleTabs.map(tab => tab.label).join();
+
+    const handleTabChange = (event: React.MouseEvent<HTMLElement>, value: number) => {
+        setTabState(value);
+    };
+
+    // Reset tab to 0 when tab visibility changes
+    // (or if tab set change causes visible set to change)
+    useEffect(() => {
+        setTabState(0);
+    }, [visibleTabNames]);
+
+    return <>
+        <Tabs
+            {...props}
+            value={tabState}
+            onChange={handleTabChange} >
+            {visibleTabs.map(tab => <Tab label={tab.label} />)}
+        </Tabs>
+        {activeTab && activeTab.content}
+    </>;
+};
index 9fce7e83d4bd516cb5f9e1247fd320a7b0b09404..e05c383b1cb9a23d3b9726d7e370a823c3990587 100644 (file)
@@ -2,7 +2,7 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import React, { ReactElement, memo, useState } from "react";
+import React, { ReactElement, memo } from "react";
 import { Dispatch } from "redux";
 import {
     StyleRulesCallback,
@@ -14,8 +14,6 @@ import {
     CardContent,
     Tooltip,
     Typography,
-    Tabs,
-    Tab,
     Table,
     TableHead,
     TableBody,
@@ -70,6 +68,7 @@ import { KEEP_URL_REGEX } from "models/resource";
 import { FixedSizeList } from 'react-window';
 import AutoSizer from "react-virtualized-auto-sizer";
 import { LinkProps } from "@material-ui/core/Link";
+import { ConditionalTabs } from "components/conditional-tabs/conditional-tabs";
 
 type CssRules =
     | "card"
@@ -299,15 +298,6 @@ export const ProcessIOCard = withStyles(styles)(
             navigateTo,
             forceShowParams,
         }: ProcessIOCardProps) => {
-            const [mainProcTabState, setMainProcTabState] = useState(0);
-            const [subProcTabState, setSubProcTabState] = useState(0);
-            const handleMainProcTabChange = (event: React.MouseEvent<HTMLElement>, value: number) => {
-                setMainProcTabState(value);
-            };
-            const handleSubProcTabChange = (event: React.MouseEvent<HTMLElement>, value: number) => {
-                setSubProcTabState(value);
-            };
-
             const PanelIcon = label === ProcessIOCardType.INPUT ? InputIcon : OutputIcon;
             const mainProcess = !(process && process!.containerRequest.requestingContainerUuid);
             const showParamTable = mainProcess || forceShowParams;
@@ -403,54 +393,35 @@ export const ProcessIOCard = withStyles(styles)(
                                   *   Params when raw is provided by containerRequest properties but workflow mount is absent for preview
                                   */}
                                 {!loading && (hasRaw || hasParams) && (
-                                    <>
-                                        <Tabs
-                                            value={mainProcTabState}
-                                            onChange={handleMainProcTabChange}
-                                            variant="fullWidth"
-                                            className={classes.symmetricTabs}
-                                        >
-                                            {/* params will be empty on processes without workflow definitions in mounts, so we only show raw */}
-                                            {hasParams && <Tab label="Parameters" />}
-                                            {!forceShowParams && <Tab label="JSON" />}
-                                            {hasOutputCollecton && <Tab label="Collection" />}
-                                        </Tabs>
-                                        {mainProcTabState === 0 && params && hasParams && (
-                                            <div className={classes.tableWrapper}>
-                                                <ProcessIOPreview
-                                                    data={params}
-                                                    valueLabel={forceShowParams ? "Default value" : "Value"}
-                                                />
-                                            </div>
-                                        )}
-                                        {(mainProcTabState === 1 || !hasParams) && (
-                                            <div className={classes.jsonWrapper}>
-                                                <ProcessIORaw data={raw} />
-                                            </div>
-                                        )}
-                                        {mainProcTabState === 2 && hasOutputCollecton && (
-                                            <>
-                                                {outputUuid && (
-                                                    <Typography className={classes.collectionLink}>
-                                                        Output Collection:{" "}
-                                                        <MuiLink
-                                                            className={classes.keepLink}
-                                                            onClick={() => {
-                                                                navigateTo(outputUuid || "");
-                                                            }}
-                                                        >
-                                                            {outputUuid}
-                                                        </MuiLink>
-                                                    </Typography>
-                                                )}
-                                                <ProcessOutputCollectionFiles
-                                                    isWritable={false}
-                                                    currentItemUuid={outputUuid}
-                                                />
-                                            </>
-                                        )}
-
-                                    </>
+                                    <ConditionalTabs
+                                        variant="fullWidth"
+                                        className={classes.symmetricTabs}
+                                        tabs={[
+                                            {
+                                                // params will be empty on processes without workflow definitions in mounts, so we only show raw
+                                                show: hasParams,
+                                                label: "Parameters",
+                                                content: <div className={classes.tableWrapper}>
+                                                    <ProcessIOPreview
+                                                        data={params || []}
+                                                        valueLabel={forceShowParams ? "Default value" : "Value"}
+                                                    />
+                                                </div>,
+                                            },
+                                            {
+                                                show: !forceShowParams,
+                                                label: "JSON",
+                                                content: <div className={classes.jsonWrapper}>
+                                                    <ProcessIORaw data={raw} />
+                                                </div>,
+                                            },
+                                            {
+                                                show: hasOutputCollecton,
+                                                label: "Collection",
+                                                content: <ProcessOutputCollection outputUuid={outputUuid} />,
+                                            },
+                                        ]}
+                                    />
                                 )}
                                 {!loading && !hasRaw && !hasParams && (
                                     <Grid
@@ -476,47 +447,29 @@ export const ProcessIOCard = withStyles(styles)(
                                         <CircularProgress />
                                     </Grid>
                                 ) : !subProcessLoading && (hasInputMounts || hasOutputCollecton || isRawLoaded) ? (
-                                    <>
-                                        <Tabs
-                                            value={subProcTabState}
-                                            onChange={handleSubProcTabChange}
-                                            variant="fullWidth"
-                                            className={classes.symmetricTabs}
-                                        >
-                                            {hasInputMounts && <Tab label="Collections" />}
-                                            {hasOutputCollecton && <Tab label="Collection" />}
-                                            {isRawLoaded && <Tab label="JSON" />}
-                                        </Tabs>
-                                        {subProcTabState === 0 && hasInputMounts && <ProcessInputMounts mounts={mounts || []} />}
-                                        {subProcTabState === 0 && hasOutputCollecton && (
-                                            <div className={classes.tableWrapper}>
-                                                <>
-                                                    {outputUuid && (
-                                                        <Typography className={classes.collectionLink}>
-                                                            Output Collection:{" "}
-                                                            <MuiLink
-                                                                className={classes.keepLink}
-                                                                onClick={() => {
-                                                                    navigateTo(outputUuid || "");
-                                                                }}
-                                                            >
-                                                                {outputUuid}
-                                                            </MuiLink>
-                                                        </Typography>
-                                                    )}
-                                                    <ProcessOutputCollectionFiles
-                                                        isWritable={false}
-                                                        currentItemUuid={outputUuid}
-                                                    />
-                                                </>
-                                            </div>
-                                        )}
-                                        {isRawLoaded && (subProcTabState === 1 || (!hasInputMounts && !hasOutputCollecton)) && (
-                                            <div className={classes.jsonWrapper}>
-                                                <ProcessIORaw data={raw} />
-                                            </div>
-                                        )}
-                                    </>
+                                    <ConditionalTabs
+                                        variant="fullWidth"
+                                        className={classes.symmetricTabs}
+                                        tabs={[
+                                            {
+                                                show: hasInputMounts,
+                                                label: "Collections",
+                                                content: <ProcessInputMounts mounts={mounts || []} />,
+                                            },
+                                            {
+                                                show: hasOutputCollecton,
+                                                label: "Collection",
+                                                content: <ProcessOutputCollection outputUuid={outputUuid} />,
+                                            },
+                                            {
+                                                show: isRawLoaded,
+                                                label: "JSON",
+                                                content: <div className={classes.jsonWrapper}>
+                                                    <ProcessIORaw data={raw} />
+                                                </div>,
+                                            },
+                                        ]}
+                                    />
                                 ) : (
                                     <Grid
                                         container
@@ -709,6 +662,32 @@ const ProcessInputMounts = withStyles(styles)(
     ))
 );
 
+type ProcessOutputCollectionProps = {outputUuid: string | undefined} &  WithStyles<CssRules>;
+
+const ProcessOutputCollection = withStyles(styles)(({ outputUuid, classes }: ProcessOutputCollectionProps) => (
+    <div className={classes.tableWrapper}>
+        <>
+            {outputUuid && (
+                <Typography className={classes.collectionLink}>
+                    Output Collection:{" "}
+                    <MuiLink
+                        className={classes.keepLink}
+                        onClick={() => {
+                            navigateTo(outputUuid || "");
+                        }}
+                    >
+                        {outputUuid}
+                    </MuiLink>
+                </Typography>
+            )}
+            <ProcessOutputCollectionFiles
+                isWritable={false}
+                currentItemUuid={outputUuid}
+            />
+        </>
+    </div>
+));
+
 type FileWithSecondaryFiles = {
     secondaryFiles: File[];
 };