diff --git a/CHANGELOG.md b/CHANGELOG.md index a3fe09565..45d0180f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [3.10.0] + +### Added + +- deps/react-18: Update React to v18 (#979) +- WP-50: Fix sizing of buttons "as-link" (#986) +- WP-509: Handle file/folder download feature with large number of files (#981) +- WP-520: AppTray should use versionEnabled for list of apps instead of enabled (#991) +- WP-24: Disabling Google Drive Integration (#988) +- WP-730: Refactor useRename to use react-query (#993) +- WP-728: Mutation hook: Copy file (#1000) +- WP-78: V3 Shared Workspaces Tests (#987) + +### Fixed + +- WP-419 Public Data Header Left Margin (#1003) +- WP-765: Fix job status button to show background (#1015) + + ## [3.9.0] ### Fixed @@ -1115,7 +1134,8 @@ WP-306: Fix target path regression (#871) ## [1.0.0] - 2020-02-28 v1.0.0 Production release as of Feb 28, 2020. -[unreleased]: https://github.com/TACC/Core-Portal/compare/v3.9.0...HEAD +[unreleased]: https://github.com/TACC/Core-Portal/compare/v3.10.0...HEAD +[3.10.0]: https://github.com/TACC/Core-Portal/releases/tag/v3.10.0 [3.9.0]: https://github.com/TACC/Core-Portal/releases/tag/v3.9.0 [3.8.2]: https://github.com/TACC/Core-Portal/releases/tag/v3.8.2 [3.8.1]: https://github.com/TACC/Core-Portal/releases/tag/v3.8.1 diff --git a/client/src/components/Jobs/JobsStatus/JobsStatus.jsx b/client/src/components/Jobs/JobsStatus/JobsStatus.jsx index b3886feaa..c26fbcb3c 100644 --- a/client/src/components/Jobs/JobsStatus/JobsStatus.jsx +++ b/client/src/components/Jobs/JobsStatus/JobsStatus.jsx @@ -92,7 +92,7 @@ function JobsStatus({ status, fancy, jobUuid }) { return (
{fancy && color ? ( - + {userStatus} ) : ( diff --git a/client/src/components/ManageAccount/tests/ManageAccountTables.test.jsx b/client/src/components/ManageAccount/tests/ManageAccountTables.test.jsx index 548df3bbb..25194294f 100644 --- a/client/src/components/ManageAccount/tests/ManageAccountTables.test.jsx +++ b/client/src/components/ManageAccount/tests/ManageAccountTables.test.jsx @@ -156,6 +156,33 @@ describe('Third Party Apps', () => { expect(getByText('Google Drive')).toBeDefined(); expect(getByText('Disconnect')).toBeDefined(); }); + it('Shows potential 3rd party connections other than Google Drive', () => { + const testStore = mockStore({ + profile: { + ...dummyState, + data: { + ...dummyState.data, + integrations: [ + { + label: '3rd Party Service', + description: '3rd Party Service description', + activated: true, + }, + ], + }, + }, + }); + const { getByText, queryByText } = render( + + + + ); + expect(getByText(/3rd Party Apps/)).toBeInTheDocument(); + // Check that Google Drive is not rendered + expect(queryByText('Google Drive')).toBeNull(); + // Check that other integrations are rendered + expect(getByText('3rd Party Service')).toBeInTheDocument(); + }); }); describe('License Cell', () => { diff --git a/client/src/components/PublicData/PublicData.jsx b/client/src/components/PublicData/PublicData.jsx index d32750e66..29afbee7e 100644 --- a/client/src/components/PublicData/PublicData.jsx +++ b/client/src/components/PublicData/PublicData.jsx @@ -119,6 +119,7 @@ const PublicDataListing = ({ canDownload, downloadCallback }) => { disabled={!canDownload} /> } + contentLayoutName="oneColumn" > state.files.operationStatus.copy, - shallowEqual - ); - - const setStatus = (newStatus) => - dispatch({ - type: 'DATA_FILES_SET_OPERATION_STATUS', - payload: { operation: 'copy', status: newStatus }, - }); - - const copy = ({ srcApi, destApi, destSystem, destPath, name, callback }) => { - const filteredSelected = selected - .filter((f) => status[f.id] !== 'SUCCESS') - .map((f) => ({ ...f, api: srcApi })); - dispatch({ - type: 'DATA_FILES_COPY', - payload: { - dest: { system: destSystem, path: destPath, api: destApi, name }, - src: filteredSelected, - reloadCallback: callback, - }, - }); - }; - - return { copy, status, setStatus }; -} - -export default useCopy; diff --git a/client/src/hooks/datafiles/mutations/useCopy.ts b/client/src/hooks/datafiles/mutations/useCopy.ts new file mode 100644 index 000000000..3398feade --- /dev/null +++ b/client/src/hooks/datafiles/mutations/useCopy.ts @@ -0,0 +1,157 @@ +import { useDispatch, useSelector, shallowEqual } from 'react-redux'; +import { useSelectedFiles } from 'hooks/datafiles'; +import Cookies from 'js-cookie'; +import { apiClient } from 'utils/apiClient'; +import { useMutation } from '@tanstack/react-query'; +import truncateMiddle from 'utils/truncateMiddle'; + +export async function copyFileUtil({ + api, + scheme, + system, + path, + filename, + filetype, + destApi, + destSystem, + destPath, + destPathName, +}: { + api: string; + scheme: string; + system: string; + path: string; + filename: string; + filetype: string; + destApi: string; + destSystem: string; + destPath: string; + destPathName: string; +}) { + let url: string, body: any; + if (api === destApi) { + url = `/api/datafiles/${api}/copy/${scheme}/${system}/${path}/`; + url = url.replace(/\/{2,}/g, '/'); + body = { + dest_system: destSystem, + dest_path: destPath, + file_name: filename, + filetype, + dest_path_name: destPathName, + }; + } else { + url = `/api/datafiles/transfer/${filetype}/`; + url = url.replace(/\/{2,}/g, '/'); + body = { + src_api: api, + dest_api: destApi, + src_system: system, + dest_system: destSystem, + src_path: path, + dest_path: destPath, + dest_path_name: destPathName, + dirname: filename, + }; + } + + const response = await apiClient.put(url, body, { + headers: { 'X-CSRFToken': Cookies.get('csrftoken') || '' }, + withCredentials: true, + }); + return response.data; +} + +function useCopy() { + const dispatch = useDispatch(); + + const { selectedFiles: selected } = useSelectedFiles(); + + const status = useSelector( + (state: any) => state.files.operationStatus.copy, + shallowEqual + ); + + const { scheme } = useSelector( + (state: any) => state.files.params.FilesListing + ); + const setStatus = (newStatus: string) => + dispatch({ + type: 'DATA_FILES_SET_OPERATION_STATUS', + payload: { operation: 'copy', status: newStatus }, + }); + + const { mutateAsync } = useMutation({ mutationFn: copyFileUtil }); + const copy = ({ + srcApi, + destApi, + destSystem, + destPath, + name, + callback, + }: { + srcApi: string; + destApi: string; + destSystem: string; + destPath: string; + name: string; + callback: any; + }) => { + const filteredSelected = selected + .filter((f: any) => status[f.id] !== 'SUCCESS') + .map((f: any) => ({ ...f, api: srcApi })); + const copyCalls: Promise[] = filteredSelected.map((file: any) => { + // Copy File + dispatch({ + type: 'DATA_FILES_SET_OPERATION_STATUS_BY_KEY', + payload: { status: 'RUNNING', key: file.id, operation: 'copy' }, + }); + return mutateAsync( + { + api: file.api, + scheme: scheme, + system: file.system, + path: file.path, + filename: file.name, + filetype: file.type, + destApi, + destSystem, + destPath, + destPathName: name, + }, + { + onSuccess: () => { + dispatch({ + type: 'DATA_FILES_SET_OPERATION_STATUS_BY_KEY', + payload: { status: 'SUCCESS', key: file.id, operation: 'copy' }, + }); + }, + onError: (error: any) => { + dispatch({ + type: 'DATA_FILES_SET_OPERATION_STATUS_BY_KEY', + payload: { status: 'ERROR', key: file.id, operation: 'copy' }, + }); + }, + } + ); + }); + // Result + Promise.all(copyCalls).then(() => { + dispatch({ + type: 'DATA_FILES_TOGGLE_MODAL', + payload: { operation: 'copy', props: {} }, + }); + dispatch({ + type: 'ADD_TOAST', + payload: { + message: `${ + copyCalls.length > 1 ? `${copyCalls.length} files` : 'File' + } copied to ${truncateMiddle(`${destPath}`, 20) || '/'}`, + }, + }); + callback(); + }); + }; + return { copy, status, setStatus }; +} + +export default useCopy; diff --git a/server/portal/apps/workspace/api/views.py b/server/portal/apps/workspace/api/views.py index 69571375f..af07ad54d 100644 --- a/server/portal/apps/workspace/api/views.py +++ b/server/portal/apps/workspace/api/views.py @@ -429,7 +429,8 @@ def get(self, request, job_uuid): class AppsTrayView(BaseApiView): def getPrivateApps(self, user): tapis = user.tapis_oauth.client - apps_listing = tapis.apps.getApps(select="version,id,notes", search="(enabled.eq.true)", listType="MINE") + # Only shows enabled versions of apps + apps_listing = tapis.apps.getApps(select="version,id,notes", search="(versionEnabled.eq.true)", listType="MINE") my_apps = list(map(lambda app: { "label": getattr(app.notes, 'label', app.id), "version": app.version, @@ -441,7 +442,8 @@ def getPrivateApps(self, user): def getPublicApps(self, user): tapis = user.tapis_oauth.client - apps_listing = tapis.apps.getApps(select="version,id,notes", search="(enabled.eq.true)", listType="SHARED_PUBLIC") + # Only shows enabled versions of apps + apps_listing = tapis.apps.getApps(select="version,id,notes", search="(versionEnabled.eq.true)", listType="SHARED_PUBLIC") categories = [] html_definitions = {} # Traverse category records in descending priority diff --git a/server/portal/settings/settings_default.py b/server/portal/settings/settings_default.py index 5bb0592e8..421d62e19 100644 --- a/server/portal/settings/settings_default.py +++ b/server/portal/settings/settings_default.py @@ -110,14 +110,6 @@ 'readOnly': False, 'hideSearchBar': False }, - { - 'name': 'Google Drive', - 'system': 'googledrive', - 'scheme': 'private', - 'api': 'googledrive', - 'icon': None, - 'integration': 'portal.apps.googledrive_integration' - } ] ########################