});
cy.get('[role=tooltip]').click();
cy.get('.sharing-dialog').contains('Save').click();
+ cy.get('.sharing-dialog').contains('Close').click();
// Check that both users are present with appropriate permissions
cy.get('[data-cy=group-members-data-explorer]')
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 () {
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);
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').within(() => {
+ cy.contains('Save changes').click();
+ cy.contains('Close').click();
+ });
});
cy.createGroup(adminUser.token, {
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').within(() => {
+ cy.contains('Save changes').click();
+ cy.contains('Close').click();
+ });
});
cy.getAll('@mySharedWritableProject', '@mySharedReadonlyProject')
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}`;
}),
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 };
+ })
+ })
})
+ })
}
)
"version": "0.1.0",
"private": true,
"dependencies": {
+ "@date-io/date-fns": "1",
"@fortawesome/fontawesome-svg-core": "1.2.28",
"@fortawesome/free-solid-svg-icons": "5.13.0",
"@fortawesome/react-fontawesome": "0.1.9",
"caniuse-lite": "1.0.30001299",
"classnames": "2.2.6",
"cwlts": "1.15.29",
+ "date-fns": "^2.28.0",
"debounce": "1.2.0",
"elliptic": "6.5.4",
"file-saver": "2.0.1",
"lodash-es": "4.17.14",
"lodash.mergewith": "4.6.2",
"lodash.template": "4.5.0",
+ "material-ui-pickers": "^2.2.4",
"mem": "4.0.0",
"moment": "2.29.1",
"parse-duration": "0.4.4",
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import axios, { AxiosInstance } from "axios";
+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 and no expiration date', 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`,
+ ]
+ }
+ );
+ });
+
+ it('should make a create request with proper scopes and expiration date', async () => {
+ serverApi.post = jest.fn(() => Promise.resolve(
+ { data: { uuid: 'zzzzz-4zz18-0123456789abcde' } }
+ ));
+ const uuid = 'zzzzz-4zz18-0123456789abcde'
+ const expDate = new Date(2022, 8, 28, 12, 0, 0);
+ await apiClientAuthorizationService.createCollectionSharingToken(uuid, expDate);
+ expect(serverApi.post).toHaveBeenCalledWith(
+ '/api_client_authorizations', {
+ scopes: [
+ `GET /arvados/v1/collections/${uuid}`,
+ `GET /arvados/v1/collections/${uuid}/`,
+ `GET /arvados/v1/keep_services/accessible`,
+ ],
+ expires_at: expDate.toUTCString()
+ }
+ );
+ });
+ });
+
+ 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: JSON.stringify([["scopes","=",[
+ `GET /arvados/v1/collections/${uuid}`,
+ `GET /arvados/v1/collections/${uuid}/`,
+ 'GET /arvados/v1/keep_services/accessible',
+ ]]]),
+ select: undefined,
+ }}
+ );
+ });
+ });
+});
\ No newline at end of file
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, expDate: Date | undefined): Promise<ApiClientAuthorization> {
+ if (extractUuidObjectType(uuid) !== ResourceObjectType.COLLECTION) {
+ throw new Error(`UUID ${uuid} is not a collection`);
+ }
+ const data = {
+ scopes: [
+ `GET /arvados/v1/collections/${uuid}`,
+ `GET /arvados/v1/collections/${uuid}/`,
+ `GET /arvados/v1/keep_services/accessible`,
+ ]
+ }
+ return expDate !== undefined
+ ? this.create({...data, expiresAt: expDate.toUTCString()})
+ : this.create(data);
+ }
+
+ 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", [
+ `GET /arvados/v1/collections/${uuid}`,
+ `GET /arvados/v1/collections/${uuid}/`,
+ "GET /arvados/v1/keep_services/accessible"
+ ]).getFilters()
+ });
+ }
+}
\ No newline at end of file
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);
}
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,
+ SharingPublicAccessFormData,
+ VisibilityLevel,
+ SHARING_PUBLIC_ACCESS_FORM_NAME,
+} from './sharing-dialog-types';
import { Dispatch } from 'redux';
import { ServiceRepository } from "services/services";
import { FilterBuilder } from 'services/api/filter-builder';
import { SHARING_MANAGEMENT_FORM_NAME } from 'store/sharing-dialog/sharing-dialog-types';
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 { PermissionLevel, 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, ResourceKind } from "models/resource";
+import {
+ extractUuidObjectType,
+ ResourceObjectType
+} from "models/resource";
+import { resourcesActions } from "store/resources/resources-actions";
+import { getPublicGroupUuid } from "store/workflow-panel/workflow-panel-actions";
+import { getSharingPublicAccessFormData } from './sharing-dialog-types';
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) {
}
};
-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));
-
+export interface SharingDialogData {
+ resourceUuid: string;
+ refresh: () => void;
+}
+
+export const createSharingToken = (expDate: Date | undefined) => async (dispatch: Dispatch, getState: () => RootState, { apiClientAuthorizationService }: ServiceRepository) => {
const dialog = getDialog<SharingDialogData>(getState().dialog, SHARING_DIALOG_NAME);
- if (dialog && dialog.data.refresh) {
- dialog.data.refresh();
+ 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, expDate);
+ 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));
+ }
+ }
}
};
-interface SharingDialogData {
- resourceUuid: string;
- refresh: () => void;
-}
+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 }: ServiceRepository) => {
+const loadSharingDialog = async (dispatch: Dispatch, getState: () => RootState, { 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);
- dispatch<any>(initializePublicAccessForm(items));
- await dispatch<any>(initializeManagementForm(items));
- dispatch(progressIndicatorActions.STOP_WORKING(SHARING_DIALOG_NAME));
+ const resourceUuid = dialog.data.resourceUuid;
+ 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);
+ 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 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);
+ dispatch<any>(initializePublicAccessForm(permissionLinks));
const filters = new FilterBuilder()
.addIn('uuid', permissionLinks.map(({ tailUuid }) => tailUuid))
.getFilters();
};
dispatch(initialize(SHARING_MANAGEMENT_FORM_NAME, managementFormData));
+ dispatch(progressIndicatorActions.STOP_WORKING(SHARING_DIALOG_NAME));
};
const initializePublicAccessForm = (permissionLinks: PermissionResource[]) =>
(dispatch: Dispatch, getState: () => RootState, ) => {
-
const [publicPermission] = permissionLinks
.filter(item => item.tailUuid === getPublicGroupUuid(getState()));
-
const publicAccessFormData: SharingPublicAccessFormData = publicPermission
? {
visibility: VisibilityLevel.PUBLIC,
: VisibilityLevel.PRIVATE,
permissionUuid: '',
};
-
dispatch(initialize(SHARING_PUBLIC_ACCESS_FORM_NAME, publicAccessFormData));
};
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, {
} else {
await permissionService.delete(permissionUuid);
}
-
} else if (visibility === VisibilityLevel.PUBLIC) {
-
await permissionService.create({
ownerUuid: user.uuid,
headUuid: dialog.data.resourceUuid,
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(
+ const cancelledPermissions = visibility === VisibilityLevel.PRIVATE
+ ? initialPermissions
+ : differenceWith(
initialPermissions,
permissions,
(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]);
}
};
-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);
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);
}
};
+++ /dev/null
-// 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
// 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,
+ Checkbox,
+ FormControlLabel,
+ Typography,
+} 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 { SharingURLsContent } from './sharing-urls';
+import {
+ extractUuidObjectType,
+ ResourceObjectType
+} from 'models/resource';
+import { SharingInvitationForm } from './sharing-invitation-form';
+import { SharingManagementForm } from './sharing-management-form';
+import {
+ BasePicker,
+ Calendar,
+ MuiPickersUtilsProvider,
+ TimePickerView
+} from 'material-ui-pickers';
+import DateFnsUtils from "@date-io/date-fns";
+import moment from 'moment';
+import { SharingPublicAccessForm } from './sharing-public-access-form';
export interface SharingDialogDataProps {
open: boolean;
loading: boolean;
saveEnabled: boolean;
- advancedEnabled: boolean;
- children: React.ReactNode;
+ sharedResourceUuid: string;
+ sharingURLsNr: number;
+ privateAccess: boolean;
}
export interface SharingDialogActionProps {
onClose: () => void;
- onExited: () => void;
onSave: () => void;
- onAdvanced: () => void;
+ onCreateSharingToken: (d: Date | undefined) => () => void;
+ refreshPermissions: () => 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,
+ sharingURLsNr, privateAccess,
+ onClose, onSave, onCreateSharingToken, refreshPermissions } = props;
+ const showTabs = extractUuidObjectType(sharedResourceUuid) === ResourceObjectType.COLLECTION;
+ const [tabNr, setTabNr] = React.useState<number>(SharingDialogTab.PERMISSIONS);
+ const [expDate, setExpDate] = React.useState<Date>();
+ const [withExpiration, setWithExpiration] = React.useState<boolean>(false);
+
+ // Sets up the dialog depending on the resource type
+ if (!showTabs && tabNr !== SharingDialogTab.PERMISSIONS) {
+ setTabNr(SharingDialogTab.PERMISSIONS);
+ }
+
+ React.useEffect(() => {
+ if (!withExpiration) {
+ setExpDate(undefined);
+ } else {
+ setExpDate(moment().add(2, 'hour').minutes(0).seconds(0).toDate());
+ }
+ }, [withExpiration]);
+
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) => {
+ if (tb === SharingDialogTab.PERMISSIONS) {
+ refreshPermissions();
+ }
+ setTabNr(tb)}
+ }>
+ <Tab label="With users/groups" />
+ <Tab label={`Sharing URLs ${sharingURLsNr > 0 ? '('+sharingURLsNr+')' : ''}`} disabled={saveEnabled} />
+ </Tabs>
+ }
<DialogContent>
- {children}
+ { tabNr === SharingDialogTab.PERMISSIONS &&
+ <Grid container direction='column' spacing={24}>
+ <Grid item>
+ <SharingPublicAccessForm />
+ </Grid>
+ <Grid item>
+ <SharingManagementForm />
+ </Grid>
+ </Grid>
+ }
+ { tabNr === SharingDialogTab.URLS &&
+ <SharingURLsContent uuid={sharedResourceUuid} />
+ }
</DialogContent>
<DialogActions>
<Grid container spacing={8}>
- {advancedEnabled &&
- <Grid item>
- <Button
- color='primary'
- onClick={onAdvanced}>
- Advanced
- </Button>
- </Grid>
+ { tabNr === SharingDialogTab.PERMISSIONS &&
+ <Grid item md={12}>
+ <SharingInvitationForm />
+ </Grid>
+ }
+ { tabNr === SharingDialogTab.URLS && withExpiration && <>
+ <Grid item container direction='row' md={12}>
+ <MuiPickersUtilsProvider utils={DateFnsUtils}>
+ <BasePicker autoOk value={expDate} onChange={setExpDate}>
+ {({ date, handleChange }) => (<>
+ <Grid item md={6}>
+ <Calendar date={date} minDate={new Date()} maxDate={undefined}
+ onChange={handleChange} />
+ </Grid>
+ <Grid item md={6}>
+ <TimePickerView type="hours" date={date} ampm={false}
+ onMinutesChange={() => {}}
+ onSecondsChange={() => {}}
+ onHourChange={handleChange}
+ />
+ </Grid>
+ </>)}
+ </BasePicker>
+ </MuiPickersUtilsProvider>
+ </Grid>
+ <Grid item md={12}>
+ <Typography variant='caption' align='center'>
+ Maximum expiration date may be limited by the cluster configuration.
+ </Typography>
+ </Grid>
+ </>
+ }
+ { tabNr === SharingDialogTab.PERMISSIONS && privateAccess && sharingURLsNr > 0 &&
+ <Grid item md={12}>
+ <Typography variant='caption' align='center' color='error'>
+ Although there aren't specific permissions set, this is publicly accessible via Sharing URL(s).
+ </Typography>
+ </Grid>
}
<Grid item xs />
+ { tabNr === SharingDialogTab.URLS && <>
+ <Grid item><FormControlLabel
+ control={<Checkbox color="primary" checked={withExpiration}
+ onChange={(e) => setWithExpiration(e.target.checked)} />}
+ label="With expiration" />
+ </Grid>
<Grid item>
- <Button onClick={onClose}>
- Close
+ <Button variant="contained" color="primary"
+ disabled={expDate !== undefined && expDate <= new Date()}
+ onClick={onCreateSharingToken(expDate)}>
+ Create sharing URL
</Button>
</Grid>
+ </>
+ }
+ { tabNr === SharingDialogTab.PERMISSIONS &&
<Grid item>
- <Button
- variant='contained'
- color='primary'
- onClick={onSave}
+ <Button onClick={onSave} variant="contained" color="primary"
disabled={!saveEnabled}>
- Save
+ Save changes
+ </Button>
+ </Grid>
+ }
+ <Grid item>
+ <Button onClick={() => {
+ onClose();
+ setWithExpiration(false);
+ }}>
+ Close
</Button>
</Grid>
</Grid>
+++ /dev/null
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import React from 'react';
-import { Grid, Typography } 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 }) =>
- <Grid container direction='column' spacing={24}>
- {props.advancedViewOpen &&
- <>
- <Grid item>
- <Typography variant='subtitle1'>
- Who can access
- </Typography>
- <SharingPublicAccessForm />
- <SharingManagementForm />
- </Grid>
- </>
- }
- <Grid item>
- <SharingInvitationForm />
- </Grid>
- </Grid>;
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,
+ initializeManagementForm
+} from 'store/sharing-dialog/sharing-dialog-actions';
+import { WithDialogProps } from 'store/dialog/with-dialog';
+import SharingDialogComponent, {
+ SharingDialogDataProps,
+ SharingDialogActionProps
+} from './sharing-dialog-component';
+import {
+ getSharingPublicAccessFormData,
+ hasChanges,
+ SHARING_DIALOG_NAME,
+ VisibilityLevel
+} from 'store/sharing-dialog/sharing-dialog-types';
import { WithProgressStateProps } from 'store/progress-indicator/with-progress';
+import { getDialog } from 'store/dialog/dialog-reducer';
+import { filterResources } from 'store/resources/resources';
+import { ApiClientAuthorization } from 'models/api-client-authorization';
+import { ResourceKind } from 'models/resource';
-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);
+ const sharedResourceUuid = dialog?.data.resourceUuid || '';
+ return ({
...props,
saveEnabled: hasChanges(state),
loading: working,
- advancedEnabled: !advancedViewOpen,
- children: <SharingDialogContent {...{ advancedViewOpen }} />,
-});
+ sharedResourceUuid,
+ sharingURLsNr: (filterResources(
+ (resource: ApiClientAuthorization) =>
+ resource.kind === ResourceKind.API_CLIENT_AUTHORIZATION &&
+ resource.scopes.includes(`GET /arvados/v1/collections/${sharedResourceUuid}`) &&
+ resource.scopes.includes(`GET /arvados/v1/collections/${sharedResourceUuid}/`) &&
+ resource.scopes.includes('GET /arvados/v1/keep_services/accessible')
+ )(state.resources) as ApiClientAuthorization[]).length,
+ privateAccess: getSharingPublicAccessFormData(state)?.visibility === VisibilityLevel.PRIVATE,
+ })
+};
-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);
+ },
+ onCreateSharingToken: (d: Date) => () => {
+ dispatch<any>(createSharingToken(d));
},
- onAdvanced: toggleAdvancedView,
+ refreshPermissions: () => {
+ dispatch<any>(initializeManagementForm);
+ }
});
export const SharingDialog = compose(
- connectAdvancedViewSwitch,
connectSharingDialog,
connectSharingDialogProgress,
connect(mapStateToProps, mapDispatchToProps)
<FieldArray name='permissions' component={SharingManagementFieldArray as any} />;
const SharingManagementFieldArray = ({ fields }: WrappedFieldArrayProps<{ email: string }>) =>
- <div>
- {
- fields.map((field, index, fields) =>
- <PermissionManagementRow key={field} {...{ field, index, fields }} />)
- }
+ <div>{ fields.map((field, index, fields) =>
+ <PermissionManagementRow key={field} {...{ field, index, fields }} />) }
<Divider />
</div>;
const VisibilityLevelSelectComponent = ({ input }: WrappedFieldProps) =>
<VisibilityLevelSelect fullWidth disableUnderline {...input} />;
+
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 { SHARING_PUBLIC_ACCESS_FORM_NAME, VisibilityLevel } from 'store/sharing-dialog/sharing-dialog-types';
import { RootState } from 'store/store';
import { getSharingPublicAccessFormData } from '../../store/sharing-dialog/sharing-dialog-types';
),
connect(
(state: RootState) => {
- const { visibility } = getSharingPublicAccessFormData(state);
+ const { visibility } = getSharingPublicAccessFormData(state) || { visibility: VisibilityLevel.PRIVATE };
return { visibility };
}
)
)(SharingPublicAccessFormComponent);
+
--- /dev/null
+// 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("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);
+ });
+});
\ No newline at end of file
--- /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,
+ Typography,
+ 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;
+}
+
+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
+ ? props.sharingTokens
+ .sort((a, b) => (new Date(a.expiresAt).getTime() - new Date(b.expiresAt).getTime()))
+ .map(token => {
+ 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.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>
+ <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 data-cy='remove-url-btn' className={props.classes.sharingUrlButton}><Tooltip title='Remove'>
+ <IconButton onClick={() => props.onDeleteSharingToken(token.uuid)}>
+ <RemoveIcon />
+ </IconButton>
+ </Tooltip></span>
+ </Grid>
+ </Grid>
+ })
+ : <Grid item><Typography>No sharing URLs</Typography></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)
+
API:
RequestTimeout: 30s
VocabularyPath: ""
+ MaxTokenLifetime: 24h
TLS:
Insecure: true
Collections:
languageName: node
linkType: hard
+"@date-io/core@npm:^1.3.13":
+ version: 1.3.13
+ resolution: "@date-io/core@npm:1.3.13"
+ checksum: 5a9e9d1de20f0346a3c7d2d5946190caef4bfb0b64d82ba1f4c566657a9192667c94ebe7f438d11d4286d9c190974daad4fb2159294225cd8af4d9a140239879
+ languageName: node
+ linkType: hard
+
+"@date-io/date-fns@npm:1":
+ version: 1.3.13
+ resolution: "@date-io/date-fns@npm:1.3.13"
+ dependencies:
+ "@date-io/core": ^1.3.13
+ peerDependencies:
+ date-fns: ^2.0.0
+ checksum: 0026c0e538ea4add57a11936ff6bdb07e99f25275f8bb28c4702bbb7e82c3a41b3e8124132aa719180d462c01a26a3b4801e41b7349cdb73813749d4bf5e8fbd
+ languageName: node
+ linkType: hard
+
"@fortawesome/fontawesome-common-types@npm:^0.2.28":
version: 0.2.35
resolution: "@fortawesome/fontawesome-common-types@npm:0.2.35"
languageName: node
linkType: hard
+"@types/react-text-mask@npm:^5.4.3":
+ version: 5.4.11
+ resolution: "@types/react-text-mask@npm:5.4.11"
+ dependencies:
+ "@types/react": "*"
+ checksum: 4defba1467e61b73bfdae74d0b1bea0f27846aabf5283f137fa372ef05bf23accfdf04fffaba33272e9eff5abf00a74863e9c24ca6974c731d73f3fae6efc577
+ languageName: node
+ linkType: hard
+
"@types/react-transition-group@npm:^2.0.8":
version: 2.9.2
resolution: "@types/react-transition-group@npm:2.9.2"
version: 0.0.0-use.local
resolution: "arvados-workbench-2@workspace:."
dependencies:
+ "@date-io/date-fns": 1
"@fortawesome/fontawesome-svg-core": 1.2.28
"@fortawesome/free-solid-svg-icons": 5.13.0
"@fortawesome/react-fontawesome": 0.1.9
classnames: 2.2.6
cwlts: 1.15.29
cypress: 6.3.0
+ date-fns: ^2.28.0
debounce: 1.2.0
elliptic: 6.5.4
enzyme: 3.11.0
lodash-es: 4.17.14
lodash.mergewith: 4.6.2
lodash.template: 4.5.0
+ material-ui-pickers: ^2.2.4
mem: 4.0.0
moment: 2.29.1
node-sass: ^4.9.4
languageName: node
linkType: hard
+"clsx@npm:^1.0.2":
+ version: 1.1.1
+ resolution: "clsx@npm:1.1.1"
+ checksum: ff052650329773b9b245177305fc4c4dc3129f7b2be84af4f58dc5defa99538c61d4207be7419405a5f8f3d92007c954f4daba5a7b74e563d5de71c28c830063
+ languageName: node
+ linkType: hard
+
"co@npm:^4.6.0":
version: 4.6.0
resolution: "co@npm:4.6.0"
languageName: node
linkType: hard
+"date-fns@npm:^2.28.0":
+ version: 2.28.0
+ resolution: "date-fns@npm:2.28.0"
+ checksum: a0516b2e4f99b8bffc6cc5193349f185f195398385bdcaf07f17c2c4a24473c99d933eb0018be4142a86a6d46cb0b06be6440ad874f15e795acbedd6fd727a1f
+ languageName: node
+ linkType: hard
+
"debounce@npm:1.2.0":
version: 1.2.0
resolution: "debounce@npm:1.2.0"
languageName: node
linkType: hard
+"material-ui-pickers@npm:^2.2.4":
+ version: 2.2.4
+ resolution: "material-ui-pickers@npm:2.2.4"
+ dependencies:
+ "@types/react-text-mask": ^5.4.3
+ clsx: ^1.0.2
+ react-event-listener: ^0.6.6
+ react-text-mask: ^5.4.3
+ react-transition-group: ^2.5.3
+ tslib: ^1.9.3
+ peerDependencies:
+ "@material-ui/core": ^3.2.0
+ prop-types: ^15.6.0
+ react: ^16.3.0
+ react-dom: ^16.3.0
+ checksum: be93e30a824c347ede9f82c6adc92748807ebc9665f00ed86b62b580748ca03470823871337d554659d6a6cb6d5898d3636a7fed9e4f2d9cbfa295c196d8c008
+ languageName: node
+ linkType: hard
+
"md5.js@npm:^1.3.4":
version: 1.3.5
resolution: "md5.js@npm:1.3.5"
languageName: node
linkType: hard
+"prop-types@npm:^15.5.6":
+ version: 15.8.1
+ resolution: "prop-types@npm:15.8.1"
+ dependencies:
+ loose-envify: ^1.4.0
+ object-assign: ^4.1.1
+ react-is: ^16.13.1
+ checksum: c056d3f1c057cb7ff8344c645450e14f088a915d078dcda795041765047fa080d38e5d626560ccaac94a4e16e3aa15f3557c1a9a8d1174530955e992c675e459
+ languageName: node
+ linkType: hard
+
"proxy-addr@npm:~2.0.5":
version: 2.0.7
resolution: "proxy-addr@npm:2.0.7"
languageName: node
linkType: hard
-"react-event-listener@npm:^0.6.2":
+"react-event-listener@npm:^0.6.2, react-event-listener@npm:^0.6.6":
version: 0.6.6
resolution: "react-event-listener@npm:0.6.6"
dependencies:
languageName: node
linkType: hard
+"react-text-mask@npm:^5.4.3":
+ version: 5.4.3
+ resolution: "react-text-mask@npm:5.4.3"
+ dependencies:
+ prop-types: ^15.5.6
+ peerDependencies:
+ react: ^0.14.0 || ^15.0.0 || ^16.0.0
+ checksum: ee9c560f47d2f67d0193636eeea36852503d6d7bfd16d75ecb8170256606923d786bbb3511971deedbd01136340acf597fe2b6ba0be3cddb2a17a602767eb7b9
+ languageName: node
+ linkType: hard
+
"react-transition-group@npm:2.5.0":
version: 2.5.0
resolution: "react-transition-group@npm:2.5.0"
languageName: node
linkType: hard
-"react-transition-group@npm:^2.2.1":
+"react-transition-group@npm:^2.2.1, react-transition-group@npm:^2.5.3":
version: 2.9.0
resolution: "react-transition-group@npm:2.9.0"
dependencies:
languageName: node
linkType: hard
-"tslib@npm:^1.8.0, tslib@npm:^1.8.1, tslib@npm:^1.9.0":
+"tslib@npm:^1.8.0, tslib@npm:^1.8.1, tslib@npm:^1.9.0, tslib@npm:^1.9.3":
version: 1.14.1
resolution: "tslib@npm:1.14.1"
checksum: dbe628ef87f66691d5d2959b3e41b9ca0045c3ee3c7c7b906cc1e328b39f199bb1ad9e671c39025bd56122ac57dfbf7385a94843b1cc07c60a4db74795829acd