16068: Merge branch 'main' of git.arvados.org:arvados-workbench2 into 16068-merge...
authorStephen Smith <stephen@curii.com>
Tue, 19 Apr 2022 17:50:34 +0000 (13:50 -0400)
committerStephen Smith <stephen@curii.com>
Tue, 19 Apr 2022 18:18:37 +0000 (14:18 -0400)
Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen@curii.com>

src/common/custom-theme.ts
src/components/data-explorer/data-explorer.tsx
src/components/data-table/data-table.tsx
src/components/refresh-button/refresh-button.test.tsx
src/components/refresh-button/refresh-button.tsx
src/store/processes/process.ts
src/views-components/data-explorer/data-explorer.tsx
src/views-components/data-explorer/renderers.test.tsx
src/views-components/main-content-bar/main-content-bar.tsx

index b0703237af97f4c603dffdf42548302cb67a7eac..fc89a4ae216b2790385570f293b74c9ebd3145ba 100644 (file)
@@ -31,6 +31,7 @@ interface Colors {
     blue500: string;
     grey500: string;
     purple: string;
+    orange: string;
 }
 
 const arvadosPurple = '#361336';
@@ -53,7 +54,8 @@ export const themeOptions: ArvadosThemeOptions = {
             red900: red['900'],
             blue500: blue['500'],
             grey500: grey500,
-            purple: arvadosPurple
+            purple: arvadosPurple,
+            orange: '#f0ad4e',
         }
     },
     overrides: {
index 051f5d34a6ef45ebc33d1cbcbbab5e1815b05184..0363d33399c1dc90299fd2a73f78ca8d22cb6c0b 100644 (file)
@@ -66,6 +66,8 @@ interface DataExplorerDataProps<T> {
     contextMenuColumn: boolean;
     dataTableDefaultView?: React.ReactNode;
     working?: boolean;
+    currentRefresh?: string;
+    currentRoute?: string;
     hideColumnSelector?: boolean;
     paperProps?: PaperProps;
     actions?: React.ReactNode;
@@ -96,16 +98,55 @@ type DataExplorerProps<T> = DataExplorerDataProps<T> &
 
 export const DataExplorer = withStyles(styles)(
     class DataExplorerGeneric<T> extends React.Component<DataExplorerProps<T>> {
+        state = {
+            showLoading: false,
+            prevRefresh: '',
+            prevRoute: '',
+        };
+
+        componentDidUpdate(prevProps: DataExplorerProps<T>) {
+            const currentRefresh = this.props.currentRefresh || '';
+            const currentRoute = this.props.currentRoute || '';
+
+            if (currentRoute !== this.state.prevRoute) {
+                // Component already mounted, but the user comes from a route change,
+                // like browsing through a project hierarchy.
+                this.setState({
+                    showLoading: this.props.working,
+                    prevRoute: currentRoute,
+                });
+            }
+
+            if (currentRefresh !== this.state.prevRefresh) {
+                // Component already mounted, but the user just clicked the
+                // refresh button.
+                this.setState({
+                    showLoading: this.props.working,
+                    prevRefresh: currentRefresh,
+                });
+            }
+            if (this.state.showLoading && !this.props.working) {
+                this.setState({
+                    showLoading: false,
+                });
+            }
+        }
 
         componentDidMount() {
             if (this.props.onSetColumns) {
                 this.props.onSetColumns(this.props.columns);
             }
+            // Component just mounted, so we need to show the loading indicator.
+            this.setState({
+                showLoading: this.props.working,
+                prevRefresh: this.props.currentRefresh || '',
+                prevRoute: this.props.currentRoute || '',
+            });
         }
 
         render() {
             const {
-                columns, onContextMenu, onFiltersChange, onSortToggle, working, extractKey,
+                columns, onContextMenu, onFiltersChange, onSortToggle, extractKey,
                 rowsPerPage, rowsPerPageOptions, onColumnToggle, searchLabel, searchValue, onSearch,
                 items, itemsAvailable, onRowClick, onRowDoubleClick, classes,
                 dataTableDefaultView, hideColumnSelector, actions, paperProps, hideSearchInput,
@@ -113,8 +154,7 @@ export const DataExplorer = withStyles(styles)(
                 doHidePanel, doMaximizePanel, panelName, panelMaximized, elementPath
             } = this.props;
 
-            const dataCy = this.props["data-cy"];
-            return <Paper className={classes.root} {...paperProps} key={paperKey} data-cy={dataCy}>
+            return <Paper className={classes.root} {...paperProps} key={paperKey} data-cy={this.props["data-cy"]}>
                 <Grid container direction="column" wrap="nowrap" className={classes.container}>
                     <div>
                         {title && <Grid item xs className={classes.title}>{title}</Grid>}
@@ -156,7 +196,7 @@ export const DataExplorer = withStyles(styles)(
                     onFiltersChange={onFiltersChange}
                     onSortToggle={onSortToggle}
                     extractKey={extractKey}
-                    working={working}
+                    working={this.state.showLoading}
                     defaultView={dataTableDefaultView}
                     currentItemUuid={currentItemUuid}
                     currentRoute={paperKey} /></Grid>
index e8a6ce69aad0cd923f4e3c77d2385e5bb107f526..14dfdacaf9707acaba47283555edf80138750d16 100644 (file)
@@ -86,7 +86,7 @@ type DataTableProps<T> = DataTableDataProps<T> & WithStyles<CssRules>;
 export const DataTable = withStyles(styles)(
     class Component<T> extends React.Component<DataTableProps<T>> {
         render() {
-            const { items, classes } = this.props;
+            const { items, classes, working } = this.props;
             return <div className={classes.root}>
                 <div className={classes.content}>
                     <Table>
@@ -96,16 +96,16 @@ export const DataTable = withStyles(styles)(
                             </TableRow>
                         </TableHead>
                         <TableBody className={classes.tableBody}>
-                            { this.props.working !== undefined && !this.props.working && items.map(this.renderBodyRow) }
+                            { !working && items.map(this.renderBodyRow) }
                         </TableBody>
                     </Table>
-                    { this.props.working &&
+                    { !!working &&
                         <div className={classes.loader}>
                             <DataTableDefaultView
                                 icon={PendingIcon}
                                 messages={['Loading data, please wait.']} />
                         </div> }
-                    {items.length === 0 && this.props.working !== undefined && !this.props.working && this.renderNoItemsPlaceholder()}
+                    {items.length === 0 && !working && this.renderNoItemsPlaceholder()}
                 </div>
             </div>;
         }
index f9fa32d21fdc9cfc01e6fbaa1578f873cf601332..3a9292e6ff703c9130465c7578af6ef00b183c13 100644 (file)
@@ -6,7 +6,7 @@ import React from "react";
 import { Button } from "@material-ui/core";
 import { shallow, configure } from "enzyme";
 import Adapter from "enzyme-adapter-react-16";
-import { RefreshButton } from './refresh-button';
+import { LAST_REFRESH_TIMESTAMP, RefreshButton } from './refresh-button';
 
 configure({ adapter: new Adapter() });
 
@@ -31,6 +31,7 @@ describe('<RefreshButton />', () => {
     });
 
     it('should pass window location to router', () => {
+        expect(localStorage.getItem(LAST_REFRESH_TIMESTAMP)).toBeFalsy();
         // setup
         const wrapper = shallow(<RefreshButton {...props} />);
 
@@ -39,5 +40,6 @@ describe('<RefreshButton />', () => {
 
         // then
         expect(props.history.replace).toHaveBeenCalledWith('/');
+        expect(localStorage.getItem(LAST_REFRESH_TIMESTAMP)).not.toBeFalsy();
     });
 });
index 9971547bf092a5462b90f4db0e9ee66bb32f43a7..e2fe54846820d3efbaf744ce20a1f75d57f0cfec 100644 (file)
@@ -26,12 +26,17 @@ interface RefreshButtonProps {
     onClick?: () => void;
 }
 
+export const LAST_REFRESH_TIMESTAMP = 'lastRefreshTimestamp';
+
 export const RefreshButton = ({ history, classes, onClick }: RouteComponentProps & WithStyles<CssRules> & RefreshButtonProps) =>
     <Button
         color="primary"
         size="small"
         variant="contained"
         onClick={() => {
+            // Notify interested parties that the refresh button was clicked.
+            const now = (new Date()).getTime();
+            localStorage.setItem(LAST_REFRESH_TIMESTAMP, now.toString());
             history.replace(window.location.pathname);
             if (onClick) {
                 onClick();
index b72a0c2b10b9a280ad4a59c6167cd45d152ff520..19f30dd2b333d005096e7916faccb8f19e32f837 100644 (file)
@@ -82,6 +82,7 @@ export const getProcessStatusColor = (status: string, { customs }: ArvadosTheme)
         case ProcessStatus.WARNING:
             return customs.colors.yellow700;
         case ProcessStatus.FAILING:
+            return customs.colors.orange;
         case ProcessStatus.CANCELLED:
         case ProcessStatus.FAILED:
             return customs.colors.red900;
index 900ab94e0cd00ee02e5566934ea95097feafaa73..06d97038e759c96712502ab52f6e9c80ba2af3c1 100644 (file)
@@ -11,6 +11,7 @@ import { dataExplorerActions } from "store/data-explorer/data-explorer-action";
 import { DataColumn } from "components/data-table/data-column";
 import { DataColumns } from "components/data-table/data-table";
 import { DataTableFilters } from 'components/data-table-filters/data-table-filters-tree';
+import { LAST_REFRESH_TIMESTAMP } from "components/refresh-button/refresh-button";
 
 interface Props {
     id: string;
@@ -18,30 +19,23 @@ interface Props {
     onContextMenu?: (event: React.MouseEvent<HTMLElement>, item: any, isAdmin?: boolean) => void;
     onRowDoubleClick: (item: any) => void;
     extractKey?: (item: any) => React.Key;
-    working?: boolean;
 }
 
-let data: any[] = [];
-let href: string = '';
-
 const mapStateToProps = (state: RootState, { id }: Props) => {
     const progress = state.progressIndicator.find(p => p.id === id);
     const dataExplorerState = getDataExplorer(state.dataExplorer, id);
     const currentRoute = state.router.location ? state.router.location.pathname : '';
+    const currentRefresh = localStorage.getItem(LAST_REFRESH_TIMESTAMP) || '';
     const currentItemUuid = currentRoute === '/workflows' ? state.properties.workflowPanelDetailsUuid : state.detailsPanel.resourceUuid;
 
-    let loading = false;
-
-    if (dataExplorerState.items.length > 0 && data === dataExplorerState.items && href !== window.location.href) {
-        loading = true
-    } else {
-        href = window.location.href;
-        data = dataExplorerState.items;
-    }
-
-    const working = (progress && progress.working) || loading;
-
-    return { ...dataExplorerState, working, paperKey: currentRoute, currentItemUuid };
+    return {
+        ...dataExplorerState,
+        working: !!progress?.working,
+        currentRefresh: currentRefresh,
+        currentRoute: currentRoute,
+        paperKey: currentRoute,
+        currentItemUuid
+    };
 };
 
 const mapDispatchToProps = () => {
@@ -86,5 +80,5 @@ const mapDispatchToProps = () => {
     });
 };
 
-export const DataExplorer = connect(mapStateToProps, mapDispatchToProps())(DataExplorerComponent);
+export const DataExplorer = connect(mapStateToProps, mapDispatchToProps)(DataExplorerComponent);
 
index 229d99094ed36ed7fd5500fc0607cbfc4092a102..5bc123df60016f041aef06a78a597557ea100757 100644 (file)
@@ -33,6 +33,7 @@ describe('renderers', () => {
                         green700: 'rgb(0, 255, 0)',
                         yellow700: 'rgb(255, 255, 0)',
                         red900: 'rgb(255, 0, 0)',
+                        orange: 'rgb(240, 173, 78)',
                         grey500: 'rgb(128, 128, 128)',
                     }
                 },
@@ -50,7 +51,7 @@ describe('renderers', () => {
         [
             // CR Status ; Priority ; C Status ; Exit Code ; C RuntimeStatus ; Expected label ; Expected Color
             [CR.COMMITTED, 1, C.RUNNING, null, {}, PS.RUNNING, props.theme.customs.colors.blue500],
-            [CR.COMMITTED, 1, C.RUNNING, null, {error: 'whoops'}, PS.FAILING, props.theme.customs.colors.red900],
+            [CR.COMMITTED, 1, C.RUNNING, null, {error: 'whoops'}, PS.FAILING, props.theme.customs.colors.orange],
             [CR.COMMITTED, 1, C.RUNNING, null, {warning: 'watch out!'}, PS.WARNING, props.theme.customs.colors.yellow700],
             [CR.FINAL, 1, C.CANCELLED, null, {}, PS.CANCELLED, props.theme.customs.colors.red900],
             [CR.FINAL, 1, C.COMPLETE, 137, {}, PS.FAILED, props.theme.customs.colors.red900],
index a460a51800c7d00ee929a82258a6e9a8eea2c3fa..271d77c1085319854c8edec9d887ff0968bc233c 100644 (file)
@@ -13,6 +13,7 @@ import * as Routes from 'routes/routes';
 import { toggleDetailsPanel } from 'store/details-panel/details-panel-action';
 import RefreshButton from "components/refresh-button/refresh-button";
 import { loadSidePanelTreeProjects } from "store/side-panel-tree/side-panel-tree-actions";
+import { Dispatch } from "redux";
 
 type CssRules = "infoTooltip";
 
@@ -44,46 +45,40 @@ const isButtonVisible = ({ router }: RootState) => {
         Routes.matchAllProcessesRoute(pathname) ||
         Routes.matchTrashRoute(pathname) ||
         Routes.matchFavoritesRoute(pathname);
-
-    /* return !Routes.matchWorkflowRoute(pathname) && !Routes.matchUserVirtualMachineRoute(pathname) &&
-     *     !Routes.matchAdminVirtualMachineRoute(pathname) && !Routes.matchRepositoriesRoute(pathname) &&
-     *     !Routes.matchSshKeysAdminRoute(pathname) && !Routes.matchSshKeysUserRoute(pathname) &&
-     *     !Routes.matchSiteManagerRoute(pathname) &&
-     *     !Routes.matchKeepServicesRoute(pathname) && !Routes.matchComputeNodesRoute(pathname) &&
-     *     !Routes.matchApiClientAuthorizationsRoute(pathname) && !Routes.matchUsersRoute(pathname) &&
-     *     !Routes.matchMyAccountRoute(pathname) && !Routes.matchLinksRoute(pathname); */
 };
 
-export const MainContentBar =
-    connect((state: RootState) => ({
-        buttonVisible: isButtonVisible(state),
-        projectUuid: state.detailsPanel.resourceUuid,
-    }), (dispatch) => ({
-            onDetailsPanelToggle: () => dispatch<any>(toggleDetailsPanel()),
-            onRefreshButtonClick: (id) => {
-                dispatch<any>(loadSidePanelTreeProjects(id));
-            }
-        }))(
-            withStyles(styles)(
-                (props: MainContentBarProps & WithStyles<CssRules> & any) =>
-                    <Toolbar>
-                        <Grid container>
-                            <Grid container item xs alignItems="center">
-                                <Breadcrumbs />
-                            </Grid>
-                            <Grid item>
-                                <RefreshButton onClick={() => {
-                                    props.onRefreshButtonClick(props.projectUuid);
-                                }} />
-                            </Grid>
-                            <Grid item>
-                                {props.buttonVisible && <Tooltip title="Additional Info">
-                                    <IconButton data-cy="additional-info-icon" color="inherit" className={props.classes.infoTooltip} onClick={props.onDetailsPanelToggle}>
-                                        <DetailsIcon />
-                                    </IconButton>
-                                </Tooltip>}
-                            </Grid>
-                        </Grid>
-                    </Toolbar>
-            )
-        );
+const mapStateToProps = (state: RootState) => ({
+    buttonVisible: isButtonVisible(state),
+    projectUuid: state.detailsPanel.resourceUuid,
+});
+
+const mapDispatchToProps = () => (dispatch: Dispatch) => ({
+    onDetailsPanelToggle: () => dispatch<any>(toggleDetailsPanel()),
+    onRefreshButtonClick: (id) => {
+        dispatch<any>(loadSidePanelTreeProjects(id));
+    }
+});
+
+export const MainContentBar = connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(
+    (props: MainContentBarProps & WithStyles<CssRules> & any) =>
+        <Toolbar><Grid container>
+            <Grid container item xs alignItems="center">
+                <Breadcrumbs />
+            </Grid>
+            <Grid item>
+                <RefreshButton onClick={() => {
+                    props.onRefreshButtonClick(props.projectUuid);
+                }} />
+            </Grid>
+            <Grid item>
+                {props.buttonVisible && <Tooltip title="Additional Info">
+                    <IconButton data-cy="additional-info-icon"
+                        color="inherit"
+                        className={props.classes.infoTooltip}
+                        onClick={props.onDetailsPanelToggle}>
+                        <DetailsIcon />
+                    </IconButton>
+                </Tooltip>}
+            </Grid>
+        </Grid></Toolbar>
+));