[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