[arvados] created: 2.7.0-6103-ga50af999ab

git repository hosting git at public.arvados.org
Fri Mar 1 20:09:38 UTC 2024


        at  a50af999abab5d1937b43437fa160d6c6a36a438 (commit)


commit a50af999abab5d1937b43437fa160d6c6a36a438
Author: Stephen Smith <stephen at curii.com>
Date:   Fri Mar 1 15:08:30 2024 -0500

    21221: Add unit tests for groups panel middleware to verify loading
    itemsAvailable into store
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/services/workbench2/src/store/groups-panel/groups-panel-middleware-service.test.ts b/services/workbench2/src/store/groups-panel/groups-panel-middleware-service.test.ts
new file mode 100644
index 0000000000..810bc74bdf
--- /dev/null
+++ b/services/workbench2/src/store/groups-panel/groups-panel-middleware-service.test.ts
@@ -0,0 +1,104 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import Axios, { AxiosInstance, AxiosResponse } from "axios";
+import { mockConfig } from "common/config";
+import { createBrowserHistory } from "history";
+import { GroupsPanelMiddlewareService } from "./groups-panel-middleware-service";
+import { dataExplorerMiddleware } from "store/data-explorer/data-explorer-middleware";
+import { Dispatch, MiddlewareAPI } from "redux";
+import { DataColumns } from "components/data-table/data-table";
+import { dataExplorerActions } from "store/data-explorer/data-explorer-action";
+import { SortDirection } from "components/data-table/data-column";
+import { createTree } from 'models/tree';
+import { DataTableFilterItem } from "components/data-table-filters/data-table-filters-tree";
+import { GROUPS_PANEL_ID } from "./groups-panel-actions";
+import { RootState, RootStore, configureStore } from "store/store";
+import { ServiceRepository, createServices } from "services/services";
+import { ApiActions } from "services/api/api-actions";
+import { ListResults } from "services/common-service/common-service";
+import { GroupResource } from "models/group";
+import { getResource } from "store/resources/resources";
+
+describe("GroupsPanelMiddlewareService", () => {
+    let axiosInst: AxiosInstance;
+    let store: RootStore;
+    let services: ServiceRepository;
+    const config: any = {};
+    const actions: ApiActions = {
+        progressFn: (id: string, working: boolean) => { },
+        errorFn: (id: string, message: string) => { }
+    };
+
+    beforeEach(() => {
+        axiosInst = Axios.create({ headers: {} });
+        services = createServices(mockConfig({}), actions, axiosInst);
+        store = configureStore(createBrowserHistory(), services, config);
+    });
+
+    it("requests group member counts and updates resource store", async () => {
+        // Given
+        const fakeUuid = "zzzzz-j7d0g-000000000000000";
+        axiosInst.get = jest.fn((url: string) => {
+            if (url === '/groups') {
+                return Promise.resolve(
+                    { data: {
+                        kind: "",
+                        offset: 0,
+                        limit: 100,
+                        items: [{
+                            can_manage: true,
+                            can_write: true,
+                            created_at: "2023-11-15T20:57:01.723043000Z",
+                            delete_at: null,
+                            description: null,
+                            etag: "0000000000000000000000000",
+                            frozen_by_uuid: null,
+                            group_class: "role",
+                            href: `/groups/${fakeUuid}`,
+                            is_trashed: false,
+                            kind: "arvados#group",
+                            modified_at: "2023-11-15T20:57:01.719986000Z",
+                            modified_by_client_uuid: null,
+                            modified_by_user_uuid: "zzzzz-tpzed-000000000000000",
+                            name: "Test Group",
+                            owner_uuid: "zzzzz-tpzed-000000000000000",
+                            properties: {},
+                            trash_at: null,
+                            uuid: fakeUuid,
+                            writable_by: [
+                                "zzzzz-tpzed-000000000000000",
+                            ]
+                        }],
+                        items_available: 1,
+                    }} as AxiosResponse);
+            } else if (url === '/links') {
+                return Promise.resolve(
+                    { data: {
+                        items: [],
+                        items_available: 234,
+                        kind: "arvados#linkList",
+                        limit: 0,
+                        offset: 0
+                    }} as AxiosResponse);
+            } else {
+                return Promise.resolve(
+                    { data: {}} as AxiosResponse);
+            }
+        });
+
+        // When
+        await store.dispatch(dataExplorerActions.REQUEST_ITEMS({id: GROUPS_PANEL_ID}));
+        // Wait for async fetching of group count promises to resolve
+        await new Promise(setImmediate);
+
+        // Expect
+        expect(axiosInst.get).toHaveBeenCalledTimes(2);
+        expect(axiosInst.get).toHaveBeenCalledWith('/groups', expect.anything());
+        expect(axiosInst.get).toHaveBeenCalledWith('/links', expect.anything());
+        const group = getResource<GroupResource>(fakeUuid)(store.getState().resources);
+        expect(group?.memberCount).toBe(234);
+    });
+
+});

commit f61c01afea5fb78aa026f3e4270156a20c9eeb5a
Author: Stephen Smith <stephen at curii.com>
Date:   Fri Mar 1 15:08:11 2024 -0500

    21221: Add unit tests for GroupMembersCount renderer with loading indicator
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/services/workbench2/src/views-components/data-explorer/renderers.test.tsx b/services/workbench2/src/views-components/data-explorer/renderers.test.tsx
index ac8729aa3d..eb33d12301 100644
--- a/services/workbench2/src/views-components/data-explorer/renderers.test.tsx
+++ b/services/workbench2/src/views-components/data-explorer/renderers.test.tsx
@@ -4,7 +4,7 @@
 
 import React from 'react';
 import { mount, configure } from 'enzyme';
-import { ProcessStatus, ResourceFileSize } from './renderers';
+import { GroupMembersCount, ProcessStatus, ResourceFileSize } from './renderers';
 import Adapter from "enzyme-adapter-react-16";
 import { Provider } from 'react-redux';
 import configureMockStore from 'redux-mock-store'
@@ -12,6 +12,10 @@ import { ResourceKind } from '../../models/resource';
 import { ContainerRequestState as CR } from '../../models/container-request';
 import { ContainerState as C } from '../../models/container';
 import { ProcessStatus as PS } from '../../store/processes/process';
+import { MuiThemeProvider } from '@material-ui/core';
+import { CustomTheme } from 'common/custom-theme';
+import { InlinePulser} from 'components/loading/inline-pulser';
+import { ErrorIcon } from "components/icon/icon";
 
 const middlewares = [];
 const mockStore = configureMockStore(middlewares);
@@ -19,7 +23,7 @@ const mockStore = configureMockStore(middlewares);
 configure({ adapter: new Adapter() });
 
 describe('renderers', () => {
-    let props = null;
+    let props: any = null;
 
     describe('ProcessStatus', () => {
         props = {
@@ -161,4 +165,90 @@ describe('renderers', () => {
             expect(wrapper2.text()).toContain('');
         });
     });
+
+    describe('GroupMembersCount', () => {
+        let fakeGroup;
+        beforeEach(() => {
+            props = {
+                uuid: 'zzzzz-j7d0g-000000000000000',
+            };
+            fakeGroup = {
+                "canManage": true,
+                "canWrite": true,
+                "createdAt": "2020-09-24T22:52:57.546521000Z",
+                "deleteAt": null,
+                "description": "Test Group",
+                "etag": "0000000000000000000000000",
+                "frozenByUuid": null,
+                "groupClass": "role",
+                "href": `/groups/${props.uuid}`,
+                "isTrashed": false,
+                "kind": ResourceKind.GROUP,
+                "modifiedAt": "2020-09-24T22:52:57.545669000Z",
+                "modifiedByClientUuid": null,
+                "modifiedByUserUuid": "zzzzz-tpzed-000000000000000",
+                "name": "System group",
+                "ownerUuid": "zzzzz-tpzed-000000000000000",
+                "properties": {},
+                "trashAt": null,
+                "uuid": props.uuid,
+                "writableBy": [
+                    "zzzzz-tpzed-000000000000000",
+                ]
+            };
+        });
+
+        it('shows loading group count when no memberCount', () => {
+            // Given
+            const store = mockStore({resources: {
+                [props.uuid]: fakeGroup,
+            }});
+
+            const wrapper = mount(<Provider store={store}>
+                <MuiThemeProvider theme={CustomTheme}>
+                    <GroupMembersCount {...props} />
+                </MuiThemeProvider>
+            </Provider>);
+
+            expect(wrapper.find(InlinePulser)).toHaveLength(1);
+        });
+
+        it('shows group count when memberCount present', () => {
+            // Given
+            const store = mockStore({resources: {
+                [props.uuid]: {
+                    ...fakeGroup,
+                    "memberCount": 765,
+                }
+            }});
+
+            const wrapper = mount(<Provider store={store}>
+                <MuiThemeProvider theme={CustomTheme}>
+                    <GroupMembersCount {...props} />
+                </MuiThemeProvider>
+            </Provider>);
+
+            expect(wrapper.text()).toBe("765");
+        });
+
+        it('shows group count error icon when memberCount is null', () => {
+            // Given
+            const store = mockStore({resources: {
+                [props.uuid]: {
+                    ...fakeGroup,
+                    "memberCount": null,
+                }
+            }});
+
+            const wrapper = mount(<Provider store={store}>
+                <MuiThemeProvider theme={CustomTheme}>
+                    <GroupMembersCount {...props} />
+                </MuiThemeProvider>
+            </Provider>);
+
+            expect(wrapper.find(ErrorIcon)).toHaveLength(1);
+        });
+
+    });
+
 });

commit a56e6e6db2fa45008f9a3a1023f91207ea9f4f9a
Author: Stephen Smith <stephen at curii.com>
Date:   Fri Mar 1 15:05:53 2024 -0500

    21221: Use div wrapper in GroupMembersCount loading typography to quiet warning
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/services/workbench2/src/views-components/data-explorer/renderers.tsx b/services/workbench2/src/views-components/data-explorer/renderers.tsx
index 101e6edad0..4ecbc7e10b 100644
--- a/services/workbench2/src/views-components/data-explorer/renderers.tsx
+++ b/services/workbench2/src/views-components/data-explorer/renderers.tsx
@@ -1150,7 +1150,7 @@ export const GroupMembersCount = connect(
 )(withTheme()((props: {value: number | null | undefined, theme:ArvadosTheme}) => {
     if (props.value === undefined) {
         // Loading
-        return <Typography>
+        return <Typography component={"div"}>
             <InlinePulser />
         </Typography>;
     } else if (props.value === null) {

commit 92430a1fcb52c2fc688dae5a40f6eb086455008a
Author: Stephen Smith <stephen at curii.com>
Date:   Fri Mar 1 15:05:12 2024 -0500

    21221: Move GroupMembersCount to renderers for easier testing
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/services/workbench2/src/views-components/data-explorer/renderers.tsx b/services/workbench2/src/views-components/data-explorer/renderers.tsx
index 56926b513d..101e6edad0 100644
--- a/services/workbench2/src/views-components/data-explorer/renderers.tsx
+++ b/services/workbench2/src/views-components/data-explorer/renderers.tsx
@@ -3,7 +3,7 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import React from "react";
-import { Grid, Typography, withStyles, Tooltip, IconButton, Checkbox, Chip } from "@material-ui/core";
+import { Grid, Typography, withStyles, Tooltip, IconButton, Checkbox, Chip, withTheme } from "@material-ui/core";
 import { FavoriteStar, PublicFavoriteStar } from "../favorite-star/favorite-star";
 import { Resource, ResourceKind, TrashableResource } from "models/resource";
 import {
@@ -21,6 +21,7 @@ import {
     ActiveIcon,
     SetupIcon,
     InactiveIcon,
+    ErrorIcon,
 } from "components/icon/icon";
 import { formatDate, formatFileSize, formatTime } from "common/formatters";
 import { resourceLabel } from "common/labels";
@@ -53,6 +54,7 @@ import { VirtualMachinesResource } from "models/virtual-machines";
 import { CopyToClipboardSnackbar } from "components/copy-to-clipboard-snackbar/copy-to-clipboard-snackbar";
 import { ProjectResource } from "models/project";
 import { ProcessResource } from "models/process";
+import { InlinePulser } from "components/loading/inline-pulser";
 
 const renderName = (dispatch: Dispatch, item: GroupContentsResource) => {
     const navFunc = "groupClass" in item && item.groupClass === GroupClass.ROLE ? navigateToGroupDetails : navigateTo;
@@ -526,9 +528,9 @@ const renderResourceLink = (dispatch: Dispatch, item: Resource ) => {
             onClick={() => {
                 item.kind === ResourceKind.GROUP && (item as GroupResource).groupClass === "role"
                     ? dispatch<any>(navigateToGroupDetails(item.uuid))
-                    : item.kind === ResourceKind.USER 
+                    : item.kind === ResourceKind.USER
                     ? dispatch<any>(navigateToUserProfile(item.uuid))
-                    : dispatch<any>(navigateTo(item.uuid)); 
+                    : dispatch<any>(navigateTo(item.uuid));
             }}
         >
             {resourceLabel(item.kind, item && item.kind === ResourceKind.GROUP ? (item as GroupResource).groupClass || "" : "")}:{" "}
@@ -1135,3 +1137,30 @@ export const ContainerRunTime = connect((state: RootState, props: { uuid: string
         }
     }
 );
+
+export const GroupMembersCount = connect(
+    (state: RootState, props: { uuid: string }) => {
+        const group = getResource<GroupResource>(props.uuid)(state.resources);
+
+        return {
+            value: group?.memberCount,
+        };
+
+    }
+)(withTheme()((props: {value: number | null | undefined, theme:ArvadosTheme}) => {
+    if (props.value === undefined) {
+        // Loading
+        return <Typography>
+            <InlinePulser />
+        </Typography>;
+    } else if (props.value === null) {
+        // Error
+        return <Typography>
+            <Tooltip title="Failed to load member count">
+                <ErrorIcon style={{color: props.theme.customs.colors.greyL}}/>
+            </Tooltip>
+        </Typography>;
+    } else {
+        return <Typography children={props.value} />;
+    }
+}));
diff --git a/services/workbench2/src/views/groups-panel/groups-panel.tsx b/services/workbench2/src/views/groups-panel/groups-panel.tsx
index 96a2c3697d..86c85b5c97 100644
--- a/services/workbench2/src/views/groups-panel/groups-panel.tsx
+++ b/services/workbench2/src/views/groups-panel/groups-panel.tsx
@@ -4,12 +4,12 @@
 
 import React from 'react';
 import { connect } from 'react-redux';
-import { Grid, Button, Typography, StyleRulesCallback, WithStyles, withStyles, withTheme, Tooltip } from "@material-ui/core";
+import { Grid, Button, StyleRulesCallback, WithStyles, withStyles } from "@material-ui/core";
 import { DataExplorer } from "views-components/data-explorer/data-explorer";
 import { DataColumns } from 'components/data-table/data-table';
 import { SortDirection } from 'components/data-table/data-column';
-import { ResourceUuid } from 'views-components/data-explorer/renderers';
-import { AddIcon, ErrorIcon } from 'components/icon/icon';
+import { GroupMembersCount, ResourceUuid } from 'views-components/data-explorer/renderers';
+import { AddIcon } from 'components/icon/icon';
 import { ResourceName } from 'views-components/data-explorer/renderers';
 import { createTree } from 'models/tree';
 import { GROUPS_PANEL_ID, openCreateGroupDialog } from 'store/groups-panel/groups-panel-actions';
@@ -20,7 +20,6 @@ import { GroupResource } from 'models/group';
 import { RootState } from 'store/store';
 import { openContextMenu } from 'store/context-menu/context-menu-actions';
 import { ArvadosTheme } from 'common/custom-theme';
-import { InlinePulser } from 'components/loading/inline-pulser';
 
 type CssRules = "root";
 
@@ -121,31 +120,3 @@ export const GroupsPanel = withStyles(styles)(connect(
             }
         }
     }));
-
-
-const GroupMembersCount = connect(
-    (state: RootState, props: { uuid: string }) => {
-        const group = getResource<GroupResource>(props.uuid)(state.resources);
-
-        return {
-            value: group?.memberCount,
-        };
-
-    }
-)(withTheme()((props: {value: number | null | undefined, theme:ArvadosTheme}) => {
-    if (props.value === undefined) {
-        // Loading
-        return <Typography>
-            <InlinePulser />
-        </Typography>;
-    } else if (props.value === null) {
-        // Error
-        return <Typography>
-            <Tooltip title="Failed to load member count">
-                <ErrorIcon style={{color: props.theme.customs.colors.greyL}}/>
-            </Tooltip>
-        </Typography>;
-    } else {
-        return <Typography children={props.value} />;
-    }
-}));

commit 603c6b707287af641305f7e0389b934923d70a74
Author: Stephen Smith <stephen at curii.com>
Date:   Thu Feb 29 15:07:43 2024 -0500

    21221: Fetch count of group members in groups panel and store with resource.
    
    Also adds an inline loading spinner while the request is in flight
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/services/workbench2/package.json b/services/workbench2/package.json
index c6e2d6bcda..44548d8a94 100644
--- a/services/workbench2/package.json
+++ b/services/workbench2/package.json
@@ -62,6 +62,7 @@
     "react-dropzone": "5.1.1",
     "react-highlight-words": "0.14.0",
     "react-idle-timer": "4.3.6",
+    "react-loader-spinner": "^6.1.6",
     "react-redux": "5.0.7",
     "react-router": "4.3.1",
     "react-router-dom": "4.3.1",
diff --git a/services/workbench2/src/components/loading/inline-pulser.tsx b/services/workbench2/src/components/loading/inline-pulser.tsx
new file mode 100644
index 0000000000..def6b5edf3
--- /dev/null
+++ b/services/workbench2/src/components/loading/inline-pulser.tsx
@@ -0,0 +1,30 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import React from 'react';
+import { ThreeDots } from 'react-loader-spinner'
+import { withTheme } from '@material-ui/core';
+import { ArvadosTheme } from 'common/custom-theme';
+
+type ThemeProps = {
+    theme: ArvadosTheme;
+};
+
+type Props = {
+    color?: string;
+    height?: number;
+    width?: number;
+    radius?: number;
+};
+
+export const InlinePulser = withTheme()((props: Props & ThemeProps) => (
+    <ThreeDots
+        visible={true}
+        height={props.height || "30"}
+        width={props.width || "30"}
+        color={props.color || props.theme.customs.colors.greyL}
+        radius={props.radius || "10"}
+        ariaLabel="three-dots-loading"
+    />
+));
diff --git a/services/workbench2/src/models/group.ts b/services/workbench2/src/models/group.ts
index 0932b3c95e..078e2a27fa 100644
--- a/services/workbench2/src/models/group.ts
+++ b/services/workbench2/src/models/group.ts
@@ -18,6 +18,8 @@ export interface GroupResource extends TrashableResource, ResourceWithProperties
     ensure_unique_name: boolean;
     canWrite: boolean;
     canManage: boolean;
+    // Optional local-only field, undefined for not loaded, null for failed to load
+    memberCount?: number | null;
 }
 
 export enum GroupClass {
diff --git a/services/workbench2/src/store/groups-panel/groups-panel-middleware-service.ts b/services/workbench2/src/store/groups-panel/groups-panel-middleware-service.ts
index 7d7803f59e..fdfaf1c2cd 100644
--- a/services/workbench2/src/store/groups-panel/groups-panel-middleware-service.ts
+++ b/services/workbench2/src/store/groups-panel/groups-panel-middleware-service.ts
@@ -40,25 +40,40 @@ export class GroupsPanelMiddlewareService extends DataExplorerMiddlewareService
                     .addEqual('group_class', GroupClass.ROLE)
                     .addILike('name', dataExplorer.searchValue)
                     .getFilters();
-                const response = await this.services.groupsService
+                const groups = await this.services.groupsService
                     .list({
                         ...dataExplorerToListParams(dataExplorer),
                         filters,
                         order: order.getOrder(),
                     });
-                api.dispatch(updateResources(response.items));
+                api.dispatch(updateResources(groups.items));
                 api.dispatch(GroupsPanelActions.SET_ITEMS({
-                    ...listResultsToDataExplorerItemsMeta(response),
-                    items: response.items.map(item => item.uuid),
+                    ...listResultsToDataExplorerItemsMeta(groups),
+                    items: groups.items.map(item => item.uuid),
                 }));
-                const permissions = await this.services.permissionService.list({
-                    filters: new FilterBuilder()
-                        .addIn('head_uuid', response.items.map(item => item.uuid))
-                        .getFilters()
-                });
-                api.dispatch(updateResources(permissions.items));
+
+                // Get group member count
+                groups.items.map(group => (
+                    this.services.permissionService.list({
+                        limit: 0,
+                        filters: new FilterBuilder()
+                            .addEqual('head_uuid', group.uuid)
+                            .getFilters()
+                    }).then(members => {
+                        api.dispatch(updateResources([{
+                            ...group,
+                            memberCount: members.itemsAvailable,
+                        } as GroupResource]));
+                    }).catch(e => {
+                        // In case of error, store null to stop spinners and show failure icon
+                        api.dispatch(updateResources([{
+                            ...group,
+                            memberCount: null,
+                        } as GroupResource]));
+                    })
+                ));
             } catch (e) {
-                api.dispatch(couldNotFetchFavoritesContents());
+                api.dispatch(couldNotFetchGroupList());
             } finally {
                 api.dispatch(progressIndicatorActions.STOP_WORKING(this.getId()));
             }
@@ -72,7 +87,7 @@ const groupsPanelDataExplorerIsNotSet = () =>
         kind: SnackbarKind.ERROR
     });
 
-const couldNotFetchFavoritesContents = () =>
+const couldNotFetchGroupList = () =>
     snackbarActions.OPEN_SNACKBAR({
         message: 'Could not fetch groups.',
         kind: SnackbarKind.ERROR
diff --git a/services/workbench2/src/views/groups-panel/groups-panel.tsx b/services/workbench2/src/views/groups-panel/groups-panel.tsx
index 33acad50c6..96a2c3697d 100644
--- a/services/workbench2/src/views/groups-panel/groups-panel.tsx
+++ b/services/workbench2/src/views/groups-panel/groups-panel.tsx
@@ -4,24 +4,23 @@
 
 import React from 'react';
 import { connect } from 'react-redux';
-import { Grid, Button, Typography, StyleRulesCallback, WithStyles, withStyles } from "@material-ui/core";
+import { Grid, Button, Typography, StyleRulesCallback, WithStyles, withStyles, withTheme, Tooltip } from "@material-ui/core";
 import { DataExplorer } from "views-components/data-explorer/data-explorer";
 import { DataColumns } from 'components/data-table/data-table';
 import { SortDirection } from 'components/data-table/data-column';
 import { ResourceUuid } from 'views-components/data-explorer/renderers';
-import { AddIcon } from 'components/icon/icon';
+import { AddIcon, ErrorIcon } from 'components/icon/icon';
 import { ResourceName } from 'views-components/data-explorer/renderers';
 import { createTree } from 'models/tree';
 import { GROUPS_PANEL_ID, openCreateGroupDialog } from 'store/groups-panel/groups-panel-actions';
 import { noop } from 'lodash/fp';
 import { ContextMenuKind } from 'views-components/context-menu/context-menu';
-import { getResource, ResourcesState, filterResources } from 'store/resources/resources';
+import { getResource, ResourcesState } from 'store/resources/resources';
 import { GroupResource } from 'models/group';
 import { RootState } from 'store/store';
 import { openContextMenu } from 'store/context-menu/context-menu-actions';
-import { ResourceKind } from 'models/resource';
-import { LinkClass, LinkResource } from 'models/link';
 import { ArvadosTheme } from 'common/custom-theme';
+import { InlinePulser } from 'components/loading/inline-pulser';
 
 type CssRules = "root";
 
@@ -126,16 +125,27 @@ export const GroupsPanel = withStyles(styles)(connect(
 
 const GroupMembersCount = connect(
     (state: RootState, props: { uuid: string }) => {
-
-        const permissions = filterResources((resource: LinkResource) =>
-            resource.kind === ResourceKind.LINK &&
-            resource.linkClass === LinkClass.PERMISSION &&
-            resource.headUuid === props.uuid
-        )(state.resources);
+        const group = getResource<GroupResource>(props.uuid)(state.resources);
 
         return {
-            children: permissions.length,
+            value: group?.memberCount,
         };
 
     }
-)((props: {children: number}) => (<Typography children={props.children} />));
+)(withTheme()((props: {value: number | null | undefined, theme:ArvadosTheme}) => {
+    if (props.value === undefined) {
+        // Loading
+        return <Typography>
+            <InlinePulser />
+        </Typography>;
+    } else if (props.value === null) {
+        // Error
+        return <Typography>
+            <Tooltip title="Failed to load member count">
+                <ErrorIcon style={{color: props.theme.customs.colors.greyL}}/>
+            </Tooltip>
+        </Typography>;
+    } else {
+        return <Typography children={props.value} />;
+    }
+}));
diff --git a/services/workbench2/yarn.lock b/services/workbench2/yarn.lock
index bb3ca955a0..6df880066d 100644
--- a/services/workbench2/yarn.lock
+++ b/services/workbench2/yarn.lock
@@ -1750,6 +1750,29 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@emotion/is-prop-valid at npm:1.2.1":
+  version: 1.2.1
+  resolution: "@emotion/is-prop-valid at npm:1.2.1"
+  dependencies:
+    "@emotion/memoize": ^0.8.1
+  checksum: 8f42dc573a3fad79b021479becb639b8fe3b60bdd1081a775d32388bca418ee53074c7602a4c845c5f75fa6831eb1cbdc4d208cc0299f57014ed3a02abcad16a
+  languageName: node
+  linkType: hard
+
+"@emotion/memoize at npm:^0.8.1":
+  version: 0.8.1
+  resolution: "@emotion/memoize at npm:0.8.1"
+  checksum: a19cc01a29fcc97514948eaab4dc34d8272e934466ed87c07f157887406bc318000c69ae6f813a9001c6a225364df04249842a50e692ef7a9873335fbcc141b0
+  languageName: node
+  linkType: hard
+
+"@emotion/unitless at npm:0.8.0":
+  version: 0.8.0
+  resolution: "@emotion/unitless at npm:0.8.0"
+  checksum: 176141117ed23c0eb6e53a054a69c63e17ae532ec4210907a20b2208f91771821835f1c63dd2ec63e30e22fcc984026d7f933773ee6526dd038e0850919fae7a
+  languageName: node
+  linkType: hard
+
 "@fortawesome/fontawesome-common-types at npm:^0.2.28":
   version: 0.2.35
   resolution: "@fortawesome/fontawesome-common-types at npm:0.2.35"
@@ -2949,6 +2972,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@types/stylis at npm:4.2.0":
+  version: 4.2.0
+  resolution: "@types/stylis at npm:4.2.0"
+  checksum: 02a47584acd2fcb664f7d8270a69686c83752bdfb855f804015d33116a2b09c0b2ac535213a4a7b6d3a78b2915b22b4024cce067ae979beee0e4f8f5fdbc26a9
+  languageName: node
+  linkType: hard
+
 "@types/trusted-types at npm:*":
   version: 2.0.4
   resolution: "@types/trusted-types at npm:2.0.4"
@@ -3905,6 +3935,7 @@ __metadata:
     react-dropzone: 5.1.1
     react-highlight-words: 0.14.0
     react-idle-timer: 4.3.6
+    react-loader-spinner: ^6.1.6
     react-redux: 5.0.7
     react-router: 4.3.1
     react-router-dom: 4.3.1
@@ -5119,6 +5150,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"camelize at npm:^1.0.0":
+  version: 1.0.1
+  resolution: "camelize at npm:1.0.1"
+  checksum: 91d8611d09af725e422a23993890d22b2b72b4cabf7239651856950c76b4bf53fe0d0da7c5e4db05180e898e4e647220e78c9fbc976113bd96d603d1fcbfcb99
+  languageName: node
+  linkType: hard
+
 "caniuse-api at npm:^3.0.0":
   version: 3.0.0
   resolution: "caniuse-api at npm:3.0.0"
@@ -6041,6 +6079,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"css-color-keywords at npm:^1.0.0":
+  version: 1.0.0
+  resolution: "css-color-keywords at npm:1.0.0"
+  checksum: 8f125e3ad477bd03c77b533044bd9e8a6f7c0da52d49bbc0bbe38327b3829d6ba04d368ca49dd9ff3b667d2fc8f1698d891c198bbf8feade1a5501bf5a296408
+  languageName: node
+  linkType: hard
+
 "css-color-names at npm:0.0.4, css-color-names at npm:^0.0.4":
   version: 0.0.4
   resolution: "css-color-names at npm:0.0.4"
@@ -6135,6 +6180,17 @@ __metadata:
   languageName: node
   linkType: hard
 
+"css-to-react-native at npm:3.2.0":
+  version: 3.2.0
+  resolution: "css-to-react-native at npm:3.2.0"
+  dependencies:
+    camelize: ^1.0.0
+    css-color-keywords: ^1.0.0
+    postcss-value-parser: ^4.0.2
+  checksum: 263be65e805aef02c3f20c064665c998a8c35293e1505dbe6e3054fb186b01a9897ac6cf121f9840e5a9dfe3fb3994f6fcd0af84a865f1df78ba5bf89e77adce
+  languageName: node
+  linkType: hard
+
 "css-tree at npm:1.0.0-alpha.37":
   version: 1.0.0-alpha.37
   resolution: "css-tree at npm:1.0.0-alpha.37"
@@ -6320,6 +6376,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"csstype at npm:3.1.2":
+  version: 3.1.2
+  resolution: "csstype at npm:3.1.2"
+  checksum: e1a52e6c25c1314d6beef5168da704ab29c5186b877c07d822bd0806717d9a265e8493a2e35ca7e68d0f5d472d43fac1cdce70fd79fd0853dff81f3028d857b5
+  languageName: node
+  linkType: hard
+
 "csstype at npm:^2.0.0, csstype at npm:^2.5.2":
   version: 2.6.17
   resolution: "csstype at npm:2.6.17"
@@ -12722,6 +12785,15 @@ __metadata:
   languageName: node
   linkType: hard
 
+"nanoid at npm:^3.3.6":
+  version: 3.3.7
+  resolution: "nanoid at npm:3.3.7"
+  bin:
+    nanoid: bin/nanoid.cjs
+  checksum: d36c427e530713e4ac6567d488b489a36582ef89da1d6d4e3b87eded11eb10d7042a877958c6f104929809b2ab0bafa17652b076cdf84324aa75b30b722204f2
+  languageName: node
+  linkType: hard
+
 "nanomatch at npm:^1.2.9":
   version: 1.2.13
   resolution: "nanomatch at npm:1.2.13"
@@ -14871,6 +14943,17 @@ __metadata:
   languageName: node
   linkType: hard
 
+"postcss at npm:8.4.31":
+  version: 8.4.31
+  resolution: "postcss at npm:8.4.31"
+  dependencies:
+    nanoid: ^3.3.6
+    picocolors: ^1.0.0
+    source-map-js: ^1.0.2
+  checksum: 1d8611341b073143ad90486fcdfeab49edd243377b1f51834dc4f6d028e82ce5190e4f11bb2633276864503654fb7cab28e67abdc0fbf9d1f88cad4a0ff0beea
+  languageName: node
+  linkType: hard
+
 "postcss at npm:^7, postcss at npm:^7.0.0, postcss at npm:^7.0.1, postcss at npm:^7.0.14, postcss at npm:^7.0.17, postcss at npm:^7.0.2, postcss at npm:^7.0.23, postcss at npm:^7.0.27, postcss at npm:^7.0.32, postcss at npm:^7.0.5, postcss at npm:^7.0.6":
   version: 7.0.39
   resolution: "postcss at npm:7.0.39"
@@ -15450,6 +15533,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"react-is at npm:^18.2.0":
+  version: 18.2.0
+  resolution: "react-is at npm:18.2.0"
+  checksum: e72d0ba81b5922759e4aff17e0252bd29988f9642ed817f56b25a3e217e13eea8a7f2322af99a06edb779da12d5d636e9fda473d620df9a3da0df2a74141d53e
+  languageName: node
+  linkType: hard
+
 "react-lifecycles-compat at npm:^3.0.2, react-lifecycles-compat at npm:^3.0.4":
   version: 3.0.4
   resolution: "react-lifecycles-compat at npm:3.0.4"
@@ -15457,6 +15547,19 @@ __metadata:
   languageName: node
   linkType: hard
 
+"react-loader-spinner at npm:^6.1.6":
+  version: 6.1.6
+  resolution: "react-loader-spinner at npm:6.1.6"
+  dependencies:
+    react-is: ^18.2.0
+    styled-components: ^6.1.2
+  peerDependencies:
+    react: ^16.0.0 || ^17.0.0 || ^18.0.0
+    react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0
+  checksum: 07fbb2de7aaf9348c4c67116e25100a0a9511e51cf45be69948d618113361059a9a9688d87c142cebd80dcf6832a91f0eee7f4b303d106bd6677c51caa6aa5e3
+  languageName: node
+  linkType: hard
+
 "react-redux at npm:5.0.7":
   version: 5.0.7
   resolution: "react-redux at npm:5.0.7"
@@ -16957,7 +17060,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"shallowequal at npm:^1.0.2":
+"shallowequal at npm:1.1.0, shallowequal at npm:^1.0.2":
   version: 1.1.0
   resolution: "shallowequal at npm:1.1.0"
   checksum: f4c1de0837f106d2dbbfd5d0720a5d059d1c66b42b580965c8f06bb1db684be8783538b684092648c981294bf817869f743a066538771dbecb293df78f765e00
@@ -17249,6 +17352,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"source-map-js at npm:^1.0.2":
+  version: 1.0.2
+  resolution: "source-map-js at npm:1.0.2"
+  checksum: c049a7fc4deb9a7e9b481ae3d424cc793cb4845daa690bc5a05d428bf41bf231ced49b4cf0c9e77f9d42fdb3d20d6187619fc586605f5eabe995a316da8d377c
+  languageName: node
+  linkType: hard
+
 "source-map-resolve at npm:^0.5.0, source-map-resolve at npm:^0.5.2":
   version: 0.5.3
   resolution: "source-map-resolve at npm:0.5.3"
@@ -17835,6 +17945,26 @@ __metadata:
   languageName: node
   linkType: hard
 
+"styled-components at npm:^6.1.2":
+  version: 6.1.8
+  resolution: "styled-components at npm:6.1.8"
+  dependencies:
+    "@emotion/is-prop-valid": 1.2.1
+    "@emotion/unitless": 0.8.0
+    "@types/stylis": 4.2.0
+    css-to-react-native: 3.2.0
+    csstype: 3.1.2
+    postcss: 8.4.31
+    shallowequal: 1.1.0
+    stylis: 4.3.1
+    tslib: 2.5.0
+  peerDependencies:
+    react: ">= 16.8.0"
+    react-dom: ">= 16.8.0"
+  checksum: 367858097ca57911cc310ddf95d16fed162fbb1d2f187366b33ce5e6e22c324f9bcc7206686624a3edd15e3e9605875c8c041ac5ffb430bbee98f1ad0be71604
+  languageName: node
+  linkType: hard
+
 "stylehacks at npm:^4.0.0":
   version: 4.0.3
   resolution: "stylehacks at npm:4.0.3"
@@ -17846,6 +17976,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"stylis at npm:4.3.1":
+  version: 4.3.1
+  resolution: "stylis at npm:4.3.1"
+  checksum: d365f1b008677b2147e8391e9cf20094a4202a5f9789562e7d9d0a3bd6f0b3067d39e8fd17cce5323903a56f6c45388e3d839e9c0bb5a738c91726992b14966d
+  languageName: node
+  linkType: hard
+
 "supports-color at npm:^2.0.0":
   version: 2.0.0
   resolution: "supports-color at npm:2.0.0"
@@ -18297,6 +18434,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"tslib at npm:2.5.0":
+  version: 2.5.0
+  resolution: "tslib at npm:2.5.0"
+  checksum: ae3ed5f9ce29932d049908ebfdf21b3a003a85653a9a140d614da6b767a93ef94f460e52c3d787f0e4f383546981713f165037dc2274df212ea9f8a4541004e1
+  languageName: node
+  linkType: hard
+
 "tslib at npm:^1.8.0, tslib at npm:^1.8.1, tslib at npm:^1.9.0, tslib at npm:^1.9.3":
   version: 1.14.1
   resolution: "tslib at npm:1.14.1"

-----------------------------------------------------------------------


hooks/post-receive
-- 




More information about the arvados-commits mailing list