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) => {
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) {
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));
}
}
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,
for (const permission of permissions) {
await permissionService.update(permission.permissionUuid, { name: permission.permissions });
}
-
}
}
};
--- /dev/null
+// 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>);
--- /dev/null
+// 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)
+