[ARVADOS-WORKBENCH2] created: 2.4.0-53-ga4c96d2e

Git user git at public.arvados.org
Mon May 16 18:59:19 UTC 2022


        at  a4c96d2e98d9f2971e9268355bd31331c2b6a5e2 (commit)


commit a4c96d2e98d9f2971e9268355bd31331c2b6a5e2
Author: Lucas Di Pentima <lucas.dipentima at curii.com>
Date:   Mon May 16 15:58:28 2022 -0300

    16115: Improves link rendering. Adds tests.
    
    Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <lucas.dipentima at curii.com>

diff --git a/cypress/integration/sharing.spec.js b/cypress/integration/sharing.spec.js
index df7c7436..1d3112c2 100644
--- a/cypress/integration/sharing.spec.js
+++ b/cypress/integration/sharing.spec.js
@@ -14,13 +14,11 @@ describe('Sharing tests', function () {
         cy.getUser('admin', 'Admin', 'User', true, true)
             .as('adminUser').then(function () {
                 adminUser = this.adminUser;
-            }
-            );
+            });
         cy.getUser('collectionuser1', 'Collection', 'User', false, true)
             .as('activeUser').then(function () {
                 activeUser = this.activeUser;
-            }
-            );
+            });
     })
 
     beforeEach(function () {
@@ -28,6 +26,38 @@ describe('Sharing tests', function () {
         cy.clearLocalStorage()
     });
 
+    it('can create and delete sharing URLs on collections', () => {
+        const collName = 'shared-collection ' + new Date().getTime();
+        cy.createCollection(adminUser.token, {
+            name: collName,
+            owner_uuid: adminUser.uuid,
+        }).as('sharedCollection').then(function (sharedCollection) {
+            cy.loginAs(adminUser);
+
+            cy.get('main').contains(sharedCollection.name).rightclick();
+            cy.get('[data-cy=context-menu]').within(() => {
+                cy.contains('Share').click();
+            });
+            cy.get('.sharing-dialog').within(() => {
+                cy.contains('Sharing URLs').click();
+                cy.contains('Create sharing URL');
+                cy.contains('No sharing URLs');
+                cy.should('not.contain', 'Token');
+                cy.should('not.contain', 'expiring at:');
+
+                cy.contains('Create sharing URL').click();
+                cy.should('not.contain', 'No sharing URLs');
+                cy.contains('Token');
+                cy.contains('expiring at:');
+
+                cy.get('[data-cy=remove-url-btn]').find('button').click();
+                cy.contains('No sharing URLs');
+                cy.should('not.contain', 'Token');
+                cy.should('not.contain', 'expiring at:');
+            })
+        })
+    });
+
     it('can share projects to other users', () => {
         cy.loginAs(adminUser);
 
@@ -46,8 +76,10 @@ describe('Sharing tests', function () {
             cy.get('.sharing-dialog').as('sharingDialog');
             cy.get('[data-cy=invite-people-field]').find('input').type(activeUser.user.email);
             cy.get('[role=tooltip]').click();
-            cy.get('@sharingDialog').contains('Save changes').click();
-            cy.get('@sharingDialog').contains('Close').click();
+            cy.get('@sharingDialog').within(() => {
+                cy.contains('Save changes').click();
+                cy.contains('Close').click();
+            });
         });
 
         cy.createGroup(adminUser.token, {
@@ -62,8 +94,10 @@ describe('Sharing tests', function () {
             cy.get('.sharing-dialog').as('sharingDialog');
             cy.get('[data-cy=invite-people-field]').find('input').type(activeUser.user.email);
             cy.get('[role=tooltip]').click();
-            cy.get('@sharingDialog').contains('Save changes').click();
-            cy.get('@sharingDialog').contains('Close').click();
+            cy.get('@sharingDialog').within(() => {
+                cy.contains('Save changes').click();
+                cy.contains('Close').click();
+            });
         });
 
         cy.getAll('@mySharedWritableProject', '@mySharedReadonlyProject')
diff --git a/src/views-components/sharing-dialog/sharing-urls-component.test.tsx b/src/views-components/sharing-dialog/sharing-urls-component.test.tsx
index 0cbc661d..cf3884c7 100644
--- a/src/views-components/sharing-dialog/sharing-urls-component.test.tsx
+++ b/src/views-components/sharing-dialog/sharing-urls-component.test.tsx
@@ -57,6 +57,14 @@ describe("<SharingURLsComponent />", () => {
         expect(wrapper.find('a').at(1).props().href).toBe(`${props.collectionUuid}${sharingPrefix}/t=${props.sharingTokens[1].apiToken}/_/`);
     });
 
+    it("renders a list of URLs with no expiration", () => {
+        props.sharingTokens[0].expiresAt = null;
+        props.sharingTokens[1].expiresAt = null;
+        wrapper = mount(<SharingURLsComponent {...props} />);
+        expect(wrapper.find('a').at(0).text()).toContain(`Token aaaaaaaa... with no expiration date`);
+        expect(wrapper.find('a').at(1).text()).toContain(`Token bbbbbbbb... with no expiration date`);
+    });
+
     it("calls delete token handler when delete button is clicked", () => {
         wrapper.find('button').at(0).simulate('click');
         expect(props.onDeleteSharingToken).toHaveBeenCalledWith(props.sharingTokens[0].uuid);
diff --git a/src/views-components/sharing-dialog/sharing-urls-component.tsx b/src/views-components/sharing-dialog/sharing-urls-component.tsx
index 1638aaf7..c9cbc0df 100644
--- a/src/views-components/sharing-dialog/sharing-urls-component.tsx
+++ b/src/views-components/sharing-dialog/sharing-urls-component.tsx
@@ -66,7 +66,9 @@ export const SharingURLsComponent = withStyles(styles)((props: SharingURLsCompon
         ? `${props.sharingURLsPrefix.replace('*', props.collectionUuid)}/t=${token.apiToken}/_/`
         : `${props.sharingURLsPrefix}/c=${props.collectionUuid}/t=${token.apiToken}/_/`;
         const expDate = new Date(token.expiresAt);
-        const urlLabel = `Token ${token.apiToken.slice(0, 8)}... expiring at: ${expDate.toLocaleString()} (${moment(expDate).fromNow()})`;
+        const urlLabel = !!token.expiresAt
+        ? `Token ${token.apiToken.slice(0, 8)}... expiring at: ${expDate.toLocaleString()} (${moment(expDate).fromNow()})`
+        : `Token ${token.apiToken.slice(0, 8)}... with no expiration date`;
 
         return <Grid container alignItems='center' key={token.uuid}  className={props.classes.sharingUrlRow}>
             <Grid item>
@@ -81,7 +83,7 @@ export const SharingURLsComponent = withStyles(styles)((props: SharingURLsCompon
                     <CopyIcon />
                 </CopyToClipboard>
             </Tooltip></span>
-            <span className={props.classes.sharingUrlButton}><Tooltip title='Remove'>
+            <span data-cy='remove-url-btn' className={props.classes.sharingUrlButton}><Tooltip title='Remove'>
                 <IconButton onClick={() => props.onDeleteSharingToken(token.uuid)}>
                     <RemoveIcon />
                 </IconButton>
diff --git a/tools/arvados_config.yml b/tools/arvados_config.yml
index b9bcfbe0..3b2ecd8d 100644
--- a/tools/arvados_config.yml
+++ b/tools/arvados_config.yml
@@ -5,6 +5,7 @@ Clusters:
     API:
       RequestTimeout: 30s
       VocabularyPath: ""
+      MaxTokenLifetime: 24h
     TLS:
       Insecure: true
     Collections:

commit a51d9df296fa297f2b21201d497ed48b4639c8e9
Author: Lucas Di Pentima <lucas.dipentima at curii.com>
Date:   Mon May 16 15:21:56 2022 -0300

    16115: Sets api_client as trusted in order to handle tokens.
    
    Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <lucas.dipentima at curii.com>

diff --git a/cypress/support/commands.js b/cypress/support/commands.js
index c2d78b54..74f44f7a 100644
--- a/cypress/support/commands.js
+++ b/cypress/support/commands.js
@@ -73,30 +73,48 @@ Cypress.Commands.add(
             }),
             return_to: ',https://example.local'
         }, null, systemToken, true, false) // Don't follow redirects so we can catch the token
-            .its('headers.location').as('location')
-            // Get its token and set the account up as admin and/or active
+        .its('headers.location').as('location')
+        // Get its token and set the account up as admin and/or active
+        .then(function () {
+            this.userToken = this.location.split("=")[1]
+            assert.isString(this.userToken)
+            return cy.doRequest('GET', '/arvados/v1/users', null, {
+                filters: `[["username", "=", "${username}"]]`
+            })
+            .its('body.items.0').as('aUser')
             .then(function () {
-                this.userToken = this.location.split("=")[1]
-                assert.isString(this.userToken)
-                return cy.doRequest('GET', '/arvados/v1/users', null, {
-                    filters: `[["username", "=", "${username}"]]`
+                cy.doRequest('PUT', `/arvados/v1/users/${this.aUser.uuid}`, {
+                    user: {
+                        is_admin: is_admin,
+                        is_active: is_active
+                    }
                 })
-                    .its('body.items.0')
-                    .as('aUser')
+                .its('body').as('theUser')
+                .then(function () {
+                    cy.doRequest('GET', '/arvados/v1/api_clients', null, {
+                        filters: `[["is_trusted", "=", false]]`,
+                        order: `["created_at desc"]`
+                    })
+                    .its('body.items').as('apiClients')
                     .then(function () {
-                        cy.doRequest('PUT', `/arvados/v1/users/${this.aUser.uuid}`, {
-                            user: {
-                                is_admin: is_admin,
-                                is_active: is_active
-                            }
-                        })
-                            .its('body')
-                            .as('theUser')
-                            .then(function () {
-                                return { user: this.theUser, token: this.userToken };
+                        if (this.apiClients.length > 0) {
+                            cy.doRequest('PUT', `/arvados/v1/api_clients/${this.apiClients[0].uuid}`, {
+                                api_client: {
+                                    is_trusted: true
+                                }
                             })
+                            .its('body').as('updatedApiClient')
+                            .then(function() {
+                                assert(this.updatedApiClient.is_trusted);
+                            })
+                        }
                     })
+                    .then(function () {
+                        return { user: this.theUser, token: this.userToken };
+                    })
+                })
             })
+        })
     }
 )
 

commit f0ecfdc83d1807bfd81b285b76351e48db2ff992
Author: Lucas Di Pentima <lucas.dipentima at curii.com>
Date:   Mon May 16 11:30:31 2022 -0300

    16115: Fixes cypress tests.
    
    Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <lucas.dipentima at curii.com>

diff --git a/cypress/integration/sharing.spec.js b/cypress/integration/sharing.spec.js
index 5a297136..df7c7436 100644
--- a/cypress/integration/sharing.spec.js
+++ b/cypress/integration/sharing.spec.js
@@ -46,7 +46,8 @@ describe('Sharing tests', function () {
             cy.get('.sharing-dialog').as('sharingDialog');
             cy.get('[data-cy=invite-people-field]').find('input').type(activeUser.user.email);
             cy.get('[role=tooltip]').click();
-            cy.get('@sharingDialog').contains('Save').click();
+            cy.get('@sharingDialog').contains('Save changes').click();
+            cy.get('@sharingDialog').contains('Close').click();
         });
 
         cy.createGroup(adminUser.token, {
@@ -61,7 +62,8 @@ describe('Sharing tests', function () {
             cy.get('.sharing-dialog').as('sharingDialog');
             cy.get('[data-cy=invite-people-field]').find('input').type(activeUser.user.email);
             cy.get('[role=tooltip]').click();
-            cy.get('@sharingDialog').contains('Save').click();
+            cy.get('@sharingDialog').contains('Save changes').click();
+            cy.get('@sharingDialog').contains('Close').click();
         });
 
         cy.getAll('@mySharedWritableProject', '@mySharedReadonlyProject')
@@ -95,7 +97,7 @@ describe('Sharing tests', function () {
         cy.getAll('@mySharedWritableProject')
             .then(function ([mySharedWritableProject]) {
                 cy.loginAs(activeUser);
-                
+
                 cy.get('[data-cy=side-panel-tree]').contains('Shared with me').click();
 
                 const newProjectName = `New project name ${mySharedWritableProject.name}`;

commit 07bcb7d7b574ce4dfd5d335618f683e6fb1c7d4f
Author: Lucas Di Pentima <lucas.dipentima at curii.com>
Date:   Mon May 16 10:50:59 2022 -0300

    16115: Adds unit tests to SharingURLsComponent.
    
    Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <lucas.dipentima at curii.com>

diff --git a/src/views-components/sharing-dialog/sharing-urls-component.test.tsx b/src/views-components/sharing-dialog/sharing-urls-component.test.tsx
new file mode 100644
index 00000000..0cbc661d
--- /dev/null
+++ b/src/views-components/sharing-dialog/sharing-urls-component.test.tsx
@@ -0,0 +1,64 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import React from 'react';
+import { mount, configure } from 'enzyme';
+import Adapter from 'enzyme-adapter-react-16';
+
+import {
+    SharingURLsComponent,
+    SharingURLsComponentProps
+} from './sharing-urls-component';
+
+configure({ adapter: new Adapter() });
+
+describe("<SharingURLsComponent />", () => {
+    let props: SharingURLsComponentProps;
+    let wrapper;
+
+    beforeEach(() => {
+        props = {
+            collectionUuid: 'collection-uuid',
+            sharingURLsPrefix: 'sharing-urls-prefix',
+            sharingTokens: [
+                {
+                    uuid: 'token-uuid1',
+                    apiToken: 'aaaaaaaaaa',
+                    expiresAt: '2009-01-03T18:15:00Z',
+                },
+                {
+                    uuid: 'token-uuid2',
+                    apiToken: 'bbbbbbbbbb',
+                    expiresAt: '2009-01-03T18:15:01Z',
+                },
+            ],
+            onCopy: jest.fn(),
+            onDeleteSharingToken: jest.fn(),
+        };
+        wrapper = mount(<SharingURLsComponent {...props} />);
+    });
+
+    it("renders a list of sharing URLs", () => {
+        expect(wrapper.find('a').length).toBe(2);
+        // Check 1st URL
+        expect(wrapper.find('a').at(0).text()).toContain(`Token aaaaaaaa... expiring at: ${new Date(props.sharingTokens[0].expiresAt).toLocaleString()}`);
+        expect(wrapper.find('a').at(0).props().href).toBe(`${props.sharingURLsPrefix}/c=${props.collectionUuid}/t=${props.sharingTokens[0].apiToken}/_/`);
+        // Check 2nd URL
+        expect(wrapper.find('a').at(1).text()).toContain(`Token bbbbbbbb... expiring at: ${new Date(props.sharingTokens[1].expiresAt).toLocaleString()}`);
+        expect(wrapper.find('a').at(1).props().href).toBe(`${props.sharingURLsPrefix}/c=${props.collectionUuid}/t=${props.sharingTokens[1].apiToken}/_/`);
+    });
+
+    it("renders a list URLs with collection UUIDs as subdomains", () => {
+        props.sharingURLsPrefix = '*.sharing-urls-prefix';
+        const sharingPrefix = '.sharing-urls-prefix';
+        wrapper = mount(<SharingURLsComponent {...props} />);
+        expect(wrapper.find('a').at(0).props().href).toBe(`${props.collectionUuid}${sharingPrefix}/t=${props.sharingTokens[0].apiToken}/_/`);
+        expect(wrapper.find('a').at(1).props().href).toBe(`${props.collectionUuid}${sharingPrefix}/t=${props.sharingTokens[1].apiToken}/_/`);
+    });
+
+    it("calls delete token handler when delete button is clicked", () => {
+        wrapper.find('button').at(0).simulate('click');
+        expect(props.onDeleteSharingToken).toHaveBeenCalledWith(props.sharingTokens[0].uuid);
+    });
+});
\ No newline at end of file
diff --git a/src/views-components/sharing-dialog/sharing-urls-component.tsx b/src/views-components/sharing-dialog/sharing-urls-component.tsx
index 5151e8a2..1638aaf7 100644
--- a/src/views-components/sharing-dialog/sharing-urls-component.tsx
+++ b/src/views-components/sharing-dialog/sharing-urls-component.tsx
@@ -55,7 +55,7 @@ export interface SharingURLsComponentActionProps {
     onCopy: (message: string) => void;
 }
 
-type SharingURLsComponentProps = SharingURLsComponentDataProps & SharingURLsComponentActionProps;
+export type SharingURLsComponentProps = SharingURLsComponentDataProps & SharingURLsComponentActionProps;
 
 export const SharingURLsComponent = withStyles(styles)((props: SharingURLsComponentProps & WithStyles<CssRules>) => <Grid container direction='column' spacing={24} className={props.classes.sharingUrlList}>
     { props.sharingTokens.length > 0

commit f55d7e31aa315de4eece3139c4fa1df7d6eb929e
Author: Lucas Di Pentima <lucas.dipentima at curii.com>
Date:   Mon May 16 09:29:40 2022 -0300

    16115: Refresh permission management form on tab change.
    
    Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <lucas.dipentima at curii.com>

diff --git a/src/store/sharing-dialog/sharing-dialog-actions.ts b/src/store/sharing-dialog/sharing-dialog-actions.ts
index d734d601..3eec0b59 100644
--- a/src/store/sharing-dialog/sharing-dialog-actions.ts
+++ b/src/store/sharing-dialog/sharing-dialog-actions.ts
@@ -19,7 +19,6 @@ import { SHARING_MANAGEMENT_FORM_NAME } from 'store/sharing-dialog/sharing-dialo
 import { RootState } from 'store/store';
 import { getDialog } from 'store/dialog/dialog-reducer';
 import { PermissionLevel } from 'models/permission';
-import { PermissionResource } from 'models/permission';
 import { differenceWith } from "lodash";
 import { withProgress } from "store/progress-indicator/with-progress";
 import { progressIndicatorActions } from 'store/progress-indicator/progress-indicator-actions';
@@ -117,8 +116,7 @@ const loadSharingDialog = async (dispatch: Dispatch, getState: () => RootState,
         dispatch(progressIndicatorActions.START_WORKING(SHARING_DIALOG_NAME));
         try {
             const resourceUuid = dialog.data.resourceUuid;
-            const { items } = await permissionService.listResourcePermissions(resourceUuid);
-            await dispatch<any>(initializeManagementForm(items));
+            await dispatch<any>(initializeManagementForm);
             // For collections, we need to load the public sharing tokens
             if (extractUuidObjectType(resourceUuid) === ResourceObjectType.COLLECTION) {
                 const sharingTokens = await apiClientAuthorizationService.listCollectionSharingTokens(resourceUuid);
@@ -136,9 +134,15 @@ const loadSharingDialog = async (dispatch: Dispatch, getState: () => RootState,
     }
 };
 
-const initializeManagementForm = (permissionLinks: PermissionResource[]) =>
-    async (dispatch: Dispatch, getState: () => RootState, { userService, groupsService }: ServiceRepository) => {
+export const initializeManagementForm = async (dispatch: Dispatch, getState: () => RootState, { userService, groupsService, permissionService }: ServiceRepository) => {
 
+        const dialog = getDialog<SharingDialogData>(getState().dialog, SHARING_DIALOG_NAME);
+        if (!dialog) {
+            return;
+        }
+        dispatch(progressIndicatorActions.START_WORKING(SHARING_DIALOG_NAME));
+        const resourceUuid = dialog?.data.resourceUuid;
+        const { items: permissionLinks } = await permissionService.listResourcePermissions(resourceUuid);
         const filters = new FilterBuilder()
             .addIn('uuid', permissionLinks.map(({ tailUuid }) => tailUuid))
             .getFilters();
@@ -169,6 +173,7 @@ const initializeManagementForm = (permissionLinks: PermissionResource[]) =>
         };
 
         dispatch(initialize(SHARING_MANAGEMENT_FORM_NAME, managementFormData));
+        dispatch(progressIndicatorActions.STOP_WORKING(SHARING_DIALOG_NAME));
     };
 
 const saveManagementChanges = async (_: Dispatch, getState: () => RootState, { permissionService }: ServiceRepository) => {
diff --git a/src/views-components/sharing-dialog/sharing-dialog-component.tsx b/src/views-components/sharing-dialog/sharing-dialog-component.tsx
index 4ff9150b..259390aa 100644
--- a/src/views-components/sharing-dialog/sharing-dialog-component.tsx
+++ b/src/views-components/sharing-dialog/sharing-dialog-component.tsx
@@ -38,6 +38,7 @@ export interface SharingDialogActionProps {
     onClose: () => void;
     onSave: () => void;
     onCreateSharingToken: () => void;
+    refreshPermissions: () => void;
 }
 enum SharingDialogTab {
     PERMISSIONS = 0,
@@ -45,7 +46,7 @@ enum SharingDialogTab {
 }
 export default (props: SharingDialogDataProps & SharingDialogActionProps) => {
     const { open, loading, saveEnabled, sharedResourceUuid,
-        onClose, onSave, onCreateSharingToken } = props;
+        onClose, onSave, onCreateSharingToken, refreshPermissions } = props;
     const showTabs = extractUuidObjectType(sharedResourceUuid) === ResourceObjectType.COLLECTION;
     const [tabNr, setTabNr] = React.useState<number>(SharingDialogTab.PERMISSIONS);
 
@@ -65,7 +66,13 @@ export default (props: SharingDialogDataProps & SharingDialogActionProps) => {
             Sharing settings
         </DialogTitle>
         { showTabs &&
-        <Tabs value={tabNr} onChange={(_, tb) => setTabNr(tb)}>
+        <Tabs value={tabNr}
+            onChange={(_, tb) => {
+                if (tb === SharingDialogTab.PERMISSIONS) {
+                    refreshPermissions();
+                }
+                setTabNr(tb)}
+            }>
             <Tab label="With users/groups" />
             <Tab label="Sharing URLs" disabled={saveEnabled} />
         </Tabs>
diff --git a/src/views-components/sharing-dialog/sharing-dialog.tsx b/src/views-components/sharing-dialog/sharing-dialog.tsx
index a077f7ee..e48983a2 100644
--- a/src/views-components/sharing-dialog/sharing-dialog.tsx
+++ b/src/views-components/sharing-dialog/sharing-dialog.tsx
@@ -10,7 +10,8 @@ import {
     saveSharingDialogChanges,
     connectSharingDialogProgress,
     SharingDialogData,
-    createSharingToken
+    createSharingToken,
+    initializeManagementForm
 } from 'store/sharing-dialog/sharing-dialog-actions';
 import { WithDialogProps } from 'store/dialog/with-dialog';
 import SharingDialogComponent, {
@@ -44,6 +45,9 @@ const mapDispatchToProps = (dispatch: Dispatch, { ...props }: Props): SharingDia
     },
     onCreateSharingToken: () => {
         dispatch<any>(createSharingToken);
+    },
+    refreshPermissions: () => {
+        dispatch<any>(initializeManagementForm);
     }
 });
 

commit 5124df4b34de48cdf5387197e7b728b7590e8ae8
Author: Lucas Di Pentima <lucas.dipentima at curii.com>
Date:   Mon May 16 09:15:28 2022 -0300

    16115: UI consistency between tabs. Removes unnecessary code splitting.
    
    Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <lucas.dipentima at curii.com>

diff --git a/src/views-components/sharing-dialog/sharing-dialog-component.tsx b/src/views-components/sharing-dialog/sharing-dialog-component.tsx
index eca6dc2c..4ff9150b 100644
--- a/src/views-components/sharing-dialog/sharing-dialog-component.tsx
+++ b/src/views-components/sharing-dialog/sharing-dialog-component.tsx
@@ -20,13 +20,13 @@ import {
     withStyles
 } from '@material-ui/core/styles';
 import { DialogActions } from 'components/dialog-actions/dialog-actions';
-import { SharingDialogContent } from './sharing-dialog-content';
 import { SharingURLsContent } from './sharing-urls';
 import {
     extractUuidObjectType,
     ResourceObjectType
 } from 'models/resource';
 import { SharingInvitationForm } from './sharing-invitation-form';
+import { SharingManagementForm } from './sharing-management-form';
 
 export interface SharingDialogDataProps {
     open: boolean;
@@ -72,7 +72,11 @@ export default (props: SharingDialogDataProps & SharingDialogActionProps) => {
         }
         <DialogContent>
             { tabNr === SharingDialogTab.PERMISSIONS &&
-            <SharingDialogContent />
+            <Grid container direction='column' spacing={24}>
+              <Grid item>
+                  <SharingManagementForm />
+              </Grid>
+            </Grid>
             }
             { tabNr === SharingDialogTab.URLS &&
             <SharingURLsContent uuid={sharedResourceUuid} />
@@ -84,6 +88,7 @@ export default (props: SharingDialogDataProps & SharingDialogActionProps) => {
                 <Grid item md={12}>
                     <SharingInvitationForm />
                 </Grid> }
+                <Grid item xs />
                 { tabNr === SharingDialogTab.URLS &&
                 <Grid item>
                     <Button
@@ -94,7 +99,6 @@ export default (props: SharingDialogDataProps & SharingDialogActionProps) => {
                     </Button>
                 </Grid>
                 }
-                <Grid item xs />
                 { tabNr === SharingDialogTab.PERMISSIONS &&
                 <Grid item>
                     <Button
@@ -102,7 +106,7 @@ export default (props: SharingDialogDataProps & SharingDialogActionProps) => {
                         color='primary'
                         onClick={onSave}
                         disabled={!saveEnabled}>
-                        Save
+                        Save changes
                     </Button>
                 </Grid>
                 }
diff --git a/src/views-components/sharing-dialog/sharing-dialog-content.tsx b/src/views-components/sharing-dialog/sharing-dialog-content.tsx
deleted file mode 100644
index ee1ccf87..00000000
--- a/src/views-components/sharing-dialog/sharing-dialog-content.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import React from 'react';
-import { Grid } from '@material-ui/core';
-
-import { SharingManagementForm } from './sharing-management-form';
-
-export const SharingDialogContent = () =>
-    <Grid container direction='column' spacing={24}>
-        <Grid item>
-            <SharingManagementForm />
-        </Grid>
-    </Grid>;
diff --git a/src/views-components/sharing-dialog/sharing-management-form-component.tsx b/src/views-components/sharing-dialog/sharing-management-form-component.tsx
index 9c3b6403..2ebf8c2d 100644
--- a/src/views-components/sharing-dialog/sharing-management-form-component.tsx
+++ b/src/views-components/sharing-dialog/sharing-management-form-component.tsx
@@ -23,8 +23,10 @@ export default () =>
 const SharingManagementFieldArray = ({ fields }: WrappedFieldArrayProps<{ email: string }>) =>
     <div>
         {
-            fields.map((field, index, fields) =>
+        fields.length > 0
+        ? fields.map((field, index, fields) =>
                 <PermissionManagementRow key={field} {...{ field, index, fields }} />)
+        : <Typography>No permissions set</Typography>
         }
         <Divider />
     </div>;

commit 4ee084a36d4adf2740f84875c47d81de566ec7b1
Author: Lucas Di Pentima <lucas.dipentima at curii.com>
Date:   Mon May 16 08:14:43 2022 -0300

    16115: Fixes sharing URL building for per-collection domain configs.
    
    Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <lucas.dipentima at curii.com>

diff --git a/src/views-components/sharing-dialog/sharing-urls-component.tsx b/src/views-components/sharing-dialog/sharing-urls-component.tsx
index ee5d50be..5151e8a2 100644
--- a/src/views-components/sharing-dialog/sharing-urls-component.tsx
+++ b/src/views-components/sharing-dialog/sharing-urls-component.tsx
@@ -9,6 +9,7 @@ import {
     Link,
     StyleRulesCallback,
     Tooltip,
+    Typography,
     WithStyles,
     withStyles
 } from '@material-ui/core';
@@ -57,10 +58,13 @@ export interface SharingURLsComponentActionProps {
 type SharingURLsComponentProps = SharingURLsComponentDataProps & SharingURLsComponentActionProps;
 
 export const SharingURLsComponent = withStyles(styles)((props: SharingURLsComponentProps & WithStyles<CssRules>) => <Grid container direction='column' spacing={24} className={props.classes.sharingUrlList}>
-    { props.sharingTokens
+    { props.sharingTokens.length > 0
+    ? props.sharingTokens
     .sort((a, b) => (new Date(a.expiresAt).getTime() - new Date(b.expiresAt).getTime()))
     .map(token => {
-        const url = `${props.sharingURLsPrefix}/c=${props.collectionUuid}/t=${token.apiToken}/_/`
+        const url = props.sharingURLsPrefix.includes('*')
+        ? `${props.sharingURLsPrefix.replace('*', props.collectionUuid)}/t=${token.apiToken}/_/`
+        : `${props.sharingURLsPrefix}/c=${props.collectionUuid}/t=${token.apiToken}/_/`;
         const expDate = new Date(token.expiresAt);
         const urlLabel = `Token ${token.apiToken.slice(0, 8)}... expiring at: ${expDate.toLocaleString()} (${moment(expDate).fromNow()})`;
 
@@ -84,5 +88,6 @@ export const SharingURLsComponent = withStyles(styles)((props: SharingURLsCompon
             </Tooltip></span>
             </Grid>
         </Grid>
-    }) }
+    })
+    : <Grid item><Typography>No sharing URLs</Typography></Grid> }
 </Grid>);

commit add44b50131fc6a1e982a1af333f0e1f26803b3f
Author: Lucas Di Pentima <lucas.dipentima at curii.com>
Date:   Fri May 13 12:26:09 2022 -0300

    16115: Further code cleaning and permission handling improvements.
    
    Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <lucas.dipentima at curii.com>

diff --git a/src/store/sharing-dialog/sharing-dialog-actions.ts b/src/store/sharing-dialog/sharing-dialog-actions.ts
index 985b345e..d734d601 100644
--- a/src/store/sharing-dialog/sharing-dialog-actions.ts
+++ b/src/store/sharing-dialog/sharing-dialog-actions.ts
@@ -25,9 +25,7 @@ import { withProgress } from "store/progress-indicator/with-progress";
 import { progressIndicatorActions } from 'store/progress-indicator/progress-indicator-actions';
 import { snackbarActions, SnackbarKind } from "../snackbar/snackbar-actions";
 import {
-    extractUuidKind,
     extractUuidObjectType,
-    ResourceKind,
     ResourceObjectType
 } from "models/resource";
 import { resourcesActions } from "store/resources/resources-actions";
@@ -59,22 +57,6 @@ export const saveSharingDialogChanges = async (dispatch: Dispatch, getState: ()
     }
 };
 
-export const sendSharingInvitations = async (dispatch: Dispatch, getState: () => RootState) => {
-    dispatch(progressIndicatorActions.START_WORKING(SHARING_DIALOG_NAME));
-    await dispatch<any>(sendInvitations);
-    dispatch(closeSharingDialog());
-    dispatch(snackbarActions.OPEN_SNACKBAR({
-        message: 'Resource has been shared',
-        kind: SnackbarKind.SUCCESS,
-    }));
-    dispatch(progressIndicatorActions.STOP_WORKING(SHARING_DIALOG_NAME));
-
-    const dialog = getDialog<SharingDialogData>(getState().dialog, SHARING_DIALOG_NAME);
-    if (dialog && dialog.data.refresh) {
-        dialog.data.refresh();
-    }
-};
-
 export interface SharingDialogData {
     resourceUuid: string;
     refresh: () => void;
@@ -201,13 +183,11 @@ const saveManagementChanges = async (_: Dispatch, getState: () => RootState, { p
             (a, b) => a.permissionUuid === b.permissionUuid
         );
 
-        for (const { permissionUuid } of cancelledPermissions) {
-            await permissionService.delete(permissionUuid);
-        }
-
-        for (const permission of permissions) {
-            await permissionService.update(permission.permissionUuid, { name: permission.permissions });
-        }
+        const deletions = cancelledPermissions.map(({ permissionUuid }) =>
+            permissionService.delete(permissionUuid));
+        const updates = permissions.map(update =>
+            permissionService.update(update.permissionUuid, { name: update.permissions }));
+        await Promise.all([...deletions, ...updates]);
     }
 };
 
@@ -217,31 +197,13 @@ const sendInvitations = async (_: Dispatch, getState: () => RootState, { permiss
     const dialog = getDialog<SharingDialogData>(state.dialog, SHARING_DIALOG_NAME);
     if (dialog && user) {
         const invitations = getFormValues(SHARING_INVITATION_FORM_NAME)(state) as SharingInvitationFormData;
-
-        const getGroupsFromForm = invitations.invitedPeople.filter((invitation) => extractUuidKind(invitation.uuid) === ResourceKind.GROUP);
-        const getUsersFromForm = invitations.invitedPeople.filter((invitation) => extractUuidKind(invitation.uuid) === ResourceKind.USER);
-
-        const invitationDataUsers = getUsersFromForm
-            .map(person => ({
-                ownerUuid: user.uuid,
-                headUuid: dialog.data.resourceUuid,
-                tailUuid: person.uuid,
-                name: invitations.permissions
-            }));
-
-        const invitationsDataGroups = getGroupsFromForm.map(
-            group => ({
-                ownerUuid: user.uuid,
-                headUuid: dialog.data.resourceUuid,
-                tailUuid: group.uuid,
-                name: invitations.permissions
-            })
-        );
-
-        const data = invitationDataUsers.concat(invitationsDataGroups);
-
-        for (const invitation of data) {
-            await permissionService.create(invitation);
-        }
+        const data = invitations.invitedPeople.map(invitee => ({
+            ownerUuid: user.uuid,
+            headUuid: dialog.data.resourceUuid,
+            tailUuid: invitee.uuid,
+            name: invitations.permissions
+        }));
+        const changes = data.map( invitation => permissionService.create(invitation));
+        await Promise.all(changes);
     }
 };

commit 282606d6586ed26675ef27f9f53cfeb0375dafc8
Author: Lucas Di Pentima <lucas.dipentima at curii.com>
Date:   Fri May 13 11:30:23 2022 -0300

    16115: Further sharing dialog cleanup.
    
    * Removes code related to the "public access" form.
    * Avoids filtering out permissions from anonymous users.
    
    Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <lucas.dipentima at curii.com>

diff --git a/src/store/sharing-dialog/sharing-dialog-actions.ts b/src/store/sharing-dialog/sharing-dialog-actions.ts
index bb3b692f..985b345e 100644
--- a/src/store/sharing-dialog/sharing-dialog-actions.ts
+++ b/src/store/sharing-dialog/sharing-dialog-actions.ts
@@ -4,7 +4,13 @@
 
 import { dialogActions } from "store/dialog/dialog-actions";
 import { withDialog } from "store/dialog/with-dialog";
-import { SHARING_DIALOG_NAME, SharingPublicAccessFormData, SHARING_PUBLIC_ACCESS_FORM_NAME, SHARING_INVITATION_FORM_NAME, SharingManagementFormData, SharingInvitationFormData, VisibilityLevel, getSharingMangementFormData, getSharingPublicAccessFormData } from './sharing-dialog-types';
+import {
+    SHARING_DIALOG_NAME,
+    SHARING_INVITATION_FORM_NAME,
+    SharingManagementFormData,
+    SharingInvitationFormData,
+    getSharingMangementFormData,
+} from './sharing-dialog-types';
 import { Dispatch } from 'redux';
 import { ServiceRepository } from "services/services";
 import { FilterBuilder } from 'services/api/filter-builder';
@@ -13,13 +19,17 @@ import { SHARING_MANAGEMENT_FORM_NAME } from 'store/sharing-dialog/sharing-dialo
 import { RootState } from 'store/store';
 import { getDialog } from 'store/dialog/dialog-reducer';
 import { PermissionLevel } from 'models/permission';
-import { getPublicGroupUuid } from "store/workflow-panel/workflow-panel-actions";
 import { PermissionResource } from 'models/permission';
 import { differenceWith } from "lodash";
 import { withProgress } from "store/progress-indicator/with-progress";
 import { progressIndicatorActions } from 'store/progress-indicator/progress-indicator-actions';
 import { snackbarActions, SnackbarKind } from "../snackbar/snackbar-actions";
-import { extractUuidKind, extractUuidObjectType, ResourceKind, ResourceObjectType } from "models/resource";
+import {
+    extractUuidKind,
+    extractUuidObjectType,
+    ResourceKind,
+    ResourceObjectType
+} from "models/resource";
 import { resourcesActions } from "store/resources/resources-actions";
 
 export const openSharingDialog = (resourceUuid: string, refresh?: () => void) =>
@@ -37,7 +47,6 @@ export const connectSharingDialogProgress = withProgress(SHARING_DIALOG_NAME);
 
 export const saveSharingDialogChanges = async (dispatch: Dispatch, getState: () => RootState) => {
     dispatch(progressIndicatorActions.START_WORKING(SHARING_DIALOG_NAME));
-    await dispatch<any>(savePublicPermissionChanges);
     await dispatch<any>(saveManagementChanges);
     await dispatch<any>(sendInvitations);
     dispatch(reset(SHARING_INVITATION_FORM_NAME));
@@ -127,7 +136,6 @@ const loadSharingDialog = async (dispatch: Dispatch, getState: () => RootState,
         try {
             const resourceUuid = dialog.data.resourceUuid;
             const { items } = await permissionService.listResourcePermissions(resourceUuid);
-            dispatch<any>(initializePublicAccessForm(items));
             await dispatch<any>(initializeManagementForm(items));
             // For collections, we need to load the public sharing tokens
             if (extractUuidObjectType(resourceUuid) === ResourceObjectType.COLLECTION) {
@@ -167,8 +175,6 @@ const initializeManagementForm = (permissionLinks: PermissionResource[]) =>
         };
 
         const managementPermissions = permissionLinks
-            .filter(item =>
-                item.tailUuid !== getPublicGroupUuid(getState()))
             .map(({ tailUuid, name, uuid }) => ({
                 email: getEmail(tailUuid),
                 permissions: name as PermissionLevel,
@@ -183,86 +189,29 @@ const initializeManagementForm = (permissionLinks: PermissionResource[]) =>
         dispatch(initialize(SHARING_MANAGEMENT_FORM_NAME, managementFormData));
     };
 
-const initializePublicAccessForm = (permissionLinks: PermissionResource[]) =>
-    (dispatch: Dispatch, getState: () => RootState, ) => {
-
-        const [publicPermission] = permissionLinks
-            .filter(item => item.tailUuid === getPublicGroupUuid(getState()));
-
-        const publicAccessFormData: SharingPublicAccessFormData = publicPermission
-            ? {
-                visibility: VisibilityLevel.PUBLIC,
-                permissionUuid: publicPermission.uuid,
-            }
-            : {
-                visibility: permissionLinks.length > 0
-                    ? VisibilityLevel.SHARED
-                    : VisibilityLevel.PRIVATE,
-                permissionUuid: '',
-            };
-
-        dispatch(initialize(SHARING_PUBLIC_ACCESS_FORM_NAME, publicAccessFormData));
-    };
-
-const savePublicPermissionChanges = async (_: Dispatch, getState: () => RootState, { permissionService }: ServiceRepository) => {
-    const state = getState();
-    const { user } = state.auth;
-    const dialog = getDialog<SharingDialogData>(state.dialog, SHARING_DIALOG_NAME);
-    if (dialog && user) {
-        const { permissionUuid, visibility } = getSharingPublicAccessFormData(state);
-
-        if (permissionUuid) {
-            if (visibility === VisibilityLevel.PUBLIC) {
-                await permissionService.update(permissionUuid, {
-                    name: PermissionLevel.CAN_READ
-                });
-            } else {
-                await permissionService.delete(permissionUuid);
-            }
-
-        } else if (visibility === VisibilityLevel.PUBLIC) {
-
-            await permissionService.create({
-                ownerUuid: user.uuid,
-                headUuid: dialog.data.resourceUuid,
-                tailUuid: getPublicGroupUuid(state),
-                name: PermissionLevel.CAN_READ,
-            });
-        }
-    }
-};
-
 const saveManagementChanges = async (_: Dispatch, getState: () => RootState, { permissionService }: ServiceRepository) => {
     const state = getState();
     const { user } = state.auth;
     const dialog = getDialog<string>(state.dialog, SHARING_DIALOG_NAME);
     if (dialog && user) {
         const { initialPermissions, permissions } = getSharingMangementFormData(state);
-        const { visibility } = getSharingPublicAccessFormData(state);
-
-        if (visibility === VisibilityLevel.PRIVATE) {
-            for (const permission of initialPermissions) {
-                await permissionService.delete(permission.permissionUuid);
-            }
-        } else {
-            const cancelledPermissions = differenceWith(
-                initialPermissions,
-                permissions,
-                (a, b) => a.permissionUuid === b.permissionUuid
-            );
+        const cancelledPermissions = differenceWith(
+            initialPermissions,
+            permissions,
+            (a, b) => a.permissionUuid === b.permissionUuid
+        );
 
-            for (const { permissionUuid } of cancelledPermissions) {
-                await permissionService.delete(permissionUuid);
-            }
+        for (const { permissionUuid } of cancelledPermissions) {
+            await permissionService.delete(permissionUuid);
+        }
 
-            for (const permission of permissions) {
-                await permissionService.update(permission.permissionUuid, { name: permission.permissions });
-            }
+        for (const permission of permissions) {
+            await permissionService.update(permission.permissionUuid, { name: permission.permissions });
         }
     }
 };
 
-const sendInvitations = async (_: Dispatch, getState: () => RootState, { permissionService, userService }: ServiceRepository) => {
+const sendInvitations = async (_: Dispatch, getState: () => RootState, { permissionService }: ServiceRepository) => {
     const state = getState();
     const { user } = state.auth;
     const dialog = getDialog<SharingDialogData>(state.dialog, SHARING_DIALOG_NAME);
diff --git a/src/store/sharing-dialog/sharing-dialog-types.ts b/src/store/sharing-dialog/sharing-dialog-types.ts
index a05224e2..7ca8b5c5 100644
--- a/src/store/sharing-dialog/sharing-dialog-types.ts
+++ b/src/store/sharing-dialog/sharing-dialog-types.ts
@@ -7,21 +7,9 @@ import { getFormValues, isDirty } from 'redux-form';
 import { RootState } from 'store/store';
 
 export const SHARING_DIALOG_NAME = 'SHARING_DIALOG_NAME';
-export const SHARING_PUBLIC_ACCESS_FORM_NAME = 'SHARING_PUBLIC_ACCESS_FORM_NAME';
 export const SHARING_MANAGEMENT_FORM_NAME = 'SHARING_MANAGEMENT_FORM_NAME';
 export const SHARING_INVITATION_FORM_NAME = 'SHARING_INVITATION_FORM_NAME';
 
-export enum VisibilityLevel {
-    PRIVATE = 'Private',
-    SHARED = 'Shared',
-    PUBLIC = 'Public',
-}
-
-export interface SharingPublicAccessFormData {
-    visibility: VisibilityLevel;
-    permissionUuid: string;
-}
-
 export interface SharingManagementFormData {
     permissions: SharingManagementFormDataRow[];
     initialPermissions: SharingManagementFormDataRow[];
@@ -47,10 +35,6 @@ export interface SharingInvitationFormPersonData {
 export const getSharingMangementFormData = (state: any) =>
     getFormValues(SHARING_MANAGEMENT_FORM_NAME)(state) as SharingManagementFormData;
 
-export const getSharingPublicAccessFormData = (state: any) =>
-    getFormValues(SHARING_PUBLIC_ACCESS_FORM_NAME)(state) as SharingPublicAccessFormData;
-
 export const hasChanges = (state: RootState) =>
-    isDirty(SHARING_PUBLIC_ACCESS_FORM_NAME)(state) ||
     isDirty(SHARING_MANAGEMENT_FORM_NAME)(state) ||
     isDirty(SHARING_INVITATION_FORM_NAME)(state);
diff --git a/src/views-components/sharing-dialog/sharing-public-access-form-component.tsx b/src/views-components/sharing-dialog/sharing-public-access-form-component.tsx
deleted file mode 100644
index 8fb427af..00000000
--- a/src/views-components/sharing-dialog/sharing-public-access-form-component.tsx
+++ /dev/null
@@ -1,53 +0,0 @@
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import React from 'react';
-import { Grid, StyleRulesCallback, Divider, Typography } from '@material-ui/core';
-import { Field, WrappedFieldProps } from 'redux-form';
-import { WithStyles } from '@material-ui/core/styles';
-import withStyles from '@material-ui/core/styles/withStyles';
-import { VisibilityLevelSelect } from './visibility-level-select';
-import { VisibilityLevel } from 'store/sharing-dialog/sharing-dialog-types';
-
-const sharingPublicAccessStyles: StyleRulesCallback<'root'> = theme => ({
-    root: {
-        padding: `${theme.spacing.unit * 2}px 0`,
-    }
-});
-
-const SharingPublicAccessForm = withStyles(sharingPublicAccessStyles)(
-    ({ classes, visibility }: WithStyles<'root'> & { visibility: VisibilityLevel }) =>
-        <>
-            <Divider />
-            <Grid container alignItems='center' spacing={8} className={classes.root}>
-                <Grid item xs={8}>
-                    <Typography variant='subtitle1'>
-                        {renderVisibilityInfo(visibility)}
-                    </Typography>
-                </Grid>
-                <Grid item xs={4} container wrap='nowrap'>
-                    <Field name='visibility' component={VisibilityLevelSelectComponent} />
-                </Grid>
-            </Grid>
-        </>
-);
-
-const renderVisibilityInfo = (visibility: VisibilityLevel) => {
-    switch (visibility) {
-        case VisibilityLevel.PUBLIC:
-            return 'Anyone can access';
-        case VisibilityLevel.SHARED:
-            return 'Specific people can access';
-        case VisibilityLevel.PRIVATE:
-            return 'Only you can access';
-        default:
-            return '';
-    }
-};
-
-export default ({ visibility }: { visibility: VisibilityLevel }) =>
-    <SharingPublicAccessForm {...{ visibility }} />;
-
-const VisibilityLevelSelectComponent = ({ input }: WrappedFieldProps) =>
-    <VisibilityLevelSelect fullWidth disableUnderline {...input} />;
diff --git a/src/views-components/sharing-dialog/sharing-public-access-form.tsx b/src/views-components/sharing-dialog/sharing-public-access-form.tsx
deleted file mode 100644
index 2a216b04..00000000
--- a/src/views-components/sharing-dialog/sharing-public-access-form.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import { reduxForm } from 'redux-form';
-import { compose } from 'redux';
-import { connect } from 'react-redux';
-import SharingPublicAccessFormComponent from './sharing-public-access-form-component';
-import { SHARING_PUBLIC_ACCESS_FORM_NAME } from 'store/sharing-dialog/sharing-dialog-types';
-import { RootState } from 'store/store';
-import { getSharingPublicAccessFormData } from '../../store/sharing-dialog/sharing-dialog-types';
-
-export const SharingPublicAccessForm = compose(
-    reduxForm(
-        { form: SHARING_PUBLIC_ACCESS_FORM_NAME }
-    ),
-    connect(
-        (state: RootState) => {
-            const { visibility } = getSharingPublicAccessFormData(state);
-            return { visibility };
-        }
-    )
-)(SharingPublicAccessFormComponent);
diff --git a/src/views-components/sharing-dialog/visibility-level-select.tsx b/src/views-components/sharing-dialog/visibility-level-select.tsx
deleted file mode 100644
index 5746de1f..00000000
--- a/src/views-components/sharing-dialog/visibility-level-select.tsx
+++ /dev/null
@@ -1,54 +0,0 @@
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import React from 'react';
-import { MenuItem, Select, withStyles, StyleRulesCallback } from '@material-ui/core';
-import Lock from '@material-ui/icons/Lock';
-import People from '@material-ui/icons/People';
-import Public from '@material-ui/icons/Public';
-import { WithStyles } from '@material-ui/core/styles';
-import { SelectProps } from '@material-ui/core/Select';
-import { SelectItem } from './select-item';
-import { VisibilityLevel } from 'store/sharing-dialog/sharing-dialog-types';
-
-
-type VisibilityLevelSelectClasses = 'value';
-
-const VisibilityLevelSelectStyles: StyleRulesCallback<VisibilityLevelSelectClasses> = theme => ({
-    value: {
-        marginLeft: theme.spacing.unit,
-    }
-});
-export const VisibilityLevelSelect = withStyles(VisibilityLevelSelectStyles)(
-    ({ classes, ...props }: SelectProps & WithStyles<VisibilityLevelSelectClasses>) =>
-        <Select
-            {...props}
-            renderValue={renderPermissionItem}
-            inputProps={{ classes }}>
-            <MenuItem value={VisibilityLevel.PUBLIC}>
-                {renderPermissionItem(VisibilityLevel.PUBLIC)}
-            </MenuItem>
-            <MenuItem value={VisibilityLevel.SHARED}>
-                {renderPermissionItem(VisibilityLevel.SHARED)}
-            </MenuItem>
-            <MenuItem value={VisibilityLevel.PRIVATE}>
-                {renderPermissionItem(VisibilityLevel.PRIVATE)}
-            </MenuItem>
-        </Select>);
-
-const renderPermissionItem = (value: string) =>
-    <SelectItem {...{ value, icon: getIcon(value) }} />;
-
-const getIcon = (value: string) => {
-    switch (value) {
-        case VisibilityLevel.PUBLIC:
-            return Public;
-        case VisibilityLevel.SHARED:
-            return People;
-        case VisibilityLevel.PRIVATE:
-            return Lock;
-        default:
-            return Lock;
-    }
-};

commit e239324108497e94222407d62b9c70ddffa42ab4
Author: Lucas Di Pentima <lucas.dipentima at curii.com>
Date:   Fri May 13 10:44:30 2022 -0300

    16115: Updates the sharing dialog to support sharing URLs.
    
    * When dealing with collections, adds a new tab for sharing URLs.
    * Removes the "Advanced" mode and always show the permissions.
    * Moves the "invitation form" to the dialog's action section so that it
      keeps being visible when lots of permissions are set.
    * Allow closing the dialog by clicking away or using the Esc key when no
      pending changes need saving.
    
    Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <lucas.dipentima at curii.com>

diff --git a/src/store/sharing-dialog/sharing-dialog-actions.ts b/src/store/sharing-dialog/sharing-dialog-actions.ts
index 53c751e1..bb3b692f 100644
--- a/src/store/sharing-dialog/sharing-dialog-actions.ts
+++ b/src/store/sharing-dialog/sharing-dialog-actions.ts
@@ -20,7 +20,6 @@ import { withProgress } from "store/progress-indicator/with-progress";
 import { progressIndicatorActions } from 'store/progress-indicator/progress-indicator-actions';
 import { snackbarActions, SnackbarKind } from "../snackbar/snackbar-actions";
 import { extractUuidKind, extractUuidObjectType, ResourceKind, ResourceObjectType } from "models/resource";
-import { ApiClientAuthorizationService } from "services/api-client-authorization-service/api-client-authorization-service";
 import { resourcesActions } from "store/resources/resources-actions";
 
 export const openSharingDialog = (resourceUuid: string, refresh?: () => void) =>
diff --git a/src/views-components/sharing-dialog/advanced-view-switch.tsx b/src/views-components/sharing-dialog/advanced-view-switch.tsx
deleted file mode 100644
index 969128be..00000000
--- a/src/views-components/sharing-dialog/advanced-view-switch.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import React from 'react';
-
-export interface AdvancedViewSwitchInjectedProps {
-    toggleAdvancedView: () => void;
-    advancedViewOpen: boolean;
-}
-
-export const connectAdvancedViewSwitch = (Component: React.ComponentType<AdvancedViewSwitchInjectedProps>) =>
-    class extends React.Component<{}, { advancedViewOpen: boolean }> {
-
-        state = { advancedViewOpen: false };
-
-        toggleAdvancedView = () => {
-            this.setState(({ advancedViewOpen }) => ({ advancedViewOpen: !advancedViewOpen }));
-        }
-
-        render() {
-            return <Component {...this.state} {...this} />;
-        }
-    };
-    
\ No newline at end of file
diff --git a/src/views-components/sharing-dialog/sharing-dialog-component.tsx b/src/views-components/sharing-dialog/sharing-dialog-component.tsx
index be15cce6..eca6dc2c 100644
--- a/src/views-components/sharing-dialog/sharing-dialog-component.tsx
+++ b/src/views-components/sharing-dialog/sharing-dialog-component.tsx
@@ -3,56 +3,99 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import React from 'react';
-import { Dialog, DialogTitle, Button, Grid, DialogContent, CircularProgress, Paper } from '@material-ui/core';
+import {
+    Dialog,
+    DialogTitle,
+    Button,
+    Grid,
+    DialogContent,
+    CircularProgress,
+    Paper,
+    Tabs,
+    Tab,
+} from '@material-ui/core';
+import {
+    StyleRulesCallback,
+    WithStyles,
+    withStyles
+} from '@material-ui/core/styles';
 import { DialogActions } from 'components/dialog-actions/dialog-actions';
-import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles';
-
+import { SharingDialogContent } from './sharing-dialog-content';
+import { SharingURLsContent } from './sharing-urls';
+import {
+    extractUuidObjectType,
+    ResourceObjectType
+} from 'models/resource';
+import { SharingInvitationForm } from './sharing-invitation-form';
 
 export interface SharingDialogDataProps {
     open: boolean;
     loading: boolean;
     saveEnabled: boolean;
-    advancedEnabled: boolean;
-    children: React.ReactNode;
+    sharedResourceUuid: string;
 }
 export interface SharingDialogActionProps {
     onClose: () => void;
-    onExited: () => void;
     onSave: () => void;
-    onAdvanced: () => void;
+    onCreateSharingToken: () => void;
+}
+enum SharingDialogTab {
+    PERMISSIONS = 0,
+    URLS = 1,
 }
 export default (props: SharingDialogDataProps & SharingDialogActionProps) => {
-    const { children, open, loading, advancedEnabled, saveEnabled, onAdvanced, onClose, onExited, onSave } = props;
+    const { open, loading, saveEnabled, sharedResourceUuid,
+        onClose, onSave, onCreateSharingToken } = props;
+    const showTabs = extractUuidObjectType(sharedResourceUuid) === ResourceObjectType.COLLECTION;
+    const [tabNr, setTabNr] = React.useState<number>(SharingDialogTab.PERMISSIONS);
+
+    // Sets up the dialog depending on the resource type
+    if (!showTabs && tabNr !== SharingDialogTab.PERMISSIONS) {
+        setTabNr(SharingDialogTab.PERMISSIONS);
+    }
+
     return <Dialog
-        {...{ open, onClose, onExited }}
+        {...{ open, onClose }}
         className="sharing-dialog"
         fullWidth
         maxWidth='sm'
-        disableBackdropClick
-        disableEscapeKeyDown>
+        disableBackdropClick={saveEnabled}
+        disableEscapeKeyDown={saveEnabled}>
         <DialogTitle>
             Sharing settings
-            </DialogTitle>
+        </DialogTitle>
+        { showTabs &&
+        <Tabs value={tabNr} onChange={(_, tb) => setTabNr(tb)}>
+            <Tab label="With users/groups" />
+            <Tab label="Sharing URLs" disabled={saveEnabled} />
+        </Tabs>
+        }
         <DialogContent>
-            {children}
+            { tabNr === SharingDialogTab.PERMISSIONS &&
+            <SharingDialogContent />
+            }
+            { tabNr === SharingDialogTab.URLS &&
+            <SharingURLsContent uuid={sharedResourceUuid} />
+            }
         </DialogContent>
         <DialogActions>
             <Grid container spacing={8}>
-                {advancedEnabled &&
-                    <Grid item>
-                        <Button
-                            color='primary'
-                            onClick={onAdvanced}>
-                            Advanced
-                    </Button>
-                    </Grid>
-                }
-                <Grid item xs />
+                { tabNr === SharingDialogTab.PERMISSIONS &&
+                <Grid item md={12}>
+                    <SharingInvitationForm />
+                </Grid> }
+                { tabNr === SharingDialogTab.URLS &&
                 <Grid item>
-                    <Button onClick={onClose}>
-                        Close
+                    <Button
+                        variant="contained"
+                        color="primary"
+                        onClick={onCreateSharingToken}>
+                        Create sharing URL
                     </Button>
                 </Grid>
+                }
+                <Grid item xs />
+                { tabNr === SharingDialogTab.PERMISSIONS &&
                 <Grid item>
                     <Button
                         variant='contained'
@@ -62,6 +105,12 @@ export default (props: SharingDialogDataProps & SharingDialogActionProps) => {
                         Save
                     </Button>
                 </Grid>
+                }
+                <Grid item>
+                    <Button onClick={onClose}>
+                        Close
+                    </Button>
+                </Grid>
             </Grid>
         </DialogActions>
         {
diff --git a/src/views-components/sharing-dialog/sharing-dialog-content.tsx b/src/views-components/sharing-dialog/sharing-dialog-content.tsx
index 15df2245..ee1ccf87 100644
--- a/src/views-components/sharing-dialog/sharing-dialog-content.tsx
+++ b/src/views-components/sharing-dialog/sharing-dialog-content.tsx
@@ -3,26 +3,13 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import React from 'react';
-import { Grid, Typography } from '@material-ui/core';
+import { Grid } from '@material-ui/core';
 
-import { SharingInvitationForm } from './sharing-invitation-form';
 import { SharingManagementForm } from './sharing-management-form';
-import { SharingPublicAccessForm } from './sharing-public-access-form';
 
-export const SharingDialogContent = (props: { advancedViewOpen: boolean }) =>
+export const SharingDialogContent = () =>
     <Grid container direction='column' spacing={24}>
-        {props.advancedViewOpen &&
-            <>
-                <Grid item>
-                    <Typography variant='subtitle1'>
-                        Who can access
-                    </Typography>
-                    <SharingPublicAccessForm />
-                    <SharingManagementForm />
-                </Grid>
-            </>
-        }
         <Grid item>
-            <SharingInvitationForm />
+            <SharingManagementForm />
         </Grid>
     </Grid>;
diff --git a/src/views-components/sharing-dialog/sharing-dialog.tsx b/src/views-components/sharing-dialog/sharing-dialog.tsx
index fe3b8396..a077f7ee 100644
--- a/src/views-components/sharing-dialog/sharing-dialog.tsx
+++ b/src/views-components/sharing-dialog/sharing-dialog.tsx
@@ -4,48 +4,50 @@
 
 import { compose, Dispatch } from 'redux';
 import { connect } from 'react-redux';
-
-import React from 'react';
-import { connectSharingDialog, saveSharingDialogChanges, connectSharingDialogProgress, sendSharingInvitations } from 'store/sharing-dialog/sharing-dialog-actions';
-import { WithDialogProps } from 'store/dialog/with-dialog';
 import { RootState } from 'store/store';
-
-import SharingDialogComponent, { SharingDialogDataProps, SharingDialogActionProps } from './sharing-dialog-component';
-import { SharingDialogContent } from './sharing-dialog-content';
-import { connectAdvancedViewSwitch, AdvancedViewSwitchInjectedProps } from './advanced-view-switch';
-import { hasChanges } from 'store/sharing-dialog/sharing-dialog-types';
+import {
+    connectSharingDialog,
+    saveSharingDialogChanges,
+    connectSharingDialogProgress,
+    SharingDialogData,
+    createSharingToken
+} from 'store/sharing-dialog/sharing-dialog-actions';
+import { WithDialogProps } from 'store/dialog/with-dialog';
+import SharingDialogComponent, {
+    SharingDialogDataProps,
+    SharingDialogActionProps
+} from './sharing-dialog-component';
+import {
+    hasChanges,
+    SHARING_DIALOG_NAME
+} from 'store/sharing-dialog/sharing-dialog-types';
 import { WithProgressStateProps } from 'store/progress-indicator/with-progress';
+import { getDialog } from 'store/dialog/dialog-reducer';
 
-type Props = WithDialogProps<string> & AdvancedViewSwitchInjectedProps & WithProgressStateProps;
+type Props = WithDialogProps<string> & WithProgressStateProps;
 
-const mapStateToProps = (state: RootState, { advancedViewOpen, working, ...props }: Props): SharingDialogDataProps => ({
+const mapStateToProps = (state: RootState, { working, ...props }: Props): SharingDialogDataProps => {
+    const dialog = getDialog<SharingDialogData>(state.dialog, SHARING_DIALOG_NAME);
+    return ({
     ...props,
     saveEnabled: hasChanges(state),
     loading: working,
-    advancedEnabled: !advancedViewOpen,
-    children: <SharingDialogContent {...{ advancedViewOpen }} />,
-});
+    sharedResourceUuid: dialog?.data.resourceUuid || '',
+    })
+};
 
-const mapDispatchToProps = (dispatch: Dispatch, { toggleAdvancedView, advancedViewOpen, ...props }: Props): SharingDialogActionProps => ({
+const mapDispatchToProps = (dispatch: Dispatch, { ...props }: Props): SharingDialogActionProps => ({
     ...props,
     onClose: props.closeDialog,
-    onExited: () => {
-        if (advancedViewOpen) {
-            toggleAdvancedView();
-        }
-    },
     onSave: () => {
-        if (advancedViewOpen) {
-            dispatch<any>(saveSharingDialogChanges);
-        } else {
-            dispatch<any>(sendSharingInvitations);
-        }
+        dispatch<any>(saveSharingDialogChanges);
     },
-    onAdvanced: toggleAdvancedView,
+    onCreateSharingToken: () => {
+        dispatch<any>(createSharingToken);
+    }
 });
 
 export const SharingDialog = compose(
-    connectAdvancedViewSwitch,
     connectSharingDialog,
     connectSharingDialogProgress,
     connect(mapStateToProps, mapDispatchToProps)

commit a4a67b0a5436effb92f682c6edb512700420c374
Author: Lucas Di Pentima <lucas.dipentima at curii.com>
Date:   Thu May 12 15:47:07 2022 -0300

    16115: Adds collection's sharing URL management component and actions.
    
    Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <lucas.dipentima at curii.com>

diff --git a/src/store/sharing-dialog/sharing-dialog-actions.ts b/src/store/sharing-dialog/sharing-dialog-actions.ts
index 4c0b8825..53c751e1 100644
--- a/src/store/sharing-dialog/sharing-dialog-actions.ts
+++ b/src/store/sharing-dialog/sharing-dialog-actions.ts
@@ -19,7 +19,9 @@ import { differenceWith } from "lodash";
 import { withProgress } from "store/progress-indicator/with-progress";
 import { progressIndicatorActions } from 'store/progress-indicator/progress-indicator-actions';
 import { snackbarActions, SnackbarKind } from "../snackbar/snackbar-actions";
-import { extractUuidKind, ResourceKind } from "models/resource";
+import { extractUuidKind, extractUuidObjectType, ResourceKind, ResourceObjectType } from "models/resource";
+import { ApiClientAuthorizationService } from "services/api-client-authorization-service/api-client-authorization-service";
+import { resourcesActions } from "store/resources/resources-actions";
 
 export const openSharingDialog = (resourceUuid: string, refresh?: () => void) =>
     (dispatch: Dispatch) => {
@@ -41,6 +43,7 @@ export const saveSharingDialogChanges = async (dispatch: Dispatch, getState: ()
     await dispatch<any>(sendInvitations);
     dispatch(reset(SHARING_INVITATION_FORM_NAME));
     await dispatch<any>(loadSharingDialog);
+    dispatch(progressIndicatorActions.STOP_WORKING(SHARING_DIALOG_NAME));
 
     const dialog = getDialog<SharingDialogData>(getState().dialog, SHARING_DIALOG_NAME);
     if (dialog && dialog.data.refresh) {
@@ -57,31 +60,88 @@ export const sendSharingInvitations = async (dispatch: Dispatch, getState: () =>
         kind: SnackbarKind.SUCCESS,
     }));
     dispatch(progressIndicatorActions.STOP_WORKING(SHARING_DIALOG_NAME));
-    
+
     const dialog = getDialog<SharingDialogData>(getState().dialog, SHARING_DIALOG_NAME);
     if (dialog && dialog.data.refresh) {
         dialog.data.refresh();
     }
 };
 
-interface SharingDialogData {
+export interface SharingDialogData {
     resourceUuid: string;
     refresh: () => void;
 }
 
-const loadSharingDialog = async (dispatch: Dispatch, getState: () => RootState, { permissionService }: ServiceRepository) => {
+export const createSharingToken = async (dispatch: Dispatch, getState: () => RootState, { apiClientAuthorizationService }: ServiceRepository) => {
+    const dialog = getDialog<SharingDialogData>(getState().dialog, SHARING_DIALOG_NAME);
+    if (dialog) {
+        const resourceUuid = dialog.data.resourceUuid;
+        if (extractUuidObjectType(resourceUuid) === ResourceObjectType.COLLECTION) {
+            dispatch(progressIndicatorActions.START_WORKING(SHARING_DIALOG_NAME));
+            try {
+                const sharingToken = await apiClientAuthorizationService.createCollectionSharingToken(resourceUuid);
+                dispatch(resourcesActions.SET_RESOURCES([sharingToken]));
+                dispatch(snackbarActions.OPEN_SNACKBAR({
+                    message: 'Sharing URL created',
+                    hideDuration: 2000,
+                    kind: SnackbarKind.SUCCESS,
+                }));
+            } catch (e) {
+                dispatch(snackbarActions.OPEN_SNACKBAR({
+                    message: 'Failed to create sharing URL',
+                    hideDuration: 2000,
+                    kind: SnackbarKind.ERROR,
+                }));
+            } finally {
+                dispatch(progressIndicatorActions.STOP_WORKING(SHARING_DIALOG_NAME));
+            }
+        }
+    }
+};
+
+export const deleteSharingToken = (uuid: string) => async (dispatch: Dispatch, getState: () => RootState, { apiClientAuthorizationService }: ServiceRepository) => {
+    dispatch(progressIndicatorActions.START_WORKING(SHARING_DIALOG_NAME));
+    try {
+        await apiClientAuthorizationService.delete(uuid);
+        dispatch(resourcesActions.DELETE_RESOURCES([uuid]));
+        dispatch(snackbarActions.OPEN_SNACKBAR({
+            message: 'Sharing URL removed',
+            hideDuration: 2000,
+            kind: SnackbarKind.SUCCESS,
+        }));
+    } catch (e) {
+        dispatch(snackbarActions.OPEN_SNACKBAR({
+            message: 'Failed to remove sharing URL',
+            hideDuration: 2000,
+            kind: SnackbarKind.ERROR,
+        }));
+    } finally {
+        dispatch(progressIndicatorActions.STOP_WORKING(SHARING_DIALOG_NAME));
+    }
+};
+
+const loadSharingDialog = async (dispatch: Dispatch, getState: () => RootState, { permissionService, apiClientAuthorizationService }: ServiceRepository) => {
 
     const dialog = getDialog<SharingDialogData>(getState().dialog, SHARING_DIALOG_NAME);
     if (dialog) {
         dispatch(progressIndicatorActions.START_WORKING(SHARING_DIALOG_NAME));
         try {
-            const { items } = await permissionService.listResourcePermissions(dialog.data.resourceUuid);
+            const resourceUuid = dialog.data.resourceUuid;
+            const { items } = await permissionService.listResourcePermissions(resourceUuid);
             dispatch<any>(initializePublicAccessForm(items));
             await dispatch<any>(initializeManagementForm(items));
-            dispatch(progressIndicatorActions.STOP_WORKING(SHARING_DIALOG_NAME));
+            // For collections, we need to load the public sharing tokens
+            if (extractUuidObjectType(resourceUuid) === ResourceObjectType.COLLECTION) {
+                const sharingTokens = await apiClientAuthorizationService.listCollectionSharingTokens(resourceUuid);
+                dispatch(resourcesActions.SET_RESOURCES([...sharingTokens.items]));
+            }
         } catch (e) {
-            dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'You do not have access to share this item', hideDuration: 2000, kind: SnackbarKind.ERROR }));
+            dispatch(snackbarActions.OPEN_SNACKBAR({
+                message: 'You do not have access to share this item',
+                hideDuration: 2000,
+                kind: SnackbarKind.ERROR }));
             dispatch(dialogActions.CLOSE_DIALOG({ id: SHARING_DIALOG_NAME }));
+        } finally {
             dispatch(progressIndicatorActions.STOP_WORKING(SHARING_DIALOG_NAME));
         }
     }
@@ -178,19 +238,14 @@ const saveManagementChanges = async (_: Dispatch, getState: () => RootState, { p
     const { user } = state.auth;
     const dialog = getDialog<string>(state.dialog, SHARING_DIALOG_NAME);
     if (dialog && user) {
-
         const { initialPermissions, permissions } = getSharingMangementFormData(state);
         const { visibility } = getSharingPublicAccessFormData(state);
 
-
         if (visibility === VisibilityLevel.PRIVATE) {
-
             for (const permission of initialPermissions) {
                 await permissionService.delete(permission.permissionUuid);
             }
-
         } else {
-
             const cancelledPermissions = differenceWith(
                 initialPermissions,
                 permissions,
@@ -204,7 +259,6 @@ const saveManagementChanges = async (_: Dispatch, getState: () => RootState, { p
             for (const permission of permissions) {
                 await permissionService.update(permission.permissionUuid, { name: permission.permissions });
             }
-
         }
     }
 };
diff --git a/src/views-components/sharing-dialog/sharing-urls-component.tsx b/src/views-components/sharing-dialog/sharing-urls-component.tsx
new file mode 100644
index 00000000..ee5d50be
--- /dev/null
+++ b/src/views-components/sharing-dialog/sharing-urls-component.tsx
@@ -0,0 +1,88 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import React from 'react';
+import {
+    Grid,
+    IconButton,
+    Link,
+    StyleRulesCallback,
+    Tooltip,
+    WithStyles,
+    withStyles
+} from '@material-ui/core';
+import { ApiClientAuthorization } from 'models/api-client-authorization';
+import { CopyIcon, RemoveIcon } from 'components/icon/icon';
+import CopyToClipboard from 'react-copy-to-clipboard';
+import { ArvadosTheme } from 'common/custom-theme';
+import moment from 'moment';
+
+type CssRules = 'sharingUrlText'
+    | 'sharingUrlButton'
+    | 'sharingUrlList'
+    | 'sharingUrlRow';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+    sharingUrlText: {
+        fontSize: '1rem',
+    },
+    sharingUrlButton: {
+        color: theme.palette.grey["500"],
+        cursor: 'pointer',
+        '& svg': {
+            fontSize: '1rem'
+        },
+        verticalAlign: 'middle',
+    },
+    sharingUrlList: {
+        marginTop: '1rem',
+    },
+    sharingUrlRow: {
+        borderBottom: `1px solid ${theme.palette.grey["300"]}`,
+    },
+});
+
+export interface SharingURLsComponentDataProps {
+    collectionUuid: string;
+    sharingTokens: ApiClientAuthorization[];
+    sharingURLsPrefix: string;
+}
+
+export interface SharingURLsComponentActionProps {
+    onDeleteSharingToken: (uuid: string) => void;
+    onCopy: (message: string) => void;
+}
+
+type SharingURLsComponentProps = SharingURLsComponentDataProps & SharingURLsComponentActionProps;
+
+export const SharingURLsComponent = withStyles(styles)((props: SharingURLsComponentProps & WithStyles<CssRules>) => <Grid container direction='column' spacing={24} className={props.classes.sharingUrlList}>
+    { props.sharingTokens
+    .sort((a, b) => (new Date(a.expiresAt).getTime() - new Date(b.expiresAt).getTime()))
+    .map(token => {
+        const url = `${props.sharingURLsPrefix}/c=${props.collectionUuid}/t=${token.apiToken}/_/`
+        const expDate = new Date(token.expiresAt);
+        const urlLabel = `Token ${token.apiToken.slice(0, 8)}... expiring at: ${expDate.toLocaleString()} (${moment(expDate).fromNow()})`;
+
+        return <Grid container alignItems='center' key={token.uuid}  className={props.classes.sharingUrlRow}>
+            <Grid item>
+            <Link className={props.classes.sharingUrlText} href={url} target='_blank'>
+                {urlLabel}
+            </Link>
+            </Grid>
+            <Grid item xs />
+            <Grid item>
+            <span className={props.classes.sharingUrlButton}><Tooltip title='Copy to clipboard'>
+                <CopyToClipboard text={url} onCopy={() => props.onCopy('Sharing URL copied')}>
+                    <CopyIcon />
+                </CopyToClipboard>
+            </Tooltip></span>
+            <span className={props.classes.sharingUrlButton}><Tooltip title='Remove'>
+                <IconButton onClick={() => props.onDeleteSharingToken(token.uuid)}>
+                    <RemoveIcon />
+                </IconButton>
+            </Tooltip></span>
+            </Grid>
+        </Grid>
+    }) }
+</Grid>);
diff --git a/src/views-components/sharing-dialog/sharing-urls.tsx b/src/views-components/sharing-dialog/sharing-urls.tsx
new file mode 100644
index 00000000..6fbf799b
--- /dev/null
+++ b/src/views-components/sharing-dialog/sharing-urls.tsx
@@ -0,0 +1,53 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { RootState } from 'store/store';
+import { connect } from 'react-redux';
+import { Dispatch } from 'redux';
+import { ApiClientAuthorization } from 'models/api-client-authorization';
+import { filterResources } from 'store/resources/resources';
+import { ResourceKind } from 'models/resource';
+import {
+    SharingURLsComponent,
+    SharingURLsComponentActionProps,
+    SharingURLsComponentDataProps
+} from './sharing-urls-component';
+import {
+    snackbarActions,
+    SnackbarKind
+} from 'store/snackbar/snackbar-actions';
+import { deleteSharingToken } from 'store/sharing-dialog/sharing-dialog-actions';
+
+const mapStateToProps =
+    (state: RootState, ownProps: { uuid: string }): SharingURLsComponentDataProps => {
+        const sharingTokens = filterResources(
+            (resource: ApiClientAuthorization) =>
+                resource.kind === ResourceKind.API_CLIENT_AUTHORIZATION  &&
+                resource.scopes.includes(`GET /arvados/v1/collections/${ownProps.uuid}`) &&
+                resource.scopes.includes(`GET /arvados/v1/collections/${ownProps.uuid}/`) &&
+                resource.scopes.includes('GET /arvados/v1/keep_services/accessible')
+            )(state.resources) as ApiClientAuthorization[];
+        const sharingURLsPrefix = state.auth.config.keepWebInlineServiceUrl;
+        return {
+            collectionUuid: ownProps.uuid,
+            sharingTokens,
+            sharingURLsPrefix,
+        }
+    }
+
+const mapDispatchToProps = (dispatch: Dispatch): SharingURLsComponentActionProps => ({
+    onDeleteSharingToken(uuid: string) {
+        dispatch<any>(deleteSharingToken(uuid));
+    },
+    onCopy(message: string) {
+        dispatch(snackbarActions.OPEN_SNACKBAR({
+            message,
+            hideDuration: 2000,
+            kind: SnackbarKind.SUCCESS
+        }));
+    },
+})
+
+export const SharingURLsContent = connect(mapStateToProps, mapDispatchToProps)(SharingURLsComponent)
+

commit d918b7408326b3d1193fc49c8049b5dbbc93a767
Author: Lucas Di Pentima <lucas.dipentima at curii.com>
Date:   Mon May 9 19:50:44 2022 -0300

    16115: Fixes filter building for list equality comparison.
    
    Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <lucas.dipentima at curii.com>

diff --git a/src/services/api-client-authorization-service/api-client-authorization-service.test.ts b/src/services/api-client-authorization-service/api-client-authorization-service.test.ts
index 31fa046e..3a271f53 100644
--- a/src/services/api-client-authorization-service/api-client-authorization-service.test.ts
+++ b/src/services/api-client-authorization-service/api-client-authorization-service.test.ts
@@ -55,11 +55,11 @@ describe('ApiClientAuthorizationService', () => {
             await apiClientAuthorizationService.listCollectionSharingTokens(uuid);
             expect(serverApi.get).toHaveBeenCalledWith(
                 `/api_client_authorizations`, {params: {
-                    filters: '[["scopes","=","' + JSON.stringify([
+                    filters: JSON.stringify([["scopes","=",[
                         `GET /arvados/v1/collections/${uuid}`,
                         `GET /arvados/v1/collections/${uuid}/`,
-                        `GET /arvados/v1/keep_services/accessible`,
-                    ]) + '"]]',
+                        'GET /arvados/v1/keep_services/accessible',
+                    ]]]),
                     select: undefined,
                 }}
             );
diff --git a/src/services/api-client-authorization-service/api-client-authorization-service.ts b/src/services/api-client-authorization-service/api-client-authorization-service.ts
index 012fdb15..7c985dbb 100644
--- a/src/services/api-client-authorization-service/api-client-authorization-service.ts
+++ b/src/services/api-client-authorization-service/api-client-authorization-service.ts
@@ -33,11 +33,11 @@ export class ApiClientAuthorizationService extends CommonService<ApiClientAuthor
         }
         return this.list({
             filters: new FilterBuilder()
-                .addEqual("scopes", JSON.stringify([
+                .addEqual("scopes", [
                     `GET /arvados/v1/collections/${uuid}`,
                     `GET /arvados/v1/collections/${uuid}/`,
-                    "GET /arvados/v1/keep_services/accessible",
-                ])).getFilters()
+                    "GET /arvados/v1/keep_services/accessible"
+                ]).getFilters()
         });
     }
 }
\ No newline at end of file
diff --git a/src/services/api/filter-builder.ts b/src/services/api/filter-builder.ts
index d1a4fd08..4809e7a8 100644
--- a/src/services/api/filter-builder.ts
+++ b/src/services/api/filter-builder.ts
@@ -9,7 +9,7 @@ export function joinFilters(...filters: string[]) {
 export class FilterBuilder {
     constructor(private filters = "") { }
 
-    public addEqual(field: string, value?: string | boolean | null, resourcePrefix?: string) {
+    public addEqual(field: string, value?: string | string[] | boolean | null, resourcePrefix?: string) {
         return this.addCondition(field, "=", value, "", "", resourcePrefix);
     }
 

commit 042f8362da7b7e835d2451169eecafc5ea4d258d
Author: Lucas Di Pentima <lucas.dipentima at curii.com>
Date:   Thu Apr 21 15:03:09 2022 -0300

    16115: Removes unused import.
    
    Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <lucas.dipentima at curii.com>

diff --git a/src/services/api-client-authorization-service/api-client-authorization-service.test.ts b/src/services/api-client-authorization-service/api-client-authorization-service.test.ts
index cc1942cf..31fa046e 100644
--- a/src/services/api-client-authorization-service/api-client-authorization-service.test.ts
+++ b/src/services/api-client-authorization-service/api-client-authorization-service.test.ts
@@ -3,7 +3,6 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import axios, { AxiosInstance } from "axios";
-// import MockAdapter from 'axios-mock-adapter';
 import { ApiClientAuthorizationService } from "./api-client-authorization-service";
 
 
@@ -12,7 +11,6 @@ describe('ApiClientAuthorizationService', () => {
     let serverApi: AxiosInstance;
     let actions;
 
-
     beforeEach(() => {
         serverApi = axios.create();
         actions = {

commit 7baacecfced2112da01ae9b9709109d63f4dfcc3
Author: Lucas Di Pentima <lucas.dipentima at curii.com>
Date:   Mon Apr 18 16:43:39 2022 -0300

    16115: Adds sharing token management methods to ACA service, with tests.
    
    Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <lucas.dipentima at curii.com>

diff --git a/src/services/api-client-authorization-service/api-client-authorization-service.test.ts b/src/services/api-client-authorization-service/api-client-authorization-service.test.ts
new file mode 100644
index 00000000..cc1942cf
--- /dev/null
+++ b/src/services/api-client-authorization-service/api-client-authorization-service.test.ts
@@ -0,0 +1,70 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import axios, { AxiosInstance } from "axios";
+// import MockAdapter from 'axios-mock-adapter';
+import { ApiClientAuthorizationService } from "./api-client-authorization-service";
+
+
+describe('ApiClientAuthorizationService', () => {
+    let apiClientAuthorizationService: ApiClientAuthorizationService;
+    let serverApi: AxiosInstance;
+    let actions;
+
+
+    beforeEach(() => {
+        serverApi = axios.create();
+        actions = {
+            progressFn: jest.fn(),
+        } as any;
+        apiClientAuthorizationService = new ApiClientAuthorizationService(serverApi, actions);
+    });
+
+    describe('createCollectionSharingToken', () => {
+        it('should return error on invalid collection uuid', () => {
+            expect(() => apiClientAuthorizationService.createCollectionSharingToken("foo")).toThrowError("UUID foo is not a collection");
+        });
+
+        it('should make a create request with proper scopes', async () => {
+            serverApi.post = jest.fn(() => Promise.resolve(
+                { data: { uuid: 'zzzzz-4zz18-0123456789abcde' } }
+            ));
+            const uuid = 'zzzzz-4zz18-0123456789abcde'
+            await apiClientAuthorizationService.createCollectionSharingToken(uuid);
+            expect(serverApi.post).toHaveBeenCalledWith(
+                '/api_client_authorizations', {
+                    scopes: [
+                        `GET /arvados/v1/collections/${uuid}`,
+                        `GET /arvados/v1/collections/${uuid}/`,
+                        `GET /arvados/v1/keep_services/accessible`,
+                    ]
+                }
+            );
+        });
+    });
+
+    describe('listCollectionSharingToken', () => {
+        it('should return error on invalid collection uuid', () => {
+            expect(() => apiClientAuthorizationService.listCollectionSharingTokens("foo")).toThrowError("UUID foo is not a collection");
+        });
+
+        it('should make a list request with proper scopes', async () => {
+            serverApi.get = jest.fn(() => Promise.resolve(
+                { data: { items: [{}] } }
+            ));
+            const uuid = 'zzzzz-4zz18-0123456789abcde'
+            await apiClientAuthorizationService.listCollectionSharingTokens(uuid);
+            expect(serverApi.get).toHaveBeenCalledWith(
+                `/api_client_authorizations`, {params: {
+                    filters: '[["scopes","=","' + JSON.stringify([
+                        `GET /arvados/v1/collections/${uuid}`,
+                        `GET /arvados/v1/collections/${uuid}/`,
+                        `GET /arvados/v1/keep_services/accessible`,
+                    ]) + '"]]',
+                    select: undefined,
+                }}
+            );
+        });
+    });
+});
\ No newline at end of file
diff --git a/src/services/api-client-authorization-service/api-client-authorization-service.ts b/src/services/api-client-authorization-service/api-client-authorization-service.ts
index 386c9747..012fdb15 100644
--- a/src/services/api-client-authorization-service/api-client-authorization-service.ts
+++ b/src/services/api-client-authorization-service/api-client-authorization-service.ts
@@ -5,10 +5,39 @@
 import { AxiosInstance } from "axios";
 import { ApiActions } from 'services/api/api-actions';
 import { ApiClientAuthorization } from 'models/api-client-authorization';
-import { CommonService } from 'services/common-service/common-service';
+import { CommonService, ListResults } from 'services/common-service/common-service';
+import { extractUuidObjectType, ResourceObjectType } from "models/resource";
+import { FilterBuilder } from "services/api/filter-builder";
 
 export class ApiClientAuthorizationService extends CommonService<ApiClientAuthorization> {
     constructor(serverApi: AxiosInstance, actions: ApiActions) {
         super(serverApi, "api_client_authorizations", actions);
     }
-} 
\ No newline at end of file
+
+    createCollectionSharingToken(uuid: string): Promise<ApiClientAuthorization> {
+        if (extractUuidObjectType(uuid) !== ResourceObjectType.COLLECTION) {
+            throw new Error(`UUID ${uuid} is not a collection`);
+        }
+        return this.create({
+            scopes: [
+                `GET /arvados/v1/collections/${uuid}`,
+                `GET /arvados/v1/collections/${uuid}/`,
+                `GET /arvados/v1/keep_services/accessible`,
+            ]
+        });
+    }
+
+    listCollectionSharingTokens(uuid: string): Promise<ListResults<ApiClientAuthorization>> {
+        if (extractUuidObjectType(uuid) !== ResourceObjectType.COLLECTION) {
+            throw new Error(`UUID ${uuid} is not a collection`);
+        }
+        return this.list({
+            filters: new FilterBuilder()
+                .addEqual("scopes", JSON.stringify([
+                    `GET /arvados/v1/collections/${uuid}`,
+                    `GET /arvados/v1/collections/${uuid}/`,
+                    "GET /arvados/v1/keep_services/accessible",
+                ])).getFilters()
+        });
+    }
+}
\ No newline at end of file

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


hooks/post-receive
-- 




More information about the arvados-commits mailing list