cy.get('[data-cy=invite-people-field] input').type("admin");
});
cy.get('[role=tooltip]').click();
- cy.get('.sharing-dialog').contains('Save').click();
+ cy.get('.sharing-dialog').get('[data-cy=add-invited-people]').click();
cy.get('.sharing-dialog').contains('Close').click();
// Check that both users are present with appropriate permissions
cy.get('[data-cy=invite-people-field]').find('input').type(activeUser.user.email);
cy.get('[role=tooltip]').click();
cy.get('@sharingDialog').within(() => {
- cy.contains('Save changes').click();
+ cy.get('[data-cy=add-invited-people]').click();
cy.contains('Close').click();
});
});
cy.get('[data-cy=invite-people-field]').find('input').type(activeUser.user.email);
cy.get('[role=tooltip]').click();
cy.get('@sharingDialog').within(() => {
- cy.contains('Save changes').click();
+ cy.get('[data-cy=add-invited-people]').click();
cy.contains('Close').click();
});
});
cy.loginAs(adminUser);
cy.get('[data-cy=project-panel]').contains(collName).rightclick();
cy.get('[data-cy=context-menu]').contains('Share').click();
- cy.get('button').contains('Save changes').parent().should('be.disabled');
+ cy.get('button').get('[data-cy=add-invited-people]').should('be.disabled');
cy.get('[data-cy=invite-people-field] input').type('Anonymous');
cy.get('div[role=tooltip]').contains('anonymous').click();
- cy.get('button').contains('Save changes').parent().should('not.be.disabled');
+ cy.get('button').get('[data-cy=add-invited-people]').should('not.be.disabled');
cy.get('[data-cy=invite-people-field] div[role=button]').contains('anonymous').parent().find('svg').click();
- cy.get('button').contains('Save changes').parent().should('be.disabled');
+ cy.get('button').get('[data-cy=add-invited-people]').should('be.disabled');
});
});
-});
\ No newline at end of file
+});
import Axios from 'axios';
export const WORKBENCH_CONFIG_URL =
- process.env.REACT_APP_ARVADOS_CONFIG_URL || '/config.json';
+ process.env.REACT_APP_ARVADOS_CONFIG_URL || '/config.json';
interface WorkbenchConfig {
- API_HOST: string;
- VOCABULARY_URL?: string;
- FILE_VIEWERS_CONFIG_URL?: string;
+ API_HOST: string;
+ VOCABULARY_URL?: string;
+ FILE_VIEWERS_CONFIG_URL?: string;
}
export interface ClusterConfigJSON {
Scheme: string
}
};
- Mail?: {
- SupportEmailAddress: string;
- };
- Services: {
- Controller: {
- ExternalURL: string;
+ Mail?: {
+ SupportEmailAddress: string;
};
- Workbench1: {
- ExternalURL: string;
- };
- Workbench2: {
- ExternalURL: string;
+ Services: {
+ Controller: {
+ ExternalURL: string;
+ };
+ Workbench1: {
+ ExternalURL: string;
+ };
+ Workbench2: {
+ ExternalURL: string;
+ };
+ Workbench: {
+ DisableSharingURLsUI: boolean;
+ ArvadosDocsite: string;
+ FileViewersConfigURL: string;
+ WelcomePageHTML: string;
+ InactivePageHTML: string;
+ SSHHelpPageHTML: string;
+ SSHHelpHostSuffix: string;
+ SiteName: string;
+ IdleTimeout: string;
+ };
+ Websocket: {
+ ExternalURL: string;
+ };
+ WebDAV: {
+ ExternalURL: string;
+ };
+ WebDAVDownload: {
+ ExternalURL: string;
+ };
+ WebShell: {
+ ExternalURL: string;
+ };
};
Workbench: {
DisableSharingURLsUI: boolean;
SSHHelpHostSuffix: string;
SiteName: string;
IdleTimeout: string;
+ BannerUUID: string;
};
- Websocket: {
- ExternalURL: string;
- };
- WebDAV: {
- ExternalURL: string;
- };
- WebDAVDownload: {
- ExternalURL: string;
- };
- WebShell: {
- ExternalURL: string;
- };
- };
- Workbench: {
- DisableSharingURLsUI: boolean;
- ArvadosDocsite: string;
- FileViewersConfigURL: string;
- WelcomePageHTML: string;
- InactivePageHTML: string;
- SSHHelpPageHTML: string;
- SSHHelpHostSuffix: string;
- SiteName: string;
- IdleTimeout: string;
- BannerUUID: string;
- };
- Login: {
- LoginCluster: string;
- Google: {
- Enable: boolean;
- };
- LDAP: {
- Enable: boolean;
- };
- OpenIDConnect: {
- Enable: boolean;
- };
- PAM: {
- Enable: boolean;
+ Login: {
+ LoginCluster: string;
+ Google: {
+ Enable: boolean;
+ };
+ LDAP: {
+ Enable: boolean;
+ };
+ OpenIDConnect: {
+ Enable: boolean;
+ };
+ PAM: {
+ Enable: boolean;
+ };
+ SSO: {
+ Enable: boolean;
+ };
+ Test: {
+ Enable: boolean;
+ };
};
- SSO: {
- Enable: boolean;
+ Collections: {
+ ForwardSlashNameSubstitution: string;
+ ManagedProperties?: {
+ [key: string]: {
+ Function: string;
+ Value: string;
+ Protected?: boolean;
+ };
+ };
+ TrustAllContent: boolean;
};
- Test: {
- Enable: boolean;
- };
- };
- Collections: {
- ForwardSlashNameSubstitution: string;
- ManagedProperties?: {
- [key: string]: {
- Function: string;
- Value: string;
- Protected?: boolean;
- };
+ Volumes: {
+ [key: string]: {
+ StorageClasses: {
+ [key: string]: boolean;
+ };
+ };
};
- TrustAllContent: boolean;
- };
- Volumes: {
- [key: string]: {
- StorageClasses: {
- [key: string]: boolean;
- };
+ Users: {
+ AnonymousUserToken: string;
};
- };
}
export class Config {
- baseUrl!: string;
- keepWebServiceUrl!: string;
- keepWebInlineServiceUrl!: string;
- remoteHosts!: {
- [key: string]: string;
- };
- rootUrl!: string;
- uuidPrefix!: string;
- websocketUrl!: string;
- workbenchUrl!: string;
- workbench2Url!: string;
- vocabularyUrl!: string;
- fileViewersConfigUrl!: string;
- loginCluster!: string;
- clusterConfig!: ClusterConfigJSON;
- apiRevision!: number;
+ baseUrl!: string;
+ keepWebServiceUrl!: string;
+ keepWebInlineServiceUrl!: string;
+ remoteHosts!: {
+ [key: string]: string;
+ };
+ rootUrl!: string;
+ uuidPrefix!: string;
+ websocketUrl!: string;
+ workbenchUrl!: string;
+ workbench2Url!: string;
+ vocabularyUrl!: string;
+ fileViewersConfigUrl!: string;
+ loginCluster!: string;
+ clusterConfig!: ClusterConfigJSON;
+ apiRevision!: number;
}
export const buildConfig = (clusterConfig: ClusterConfigJSON): Config => {
- const clusterConfigJSON = removeTrailingSlashes(clusterConfig);
- const config = new Config();
- config.rootUrl = clusterConfigJSON.Services.Controller.ExternalURL;
- config.baseUrl = `${config.rootUrl}/${ARVADOS_API_PATH}`;
- config.uuidPrefix = clusterConfigJSON.ClusterID;
- config.websocketUrl = clusterConfigJSON.Services.Websocket.ExternalURL;
- config.workbench2Url = clusterConfigJSON.Services.Workbench2.ExternalURL;
- config.workbenchUrl = clusterConfigJSON.Services.Workbench1.ExternalURL;
- config.keepWebServiceUrl =
- clusterConfigJSON.Services.WebDAVDownload.ExternalURL;
- config.keepWebInlineServiceUrl =
- clusterConfigJSON.Services.WebDAV.ExternalURL;
- config.loginCluster = clusterConfigJSON.Login.LoginCluster;
- config.clusterConfig = clusterConfigJSON;
- config.apiRevision = 0;
- mapRemoteHosts(clusterConfigJSON, config);
- return config;
+ const clusterConfigJSON = removeTrailingSlashes(clusterConfig);
+ const config = new Config();
+ config.rootUrl = clusterConfigJSON.Services.Controller.ExternalURL;
+ config.baseUrl = `${config.rootUrl}/${ARVADOS_API_PATH}`;
+ config.uuidPrefix = clusterConfigJSON.ClusterID;
+ config.websocketUrl = clusterConfigJSON.Services.Websocket.ExternalURL;
+ config.workbench2Url = clusterConfigJSON.Services.Workbench2.ExternalURL;
+ config.workbenchUrl = clusterConfigJSON.Services.Workbench1.ExternalURL;
+ config.keepWebServiceUrl =
+ clusterConfigJSON.Services.WebDAVDownload.ExternalURL;
+ config.keepWebInlineServiceUrl =
+ clusterConfigJSON.Services.WebDAV.ExternalURL;
+ config.loginCluster = clusterConfigJSON.Login.LoginCluster;
+ config.clusterConfig = clusterConfigJSON;
+ config.apiRevision = 0;
+ mapRemoteHosts(clusterConfigJSON, config);
+ return config;
};
export const getStorageClasses = (config: Config): string[] => {
- const classes: Set<string> = new Set(['default']);
- const volumes = config.clusterConfig.Volumes;
- Object.keys(volumes).forEach((v) => {
- Object.keys(volumes[v].StorageClasses || {}).forEach((sc) => {
- if (volumes[v].StorageClasses[sc]) {
- classes.add(sc);
- }
+ const classes: Set<string> = new Set(['default']);
+ const volumes = config.clusterConfig.Volumes;
+ Object.keys(volumes).forEach((v) => {
+ Object.keys(volumes[v].StorageClasses || {}).forEach((sc) => {
+ if (volumes[v].StorageClasses[sc]) {
+ classes.add(sc);
+ }
+ });
});
- });
- return Array.from(classes);
+ return Array.from(classes);
};
const getApiRevision = async (apiUrl: string) => {
- try {
- const dd = (await Axios.get<any>(`${apiUrl}/${DISCOVERY_DOC_PATH}`)).data;
- return parseInt(dd.revision, 10) || 0;
- } catch {
- console.warn(
- 'Unable to get API Revision number, defaulting to zero. Some features may not work properly.'
- );
- return 0;
- }
+ try {
+ const dd = (await Axios.get<any>(`${apiUrl}/${DISCOVERY_DOC_PATH}`)).data;
+ return parseInt(dd.revision, 10) || 0;
+ } catch {
+ console.warn(
+ 'Unable to get API Revision number, defaulting to zero. Some features may not work properly.'
+ );
+ return 0;
+ }
};
const removeTrailingSlashes = (
- config: ClusterConfigJSON
+ config: ClusterConfigJSON
): ClusterConfigJSON => {
- const svcs: any = {};
- Object.keys(config.Services).forEach((s) => {
- svcs[s] = config.Services[s];
- if (svcs[s].hasOwnProperty('ExternalURL')) {
- svcs[s].ExternalURL = svcs[s].ExternalURL.replace(/\/+$/, '');
- }
- });
- return { ...config, Services: svcs };
+ const svcs: any = {};
+ Object.keys(config.Services).forEach((s) => {
+ svcs[s] = config.Services[s];
+ if (svcs[s].hasOwnProperty('ExternalURL')) {
+ svcs[s].ExternalURL = svcs[s].ExternalURL.replace(/\/+$/, '');
+ }
+ });
+ return { ...config, Services: svcs };
};
export const fetchConfig = () => {
- return Axios.get<WorkbenchConfig>(
- WORKBENCH_CONFIG_URL + '?nocache=' + new Date().getTime()
- )
- .then((response) => response.data)
- .catch(() => {
- console.warn(
- `There was an exception getting the Workbench config file at ${WORKBENCH_CONFIG_URL}. Using defaults instead.`
- );
- return Promise.resolve(getDefaultConfig());
- })
- .then((workbenchConfig) => {
- if (workbenchConfig.API_HOST === undefined) {
- throw new Error(
- `Unable to start Workbench. API_HOST is undefined in ${WORKBENCH_CONFIG_URL} or the environment.`
- );
- }
- return Axios.get<ClusterConfigJSON>(
- getClusterConfigURL(workbenchConfig.API_HOST)
- ).then(async (response) => {
- const apiRevision = await getApiRevision(
- response.data.Services.Controller.ExternalURL.replace(/\/+$/, '')
- );
- const config = { ...buildConfig(response.data), apiRevision };
- const warnLocalConfig = (varName: string) =>
- console.warn(
- `A value for ${varName} was found in ${WORKBENCH_CONFIG_URL}. To use the Arvados centralized configuration instead, \
+ return Axios.get<WorkbenchConfig>(
+ WORKBENCH_CONFIG_URL + '?nocache=' + new Date().getTime()
+ )
+ .then((response) => response.data)
+ .catch(() => {
+ console.warn(
+ `There was an exception getting the Workbench config file at ${WORKBENCH_CONFIG_URL}. Using defaults instead.`
+ );
+ return Promise.resolve(getDefaultConfig());
+ })
+ .then((workbenchConfig) => {
+ if (workbenchConfig.API_HOST === undefined) {
+ throw new Error(
+ `Unable to start Workbench. API_HOST is undefined in ${WORKBENCH_CONFIG_URL} or the environment.`
+ );
+ }
+ return Axios.get<ClusterConfigJSON>(
+ getClusterConfigURL(workbenchConfig.API_HOST)
+ ).then(async (response) => {
+ const apiRevision = await getApiRevision(
+ response.data.Services.Controller.ExternalURL.replace(/\/+$/, '')
+ );
+ const config = { ...buildConfig(response.data), apiRevision };
+ const warnLocalConfig = (varName: string) =>
+ console.warn(
+ `A value for ${varName} was found in ${WORKBENCH_CONFIG_URL}. To use the Arvados centralized configuration instead, \
remove the entire ${varName} entry from ${WORKBENCH_CONFIG_URL}`
- );
+ );
- // Check if the workbench config has an entry for vocabulary and file viewer URLs
- // If so, use these values (even if it is an empty string), but print a console warning.
- // Otherwise, use the cluster config.
- let fileViewerConfigUrl;
- if (workbenchConfig.FILE_VIEWERS_CONFIG_URL !== undefined) {
- warnLocalConfig('FILE_VIEWERS_CONFIG_URL');
- fileViewerConfigUrl = workbenchConfig.FILE_VIEWERS_CONFIG_URL;
- } else {
- fileViewerConfigUrl =
- config.clusterConfig.Workbench.FileViewersConfigURL ||
- '/file-viewers-example.json';
- }
- config.fileViewersConfigUrl = fileViewerConfigUrl;
+ // Check if the workbench config has an entry for vocabulary and file viewer URLs
+ // If so, use these values (even if it is an empty string), but print a console warning.
+ // Otherwise, use the cluster config.
+ let fileViewerConfigUrl;
+ if (workbenchConfig.FILE_VIEWERS_CONFIG_URL !== undefined) {
+ warnLocalConfig('FILE_VIEWERS_CONFIG_URL');
+ fileViewerConfigUrl = workbenchConfig.FILE_VIEWERS_CONFIG_URL;
+ } else {
+ fileViewerConfigUrl =
+ config.clusterConfig.Workbench.FileViewersConfigURL ||
+ '/file-viewers-example.json';
+ }
+ config.fileViewersConfigUrl = fileViewerConfigUrl;
- if (workbenchConfig.VOCABULARY_URL !== undefined) {
- console.warn(
- `A value for VOCABULARY_URL was found in ${WORKBENCH_CONFIG_URL}. It will be ignored as the cluster already provides its own endpoint, you can safely remove it.`
- );
- }
- config.vocabularyUrl = getVocabularyURL(workbenchConfig.API_HOST);
+ if (workbenchConfig.VOCABULARY_URL !== undefined) {
+ console.warn(
+ `A value for VOCABULARY_URL was found in ${WORKBENCH_CONFIG_URL}. It will be ignored as the cluster already provides its own endpoint, you can safely remove it.`
+ );
+ }
+ config.vocabularyUrl = getVocabularyURL(workbenchConfig.API_HOST);
- return { config, apiHost: workbenchConfig.API_HOST };
- });
- });
+ return { config, apiHost: workbenchConfig.API_HOST };
+ });
+ });
};
// Maps remote cluster hosts and removes the default RemoteCluster entry
export const mapRemoteHosts = (
- clusterConfigJSON: ClusterConfigJSON,
- config: Config
+ clusterConfigJSON: ClusterConfigJSON,
+ config: Config
) => {
- config.remoteHosts = {};
- Object.keys(clusterConfigJSON.RemoteClusters).forEach((k) => {
- config.remoteHosts[k] = clusterConfigJSON.RemoteClusters[k].Host;
- });
- delete config.remoteHosts['*'];
+ config.remoteHosts = {};
+ Object.keys(clusterConfigJSON.RemoteClusters).forEach((k) => {
+ config.remoteHosts[k] = clusterConfigJSON.RemoteClusters[k].Host;
+ });
+ delete config.remoteHosts['*'];
};
export const mockClusterConfigJSON = (
- config: Partial<ClusterConfigJSON>
+ config: Partial<ClusterConfigJSON>
): ClusterConfigJSON => ({
- API: {
- UnfreezeProjectRequiresAdmin: false,
- MaxItemsPerResponse: 1000,
- },
- ClusterID: '',
- RemoteClusters: {},
- Services: {
- Controller: { ExternalURL: '' },
- Workbench1: { ExternalURL: '' },
- Workbench2: { ExternalURL: '' },
- Websocket: { ExternalURL: '' },
- WebDAV: { ExternalURL: '' },
- WebDAVDownload: { ExternalURL: '' },
- WebShell: { ExternalURL: '' },
- Workbench: {
- DisableSharingURLsUI: false,
- ArvadosDocsite: "",
- FileViewersConfigURL: "",
- WelcomePageHTML: "",
- InactivePageHTML: "",
- SSHHelpPageHTML: "",
- SSHHelpHostSuffix: "",
- SiteName: "",
- IdleTimeout: "0s"
- },
- },
- Workbench: {
- DisableSharingURLsUI: false,
- ArvadosDocsite: '',
- FileViewersConfigURL: '',
- WelcomePageHTML: '',
- InactivePageHTML: '',
- SSHHelpPageHTML: '',
- SSHHelpHostSuffix: '',
- SiteName: '',
- IdleTimeout: '0s',
- BannerUUID: ""
- },
- Login: {
- LoginCluster: '',
- Google: {
- Enable: false,
+ API: {
+ UnfreezeProjectRequiresAdmin: false,
+ MaxItemsPerResponse: 1000,
},
- LDAP: {
- Enable: false,
+ ClusterID: '',
+ RemoteClusters: {},
+ Services: {
+ Controller: { ExternalURL: '' },
+ Workbench1: { ExternalURL: '' },
+ Workbench2: { ExternalURL: '' },
+ Websocket: { ExternalURL: '' },
+ WebDAV: { ExternalURL: '' },
+ WebDAVDownload: { ExternalURL: '' },
+ WebShell: { ExternalURL: '' },
+ Workbench: {
+ DisableSharingURLsUI: false,
+ ArvadosDocsite: "",
+ FileViewersConfigURL: "",
+ WelcomePageHTML: "",
+ InactivePageHTML: "",
+ SSHHelpPageHTML: "",
+ SSHHelpHostSuffix: "",
+ SiteName: "",
+ IdleTimeout: "0s"
+ },
},
- OpenIDConnect: {
- Enable: false,
+ Workbench: {
+ DisableSharingURLsUI: false,
+ ArvadosDocsite: '',
+ FileViewersConfigURL: '',
+ WelcomePageHTML: '',
+ InactivePageHTML: '',
+ SSHHelpPageHTML: '',
+ SSHHelpHostSuffix: '',
+ SiteName: '',
+ IdleTimeout: '0s',
+ BannerUUID: ""
},
- PAM: {
- Enable: false,
+ Login: {
+ LoginCluster: '',
+ Google: {
+ Enable: false,
+ },
+ LDAP: {
+ Enable: false,
+ },
+ OpenIDConnect: {
+ Enable: false,
+ },
+ PAM: {
+ Enable: false,
+ },
+ SSO: {
+ Enable: false,
+ },
+ Test: {
+ Enable: false,
+ },
},
- SSO: {
- Enable: false,
+ Collections: {
+ ForwardSlashNameSubstitution: '',
+ TrustAllContent: false,
},
- Test: {
- Enable: false,
+ Volumes: {},
+ Users: {
+ AnonymousUserToken: ""
},
- },
- Collections: {
- ForwardSlashNameSubstitution: '',
- TrustAllContent: false,
- },
- Volumes: {},
- ...config,
+ ...config,
});
export const mockConfig = (config: Partial<Config>): Config => ({
- baseUrl: '',
- keepWebServiceUrl: '',
- keepWebInlineServiceUrl: '',
- remoteHosts: {},
- rootUrl: '',
- uuidPrefix: '',
- websocketUrl: '',
- workbenchUrl: '',
- workbench2Url: '',
- vocabularyUrl: '',
- fileViewersConfigUrl: '',
- loginCluster: '',
- clusterConfig: mockClusterConfigJSON({}),
- apiRevision: 0,
- ...config,
+ baseUrl: '',
+ keepWebServiceUrl: '',
+ keepWebInlineServiceUrl: '',
+ remoteHosts: {},
+ rootUrl: '',
+ uuidPrefix: '',
+ websocketUrl: '',
+ workbenchUrl: '',
+ workbench2Url: '',
+ vocabularyUrl: '',
+ fileViewersConfigUrl: '',
+ loginCluster: '',
+ clusterConfig: mockClusterConfigJSON({}),
+ apiRevision: 0,
+ ...config,
});
const getDefaultConfig = (): WorkbenchConfig => {
- let apiHost = '';
- const envHost = process.env.REACT_APP_ARVADOS_API_HOST;
- if (envHost !== undefined) {
- console.warn(`Using default API host ${envHost}.`);
- apiHost = envHost;
- } else {
- console.warn(
- `No API host was found in the environment. Workbench may not be able to communicate with Arvados components.`
- );
- }
- return {
- API_HOST: apiHost,
- VOCABULARY_URL: undefined,
- FILE_VIEWERS_CONFIG_URL: undefined,
- };
+ let apiHost = '';
+ const envHost = process.env.REACT_APP_ARVADOS_API_HOST;
+ if (envHost !== undefined) {
+ console.warn(`Using default API host ${envHost}.`);
+ apiHost = envHost;
+ } else {
+ console.warn(
+ `No API host was found in the environment. Workbench may not be able to communicate with Arvados components.`
+ );
+ }
+ return {
+ API_HOST: apiHost,
+ VOCABULARY_URL: undefined,
+ FILE_VIEWERS_CONFIG_URL: undefined,
+ };
};
export const ARVADOS_API_PATH = 'arvados/v1';
export const VOCABULARY_PATH = 'arvados/v1/vocabulary';
export const DISCOVERY_DOC_PATH = 'discovery/v1/apis/arvados/v1/rest';
export const getClusterConfigURL = (apiHost: string) =>
- `https://${apiHost}/${CLUSTER_CONFIG_PATH}?nocache=${new Date().getTime()}`;
+ `https://${apiHost}/${CLUSTER_CONFIG_PATH}?nocache=${new Date().getTime()}`;
export const getVocabularyURL = (apiHost: string) =>
- `https://${apiHost}/${VOCABULARY_PATH}?nocache=${new Date().getTime()}`;
+ `https://${apiHost}/${VOCABULARY_PATH}?nocache=${new Date().getTime()}`;
);
}
- delete(uuid: string): Promise<T> {
+ delete(uuid: string, showErrors?: boolean): Promise<T> {
this.validateUuid(uuid);
return CommonService.defaultResponse(
this.serverApi
.delete(`/${this.resourceType}/${uuid}`),
- this.actions
+ this.actions,
+ true, // mapKeys
+ showErrors
);
}
ResourceObjectType
} from "models/resource";
import { resourcesActions } from "store/resources/resources-actions";
-import { getPublicGroupUuid } from "store/workflow-panel/workflow-panel-actions";
+import { getPublicGroupUuid, getAllUsersGroupUuid } from "store/workflow-panel/workflow-panel-actions";
import { getSharingPublicAccessFormData } from './sharing-dialog-types';
export const openSharingDialog = (resourceUuid: string, refresh?: () => void) =>
(dispatch: Dispatch) => {
- dispatch(dialogActions.OPEN_DIALOG({ id: SHARING_DIALOG_NAME, data: {resourceUuid, refresh} }));
+ dispatch(dialogActions.OPEN_DIALOG({ id: SHARING_DIALOG_NAME, data: { resourceUuid, refresh } }));
dispatch<any>(loadSharingDialog);
};
dispatch(snackbarActions.OPEN_SNACKBAR({
message: 'You do not have access to share this item',
hideDuration: 2000,
- kind: SnackbarKind.ERROR }));
+ kind: SnackbarKind.ERROR
+ }));
dispatch(dialogActions.CLOSE_DIALOG({ id: SHARING_DIALOG_NAME }));
} finally {
dispatch(progressIndicatorActions.STOP_WORKING(SHARING_DIALOG_NAME));
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', Array.from(new Set(permissionLinks.map(({ tailUuid }) => tailUuid))))
- .getFilters();
-
- const { items: users } = await userService.list({ filters, count: "none", limit: 1000 });
- const { items: groups } = await groupsService.list({ filters, count: "none", limit: 1000 });
+ 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', Array.from(new Set(permissionLinks.map(({ tailUuid }) => tailUuid))))
+ .getFilters();
- const getEmail = (tailUuid: string) => {
- const user = users.find(({ uuid }) => uuid === tailUuid);
- const group = groups.find(({ uuid }) => uuid === tailUuid);
- return user
- ? user.email
- : group
- ? group.name
- : tailUuid;
- };
+ const { items: users } = await userService.list({ filters, count: "none", limit: 1000 });
+ const { items: groups } = await groupsService.list({ filters, count: "none", limit: 1000 });
- const managementPermissions = permissionLinks
- .filter(item =>
- item.tailUuid !== getPublicGroupUuid(getState()))
- .map(({ tailUuid, name, uuid }) => ({
- email: getEmail(tailUuid),
- permissions: name as PermissionLevel,
- permissionUuid: uuid,
- }));
+ const getEmail = (tailUuid: string) => {
+ const user = users.find(({ uuid }) => uuid === tailUuid);
+ const group = groups.find(({ uuid }) => uuid === tailUuid);
+ return user
+ ? user.email
+ : group
+ ? group.name
+ : tailUuid;
+ };
- const managementFormData: SharingManagementFormData = {
- permissions: managementPermissions,
- initialPermissions: managementPermissions,
- };
+ const managementPermissions = permissionLinks
+ .map(({ tailUuid, name, uuid }) => ({
+ email: getEmail(tailUuid),
+ permissions: name as PermissionLevel,
+ permissionUuid: uuid,
+ }));
- dispatch(initialize(SHARING_MANAGEMENT_FORM_NAME, managementFormData));
- dispatch(progressIndicatorActions.STOP_WORKING(SHARING_DIALOG_NAME));
+ const managementFormData: SharingManagementFormData = {
+ permissions: managementPermissions,
+ initialPermissions: managementPermissions,
};
+ dispatch(initialize(SHARING_MANAGEMENT_FORM_NAME, managementFormData));
+ dispatch(progressIndicatorActions.STOP_WORKING(SHARING_DIALOG_NAME));
+};
+
const initializePublicAccessForm = (permissionLinks: PermissionResource[]) =>
- (dispatch: Dispatch, getState: () => RootState, ) => {
+ (dispatch: Dispatch, getState: () => RootState,) => {
+
+ const state = getState();
+
const [publicPermission] = permissionLinks
- .filter(item => item.tailUuid === getPublicGroupUuid(getState()));
- const publicAccessFormData: SharingPublicAccessFormData = publicPermission
- ? {
+ .filter(item => item.tailUuid === getPublicGroupUuid(state));
+
+ const [allUsersPermission] = permissionLinks
+ .filter(item => item.tailUuid === getAllUsersGroupUuid(state));
+
+ let publicAccessFormData: SharingPublicAccessFormData;
+
+ if (publicPermission) {
+ publicAccessFormData = {
visibility: VisibilityLevel.PUBLIC,
- permissionUuid: publicPermission.uuid,
- }
- : {
- visibility: permissionLinks.length > 0
- ? VisibilityLevel.SHARED
- : VisibilityLevel.PRIVATE,
- permissionUuid: '',
+ initialVisibility: VisibilityLevel.PUBLIC,
+ permissionUuid: publicPermission.uuid
+ };
+ } else if (allUsersPermission) {
+ publicAccessFormData = {
+ visibility: VisibilityLevel.ALL_USERS,
+ initialVisibility: VisibilityLevel.ALL_USERS,
+ permissionUuid: allUsersPermission.uuid
+ };
+ } else if (permissionLinks.length > 0) {
+ publicAccessFormData = {
+ visibility: VisibilityLevel.SHARED,
+ initialVisibility: VisibilityLevel.SHARED,
+ permissionUuid: ''
};
+ } else {
+ publicAccessFormData = {
+ visibility: VisibilityLevel.PRIVATE,
+ initialVisibility: VisibilityLevel.PRIVATE,
+ permissionUuid: ''
+ };
+ }
+
dispatch(initialize(SHARING_PUBLIC_ACCESS_FORM_NAME, publicAccessFormData));
};
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);
- }
+ const { permissionUuid, visibility, initialVisibility } = getSharingPublicAccessFormData(state);
+ // If visibility level changed, delete the previous link to public/all users.
+ // On PRIVATE this link will be deleted by saveManagementChanges
+ // so don't double delete (which would show an error dialog).
+ if (permissionUuid !== "" && visibility !== initialVisibility) {
+ await permissionService.delete(permissionUuid);
+ }
+ if (visibility === VisibilityLevel.ALL_USERS) {
+ await permissionService.create({
+ ownerUuid: user.uuid,
+ headUuid: dialog.data.resourceUuid,
+ tailUuid: getAllUsersGroupUuid(state),
+ name: PermissionLevel.CAN_READ,
+ });
} else if (visibility === VisibilityLevel.PUBLIC) {
await permissionService.create({
ownerUuid: user.uuid,
(a, b) => a.permissionUuid === b.permissionUuid
);
- const deletions = cancelledPermissions.map(({ permissionUuid }) =>
- permissionService.delete(permissionUuid));
- const updates = permissions.map(update =>
- permissionService.update(update.permissionUuid, { name: update.permissions }));
+ const deletions = cancelledPermissions.map(async ({ permissionUuid }) => {
+ try {
+ await permissionService.delete(permissionUuid, false);
+ } catch (e) { }
+ });
+ const updates = permissions.map(async update => {
+ try {
+ await permissionService.update(update.permissionUuid, { name: update.permissions }, false);
+ } catch (e) { }
+ });
await Promise.all([...deletions, ...updates]);
}
};
tailUuid: invitee.uuid,
name: invitations.permissions
}));
- const changes = data.map( invitation => permissionService.create(invitation));
+ const changes = data.map(invitation => permissionService.create(invitation));
await Promise.all(changes);
}
};
export enum VisibilityLevel {
PRIVATE = 'Private',
SHARED = 'Shared',
+ ALL_USERS = 'All user accounts',
PUBLIC = 'Public',
}
export interface SharingPublicAccessFormData {
visibility: VisibilityLevel;
+ initialVisibility: VisibilityLevel;
permissionUuid: string;
}
const prefix = state.auth.localCluster;
return `${prefix}-j7d0g-anonymouspublic`;
};
+export const getAllUsersGroupUuid = (state: RootState) => {
+ const prefix = state.auth.localCluster;
+ return `${prefix}-j7d0g-fffffffffffffff`;
+};
export const showWorkflowDetails = (uuid: string) =>
propertiesActions.SET_PROPERTY({ key: WORKFLOW_PANEL_DETAILS_UUID, value: uuid });
config: {
keepWebServiceUrl: 'http://example.com/',
keepWebInlineServiceUrl: 'http://*.collections.example.com/',
+ clusterConfig: {
+ Users: {
+ AnonymousUserToken: ""
+ }
+ }
}
}
store = createStore(combineReducers({
let wrapper = mount(<Provider store={store}><SharingDialogComponent {...props} /></Provider>);
expect(wrapper.html()).not.toContain('Sharing URLs');
});
-});
\ No newline at end of file
+});
Checkbox,
FormControlLabel,
Typography,
+ Tooltip,
} from '@material-ui/core';
import {
StyleRulesCallback,
import DateFnsUtils from "@date-io/date-fns";
import moment from 'moment';
import { SharingPublicAccessForm } from './sharing-public-access-form';
+import { AddIcon } from 'components/icon/icon';
export interface SharingDialogDataProps {
open: boolean;
sharingURLsNr: number;
privateAccess: boolean;
sharingURLsDisabled: boolean;
+ permissions: any[];
}
export interface SharingDialogActionProps {
onClose: () => void;
export default (props: SharingDialogComponentProps) => {
const { open, loading, saveEnabled, sharedResourceUuid,
- sharingURLsNr, privateAccess, sharingURLsDisabled,
+ sharingURLsNr, privateAccess, sharingURLsDisabled, permissions,
onClose, onSave, onCreateSharingToken, refreshPermissions } = props;
const showTabs = !sharingURLsDisabled && extractUuidObjectType(sharedResourceUuid) === ResourceObjectType.COLLECTION;
const [tabNr, setTabNr] = React.useState<number>(SharingDialogTab.PERMISSIONS);
{...{ open, onClose }}
className="sharing-dialog"
fullWidth
- maxWidth='sm'
+ maxWidth='md'
disableBackdropClick={saveEnabled}
disableEscapeKeyDown={saveEnabled}>
<DialogTitle>
Sharing settings
</DialogTitle>
- { showTabs &&
- <Tabs value={tabNr}
- onChange={(_, tb) => {
- if (tb === SharingDialogTab.PERMISSIONS) {
- refreshPermissions();
+ {showTabs &&
+ <Tabs value={tabNr}
+ onChange={(_, tb) => {
+ if (tb === SharingDialogTab.PERMISSIONS) {
+ refreshPermissions();
+ }
+ setTabNr(tb)
}
- setTabNr(tb)}
- }>
- <Tab label="With users/groups" />
- <Tab label={`Sharing URLs ${sharingURLsNr > 0 ? '('+sharingURLsNr+')' : ''}`} disabled={saveEnabled} />
- </Tabs>
+ }>
+ <Tab label="With users/groups" />
+ <Tab label={`Sharing URLs ${sharingURLsNr > 0 ? '(' + sharingURLsNr + ')' : ''}`} disabled={saveEnabled} />
+ </Tabs>
}
<DialogContent>
- { tabNr === SharingDialogTab.PERMISSIONS &&
- <Grid container direction='column' spacing={24}>
- <Grid item>
- <SharingPublicAccessForm />
- </Grid>
- <Grid item>
- <SharingManagementForm />
+ {tabNr === SharingDialogTab.PERMISSIONS &&
+ <Grid container direction='column' spacing={24}>
+ <Grid item>
+ <SharingPublicAccessForm onSave={onSave} />
+ </Grid>
+ <Grid item>
+ <SharingManagementForm onSave={onSave} />
+ </Grid>
</Grid>
- </Grid>
}
- { tabNr === SharingDialogTab.URLS &&
- <SharingURLsContent uuid={sharedResourceUuid} />
+ {tabNr === SharingDialogTab.URLS &&
+ <SharingURLsContent uuid={sharedResourceUuid} />
}
</DialogContent>
<DialogActions>
<Grid container spacing={8}>
- { tabNr === SharingDialogTab.PERMISSIONS &&
- <Grid item md={12}>
- <SharingInvitationForm />
- </Grid>
+ {tabNr === SharingDialogTab.PERMISSIONS &&
+ <Grid item md={12}>
+ <SharingInvitationForm onSave={onSave} saveEnabled={saveEnabled} />
+ </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.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 && !sharingURLsDisabled &&
+ {tabNr === SharingDialogTab.PERMISSIONS && !sharingURLsDisabled &&
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 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 variant="contained" color="primary"
- disabled={expDate !== undefined && expDate <= new Date()}
- onClick={onCreateSharingToken(expDate)}>
- Create sharing URL
- </Button>
- </Grid>
+ {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 variant="contained" color="primary"
+ disabled={expDate !== undefined && expDate <= new Date()}
+ onClick={onCreateSharingToken(expDate)}>
+ Create sharing URL
+ </Button>
+ </Grid>
</>
}
- { tabNr === SharingDialogTab.PERMISSIONS &&
- <Grid item>
- <Button onClick={onSave} variant="contained" color="primary"
- disabled={!saveEnabled}>
- Save changes
- </Button>
- </Grid>
- }
<Grid item>
<Button onClick={() => {
onClose();
import { compose, Dispatch } from 'redux';
import { connect } from 'react-redux';
import { RootState } from 'store/store';
+import { formValueSelector } from 'redux-form'
import {
connectSharingDialog,
saveSharingDialogChanges,
getSharingPublicAccessFormData,
hasChanges,
SHARING_DIALOG_NAME,
+ SHARING_MANAGEMENT_FORM_NAME,
VisibilityLevel
} from 'store/sharing-dialog/sharing-dialog-types';
import { WithProgressStateProps } from 'store/progress-indicator/with-progress';
type Props = WithDialogProps<string> & WithProgressStateProps;
+const sharingManagementFormSelector = formValueSelector(SHARING_MANAGEMENT_FORM_NAME);
+
const mapStateToProps = (state: RootState, { working, ...props }: Props): SharingDialogDataProps => {
const dialog = getDialog<SharingDialogData>(state.dialog, SHARING_DIALOG_NAME);
const sharedResourceUuid = dialog?.data.resourceUuid || '';
const sharingURLsDisabled = state.auth.config.clusterConfig.Workbench.DisableSharingURLsUI;
return ({
- ...props,
- saveEnabled: hasChanges(state),
- loading: working,
- sharedResourceUuid,
- sharingURLsDisabled,
- sharingURLsNr: !sharingURLsDisabled
- ? (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
- : 0,
- privateAccess: getSharingPublicAccessFormData(state)?.visibility === VisibilityLevel.PRIVATE,
+ ...props,
+ permissions: sharingManagementFormSelector(state, 'permissions'),
+ saveEnabled: hasChanges(state),
+ loading: working,
+ sharedResourceUuid,
+ sharingURLsDisabled,
+ sharingURLsNr: !sharingURLsDisabled
+ ? (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
+ : 0,
+ privateAccess: getSharingPublicAccessFormData(state)?.visibility === VisibilityLevel.PRIVATE,
})
};
...props,
onClose: props.closeDialog,
onSave: () => {
- dispatch<any>(saveSharingDialogChanges);
+ setTimeout(() => dispatch<any>(saveSharingDialogChanges), 0);
},
onCreateSharingToken: (d: Date) => () => {
dispatch<any>(createSharingToken(d));
connectSharingDialogProgress,
connect(mapStateToProps, mapDispatchToProps)
)(SharingDialogComponent);
-
import React from 'react';
import { Field, WrappedFieldProps, FieldArray, WrappedFieldArrayProps } from 'redux-form';
-import { Grid, FormControl, InputLabel } from '@material-ui/core';
+import { Grid, FormControl, InputLabel, Tooltip, IconButton, StyleRulesCallback } from '@material-ui/core';
import { PermissionSelect, parsePermissionLevel, formatPermissionLevel } from './permission-select';
import { ParticipantSelect, Participant } from './participant-select';
+import { AddIcon } from 'components/icon/icon';
+import { WithStyles } from '@material-ui/core/styles';
+import withStyles from '@material-ui/core/styles/withStyles';
+import { ArvadosTheme } from 'common/custom-theme';
-export default () =>
- <Grid container spacing={8}>
- <Grid data-cy="invite-people-field" item xs={8}>
- <InvitedPeopleField />
- </Grid>
- <Grid data-cy="permission-select-field" item xs={4}>
- <PermissionSelectField />
- </Grid>
- </Grid>;
+type SharingStyles = 'root' | 'addButtonRoot' | 'addButtonPrimary' | 'addButtonDisabled';
+
+const styles: StyleRulesCallback<SharingStyles> = (theme: ArvadosTheme) => ({
+ root: {
+ padding: `${theme.spacing.unit}px 0`,
+ },
+ addButtonRoot: {
+ height: "36px",
+ width: "36px",
+ marginRight: "6px",
+ marginLeft: "6px",
+ marginTop: "12px",
+ },
+ addButtonPrimary: {
+ color: theme.palette.primary.contrastText,
+ background: theme.palette.primary.main,
+ "&:hover": {
+ background: theme.palette.primary.dark,
+ }
+ },
+ addButtonDisabled: {
+ background: 'none',
+ }
+});
+
+const SharingInvitationFormComponent = (props: { onSave: () => void, saveEnabled: boolean }) => <StyledSharingInvitationFormComponent onSave={props.onSave} saveEnabled={props.saveEnabled} />
+
+export default SharingInvitationFormComponent;
+
+const StyledSharingInvitationFormComponent = withStyles(styles)(
+ ({ onSave, saveEnabled, classes }: { onSave: () => void, saveEnabled: boolean } & WithStyles<SharingStyles>) =>
+ <Grid container spacing={8} wrap='nowrap' className={classes.root} >
+ <Grid data-cy="invite-people-field" item xs={8}>
+ <InvitedPeopleField />
+ </Grid>
+ <Grid data-cy="permission-select-field" item xs={4} container wrap='nowrap'>
+ <PermissionSelectField />
+ <IconButton onClick={onSave} disabled={!saveEnabled} color="primary" classes={{
+ root: classes.addButtonRoot,
+ colorPrimary: classes.addButtonPrimary,
+ disabled: classes.addButtonDisabled
+ }}
+ data-cy='add-invited-people'>
+ <Tooltip title="Add authorization">
+ <AddIcon />
+ </Tooltip>
+ </IconButton>
+ </Grid>
+ </Grid >);
const InvitedPeopleField = () =>
<FieldArray
import SharingInvitationFormComponent from './sharing-invitation-form-component';
import { SHARING_INVITATION_FORM_NAME } from 'store/sharing-dialog/sharing-dialog-types';
import { PermissionLevel } from 'models/permission';
+import { WithStyles } from '@material-ui/core/styles';
-export const SharingInvitationForm = compose(
- connect(() => ({
+interface InvitationFormData {
+ permissions: PermissionLevel;
+ invitedPeople: string[];
+}
+
+interface SaveProps {
+ onSave: () => void;
+ saveEnabled: boolean;
+}
+
+export const SharingInvitationForm =
+ reduxForm<InvitationFormData, SaveProps>({
+ form: SHARING_INVITATION_FORM_NAME,
initialValues: {
permissions: PermissionLevel.CAN_READ,
invitedPeople: [],
}
- })),
- reduxForm({ form: SHARING_INVITATION_FORM_NAME })
-)(SharingInvitationFormComponent);
\ No newline at end of file
+ })(SharingInvitationFormComponent);
import withStyles from '@material-ui/core/styles/withStyles';
import { CloseIcon } from 'components/icon/icon';
+const SharingManagementFormComponent = (props: { onSave: () => void; }) =>
+ <FieldArray<{ onSave: () => void }> name='permissions' component={SharingManagementFieldArray as any} props={props} />;
-export default () =>
- <FieldArray name='permissions' component={SharingManagementFieldArray as any} />;
+export default SharingManagementFormComponent;
-const SharingManagementFieldArray = ({ fields }: WrappedFieldArrayProps<{ email: string }>) =>
- <div>{ fields.map((field, index, fields) =>
- <PermissionManagementRow key={field} {...{ field, index, fields }} />) }
+const SharingManagementFieldArray = ({ fields, onSave }: { onSave: () => void } & WrappedFieldArrayProps<{ email: string }>) =>
+ <div>{fields.map((field, index, fields) =>
+ <PermissionManagementRow key={field} {...{ field, index, fields }} onSave={onSave} />)}
<Divider />
</div>;
padding: `${theme.spacing.unit}px 0`,
}
});
+
const PermissionManagementRow = withStyles(permissionManagementRowStyles)(
- ({ field, index, fields, classes }: { field: string, index: number, fields: FieldArrayFieldsProps<{ email: string }> } & WithStyles<'root'>) =>
+ ({ field, index, fields, classes, onSave }: { field: string, index: number, fields: FieldArrayFieldsProps<{ email: string }>, onSave: () => void; } & WithStyles<'root'>) =>
<>
<Divider />
<Grid container alignItems='center' spacing={8} wrap='nowrap' className={classes.root}>
name={`${field}.permissions` as string}
component={PermissionSelectComponent}
format={formatPermissionLevel}
- parse={parsePermissionLevel} />
- <IconButton onClick={() => fields.remove(index)}>
+ parse={parsePermissionLevel}
+ onChange={onSave}
+ />
+ <IconButton onClick={() => { fields.remove(index); onSave(); }}>
<CloseIcon />
</IconButton>
</Grid>
import SharingManagementFormComponent from './sharing-management-form-component';
import { SHARING_MANAGEMENT_FORM_NAME } from 'store/sharing-dialog/sharing-dialog-types';
-export const SharingManagementForm = reduxForm(
+interface SaveProps {
+ onSave: () => void;
+}
+
+export const SharingManagementForm = reduxForm<{}, SaveProps>(
{ form: SHARING_MANAGEMENT_FORM_NAME }
)(SharingManagementFormComponent);
}
});
+interface AccessProps {
+ visibility: VisibilityLevel;
+ includePublic: boolean;
+ onSave: () => void;
+}
+
const SharingPublicAccessForm = withStyles(sharingPublicAccessStyles)(
- ({ classes, visibility }: WithStyles<'root'> & { visibility: VisibilityLevel }) =>
+ ({ classes, visibility, includePublic, onSave }: WithStyles<'root'> & AccessProps) =>
<>
<Divider />
<Grid container alignItems='center' spacing={8} className={classes.root}>
</Typography>
</Grid>
<Grid item xs={4} container wrap='nowrap'>
- <Field name='visibility' component={VisibilityLevelSelectComponent} />
+ <Field<{ includePublic: boolean }> name='visibility' component={VisibilityLevelSelectComponent} includePublic={includePublic} onChange={onSave} />
</Grid>
</Grid>
</>
const renderVisibilityInfo = (visibility: VisibilityLevel) => {
switch (visibility) {
case VisibilityLevel.PUBLIC:
- return 'Anyone can access';
+ return 'Anyone on the Internet can access';
+ case VisibilityLevel.ALL_USERS:
+ return 'All users on this cluster can access';
case VisibilityLevel.SHARED:
return 'Specific people can access';
case VisibilityLevel.PRIVATE:
}
};
-export default ({ visibility }: { visibility: VisibilityLevel }) =>
- <SharingPublicAccessForm {...{ visibility }} />;
+const SharingPublicAccessFormComponent = ({ visibility, includePublic, onSave }: AccessProps) =>
+ <SharingPublicAccessForm {...{ visibility, includePublic, onSave }} />;
-const VisibilityLevelSelectComponent = ({ input }: WrappedFieldProps) =>
- <VisibilityLevelSelect fullWidth disableUnderline {...input} />;
+export default SharingPublicAccessFormComponent;
+const VisibilityLevelSelectComponent = ({ input, includePublic }: { includePublic: boolean } & WrappedFieldProps) =>
+ <VisibilityLevelSelect fullWidth disableUnderline includePublic={includePublic} {...input} />;
import { RootState } from 'store/store';
import { getSharingPublicAccessFormData } from '../../store/sharing-dialog/sharing-dialog-types';
+interface SaveProps {
+ onSave: () => void;
+}
+
export const SharingPublicAccessForm = compose(
- reduxForm(
+ reduxForm<{}, SaveProps>(
{ form: SHARING_PUBLIC_ACCESS_FORM_NAME }
),
connect(
(state: RootState) => {
const { visibility } = getSharingPublicAccessFormData(state) || { visibility: VisibilityLevel.PRIVATE };
- return { visibility };
+ const includePublic = state.auth.config.clusterConfig.Users.AnonymousUserToken.length > 0;
+ return { visibility, includePublic };
}
)
)(SharingPublicAccessFormComponent);
-
withStyles
} from '@material-ui/core';
import { ApiClientAuthorization } from 'models/api-client-authorization';
-import { CopyIcon, RemoveIcon } from 'components/icon/icon';
+import { CopyIcon, CloseIcon } from 'components/icon/icon';
import CopyToClipboard from 'react-copy-to-clipboard';
import { ArvadosTheme } from 'common/custom-theme';
import moment from 'moment';
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`;
+ {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> }
+ 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)}>
+ <CloseIcon />
+ </IconButton>
+ </Tooltip></span>
+ </Grid>
+ </Grid>
+ })
+ : <Grid item><Typography>No sharing URLs</Typography></Grid>}
</Grid>);
import { VisibilityLevel } from 'store/sharing-dialog/sharing-dialog-types';
-type VisibilityLevelSelectClasses = 'value';
+type VisibilityLevelSelectClasses = 'root';
const VisibilityLevelSelectStyles: StyleRulesCallback<VisibilityLevelSelectClasses> = theme => ({
- value: {
+ root: {
marginLeft: theme.spacing.unit,
}
});
export const VisibilityLevelSelect = withStyles(VisibilityLevelSelectStyles)(
- ({ classes, ...props }: SelectProps & WithStyles<VisibilityLevelSelectClasses>) =>
+ ({ classes, includePublic, ...props }: { includePublic: boolean } & SelectProps & WithStyles<VisibilityLevelSelectClasses>) =>
<Select
{...props}
renderValue={renderPermissionItem}
inputProps={{ classes }}>
- <MenuItem value={VisibilityLevel.PUBLIC}>
+ {includePublic && <MenuItem value={VisibilityLevel.PUBLIC}>
{renderPermissionItem(VisibilityLevel.PUBLIC)}
+ </MenuItem>}
+ <MenuItem value={VisibilityLevel.ALL_USERS}>
+ {renderPermissionItem(VisibilityLevel.ALL_USERS)}
</MenuItem>
<MenuItem value={VisibilityLevel.SHARED}>
{renderPermissionItem(VisibilityLevel.SHARED)}
switch (value) {
case VisibilityLevel.PUBLIC:
return Public;
+ case VisibilityLevel.ALL_USERS:
+ return Public;
case VisibilityLevel.SHARED:
return People;
case VisibilityLevel.PRIVATE:
return Lock;
}
};
-