From d2086db7079bbbf98b6bf21de7f07df1de1b7522 Mon Sep 17 00:00:00 2001 From: Maximiliano Osorio Date: Fri, 12 Jul 2024 12:26:33 -0400 Subject: [PATCH 01/15] create: upstream autenticator --- src/upstream-hooks/authenticator/index.ts | 1 + src/upstream-hooks/authenticator/queryKeys.ts | 5 ++ src/upstream-hooks/authenticator/useLogin.ts | 49 +++++++++++++++++++ 3 files changed, 55 insertions(+) create mode 100644 src/upstream-hooks/authenticator/index.ts create mode 100644 src/upstream-hooks/authenticator/queryKeys.ts create mode 100644 src/upstream-hooks/authenticator/useLogin.ts diff --git a/src/upstream-hooks/authenticator/index.ts b/src/upstream-hooks/authenticator/index.ts new file mode 100644 index 0000000..5e4b907 --- /dev/null +++ b/src/upstream-hooks/authenticator/index.ts @@ -0,0 +1 @@ +export { default as useLogin } from './useLogin'; diff --git a/src/upstream-hooks/authenticator/queryKeys.ts b/src/upstream-hooks/authenticator/queryKeys.ts new file mode 100644 index 0000000..cd47f37 --- /dev/null +++ b/src/upstream-hooks/authenticator/queryKeys.ts @@ -0,0 +1,5 @@ +const QueryKeys = { + login: 'upstream_authenticator/login', +}; + +export default QueryKeys; diff --git a/src/upstream-hooks/authenticator/useLogin.ts b/src/upstream-hooks/authenticator/useLogin.ts new file mode 100644 index 0000000..cfba625 --- /dev/null +++ b/src/upstream-hooks/authenticator/useLogin.ts @@ -0,0 +1,49 @@ +import { useMutation } from 'react-query'; +import { login } from 'upstream-api/authenticator'; +import QueryKeys from './queryKeys'; +import { UpstreamRespCreateToken } from 'upstream-api/authenticator/login'; +import useUpstreamConfig from 'upstream-hooks/context/useUpstreamConfig'; + +type LoginHookParams = { + username: string; + password: string; +}; + +const useLogin = () => { + const { setAccessToken, basePath } = useUpstreamConfig(); + + // On successful login, save the token to the TapisContext state + const onSuccess = (response: UpstreamRespCreateToken) => { + setAccessToken(response); + }; + + // The useMutation react-query hook is used to call operations that make server-side changes + // (Other hooks would be used for data retrieval) + // + // In this case, loginHelper is called to perform the operation, with an onSuccess callback + // passed as an option + const { mutate, isLoading, isError, isSuccess, error } = useMutation< + UpstreamRespCreateToken, + Error, + LoginHookParams + >( + [QueryKeys.login, basePath], + ({ username, password }) => login(username, password, basePath), + { onSuccess } + ); + + // Return hook object with loading states and login function + return { + isLoading, + isError, + isSuccess, + error, + login: (username: string, password: string) => { + // Call mutate to trigger a single post-like API operation + return mutate({ username, password }); + }, + logout: () => setAccessToken(null), + }; +}; + +export default useLogin; From 4fd909cd2cf96db76db5e7fca30461cf3d7b4883 Mon Sep 17 00:00:00 2001 From: Maximiliano Osorio Date: Fri, 12 Jul 2024 12:27:37 -0400 Subject: [PATCH 02/15] add: authentication api --- src/upstream-api/authenticator/index.ts | 1 + src/upstream-api/authenticator/login.ts | 33 +++++++++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 src/upstream-api/authenticator/index.ts create mode 100644 src/upstream-api/authenticator/login.ts diff --git a/src/upstream-api/authenticator/index.ts b/src/upstream-api/authenticator/index.ts new file mode 100644 index 0000000..ba0d927 --- /dev/null +++ b/src/upstream-api/authenticator/index.ts @@ -0,0 +1 @@ +export { default as login } from './login'; diff --git a/src/upstream-api/authenticator/login.ts b/src/upstream-api/authenticator/login.ts new file mode 100644 index 0000000..f32d8b4 --- /dev/null +++ b/src/upstream-api/authenticator/login.ts @@ -0,0 +1,33 @@ +import { Authenticator } from '@tapis/tapis-typescript'; +import { apiGenerator, errorDecoder } from 'tapis-api/utils'; + +export interface UpstreamRespCreateToken { + access_token: string; + token_type: string; +} + +export interface UpstreamNewAccessTokenResponse { + access_token: string; + token_type: string; +} +// This helper takes the username and password and assembles an API call +const login = ( + username: string, + password: string, + basePath: string +): Promise => { + const promise: Promise = fetch(`${basePath}/token`, { + method: 'POST', + headers: { + accept: 'application/json', + }, + body: new URLSearchParams({ + grant_type: 'password', + username: username, + password: password, + }), + }); + return promise; +}; + +export default login; From 2e83ad7e3be31ec4016edff4dfd5d532a98cb3e9 Mon Sep 17 00:00:00 2001 From: Maximiliano Osorio Date: Fri, 12 Jul 2024 12:29:04 -0400 Subject: [PATCH 03/15] add: upstream context --- .../context/UpstreamContext.tsx | 14 ++++++ src/upstream-hooks/context/index.ts | 3 ++ .../context/useUpstreamConfig.ts | 48 +++++++++++++++++++ 3 files changed, 65 insertions(+) create mode 100644 src/upstream-hooks/context/UpstreamContext.tsx create mode 100644 src/upstream-hooks/context/index.ts create mode 100644 src/upstream-hooks/context/useUpstreamConfig.ts diff --git a/src/upstream-hooks/context/UpstreamContext.tsx b/src/upstream-hooks/context/UpstreamContext.tsx new file mode 100644 index 0000000..af60160 --- /dev/null +++ b/src/upstream-hooks/context/UpstreamContext.tsx @@ -0,0 +1,14 @@ +import React from 'react'; + +export type UpstreamContextType = { + basePath: string; +}; + +export const authContext: UpstreamContextType = { + basePath: '', +}; + +const UpstreamContext: React.Context = + React.createContext(authContext); + +export default UpstreamContext; diff --git a/src/upstream-hooks/context/index.ts b/src/upstream-hooks/context/index.ts new file mode 100644 index 0000000..15ac742 --- /dev/null +++ b/src/upstream-hooks/context/index.ts @@ -0,0 +1,3 @@ +export { default as UpstreamContext } from './UpstreamContext'; +export type { UpstreamContextType } from './UpstreamContext'; +export { default as useUpstreamConfig } from './useUpstreamConfig'; diff --git a/src/upstream-hooks/context/useUpstreamConfig.ts b/src/upstream-hooks/context/useUpstreamConfig.ts new file mode 100644 index 0000000..78f61c9 --- /dev/null +++ b/src/upstream-hooks/context/useUpstreamConfig.ts @@ -0,0 +1,48 @@ +import { useContext } from 'react'; +import { useQuery } from 'react-query'; +import Cookies from 'js-cookie'; +import jwt_decode from 'jwt-decode'; +import UpstreamContext from './UpstreamContext'; +import { UpstreamNewAccessTokenResponse } from 'upstream-api/authenticator/login'; + +const useUpstreamConfig = () => { + const { basePath } = useContext(UpstreamContext); + + const getAccessToken = (): UpstreamNewAccessTokenResponse | undefined => { + const cookie = Cookies.get('upstream-token'); + if (!!cookie) return JSON.parse(cookie); + return undefined; + }; + + const { data, refetch } = useQuery< + UpstreamNewAccessTokenResponse | undefined + >('upstream-token', getAccessToken, { + initialData: () => getAccessToken(), + }); + + const setAccessToken = async ( + resp: UpstreamNewAccessTokenResponse | null | undefined + ): Promise => { + if (!resp) { + Cookies.remove('upstream-token'); + await refetch(); + return; + } + const expires = new Date(0); + Cookies.set('upstream-token', JSON.stringify(resp), { expires }); + await refetch(); + }; + + const claims: { [key: string]: any } = data?.access_token + ? jwt_decode(data?.access_token) + : {}; + + return { + basePath, + accessToken: data, + setAccessToken, + claims, + }; +}; + +export default useUpstreamConfig; From 6b20bf2e5dd16167e387d5efbd63c777a6972dfe Mon Sep 17 00:00:00 2001 From: Maximiliano Osorio Date: Fri, 12 Jul 2024 13:02:03 -0400 Subject: [PATCH 04/15] add: login to upstream server too --- src/tapis-app/Login/_components/Login/Login.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/tapis-app/Login/_components/Login/Login.tsx b/src/tapis-app/Login/_components/Login/Login.tsx index c258e82..f7aafdb 100644 --- a/src/tapis-app/Login/_components/Login/Login.tsx +++ b/src/tapis-app/Login/_components/Login/Login.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { Button } from 'reactstrap'; import { useLogin } from 'tapis-hooks/authenticator'; +import { useLogin as useUpstreamLogin } from 'upstream-hooks/authenticator'; import { useTapisConfig } from 'tapis-hooks/context'; import { FormikInput } from 'tapis-ui/_common'; import { SubmitWrapper } from 'tapis-ui/_wrappers'; @@ -9,6 +10,11 @@ import * as Yup from 'yup'; const Login: React.FC = () => { const { login, isLoading, error } = useLogin(); + const { + login: loginUpstream, + isLoading: isLoadingUpstream, + error: errorUpstream, + } = useUpstreamLogin(); const { accessToken } = useTapisConfig(); const onSubmit = ({ @@ -17,7 +23,10 @@ const Login: React.FC = () => { }: { username: string; password: string; - }) => login(username, password); + }) => { + login(username, password); + loginUpstream(username, password); + }; const loginSchema = Yup.object({ username: Yup.string().required(), From ef164412d5fb5253a6ce7ba2058933b3ffc8996f Mon Sep 17 00:00:00 2001 From: Maximiliano Osorio Date: Fri, 12 Jul 2024 13:02:38 -0400 Subject: [PATCH 05/15] add: upstream provider --- src/index.tsx | 10 ++++-- .../provider/UpstreamProvider.tsx | 32 +++++++++++++++++++ src/upstream-hooks/provider/index.ts | 3 ++ 3 files changed, 42 insertions(+), 3 deletions(-) create mode 100644 src/upstream-hooks/provider/UpstreamProvider.tsx create mode 100644 src/upstream-hooks/provider/index.ts diff --git a/src/index.tsx b/src/index.tsx index 9a08a31..16237ba 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -6,13 +6,17 @@ import TapisProvider from 'tapis-hooks/provider'; import 'tapis-ui/index.css'; import { resolveBasePath } from 'utils/resloveBasePath'; import reportWebVitals from './reportWebVitals'; +import UpstreamProvider from 'upstream-hooks/provider/UpstreamProvider'; +const upstreamBasePath = 'https://upstream-dso.tacc.utexas.edu'; ReactDOM.render( - - - + + + + + , document.getElementById('react-root') diff --git a/src/upstream-hooks/provider/UpstreamProvider.tsx b/src/upstream-hooks/provider/UpstreamProvider.tsx new file mode 100644 index 0000000..19184dd --- /dev/null +++ b/src/upstream-hooks/provider/UpstreamProvider.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { QueryClient, QueryClientProvider } from 'react-query'; +import { Authenticator } from '@tapis/tapis-typescript'; +import UpstreamContext, { + UpstreamContextType, +} from '../context/UpstreamContext'; + +interface UpstreamProviderProps { + token?: Authenticator.NewAccessTokenResponse; + basePath: string; +} + +const UpstreamProvider: React.FC< + React.PropsWithChildren +> = ({ token, basePath, children }) => { + // Provide a context state for the rest of the application, including + // a way of modifying the state + const contextValue: UpstreamContextType = { + basePath, + }; + + // react-query client + const queryClient = new QueryClient(); + + return ( + + {children} + + ); +}; + +export default UpstreamProvider; diff --git a/src/upstream-hooks/provider/index.ts b/src/upstream-hooks/provider/index.ts new file mode 100644 index 0000000..7c94c95 --- /dev/null +++ b/src/upstream-hooks/provider/index.ts @@ -0,0 +1,3 @@ +import { default as TapisProvider } from './UpstreamProvider'; + +export default TapisProvider; From 46aaa532921c6a7c2c8651450fc256fa88eadc51 Mon Sep 17 00:00:00 2001 From: Maximiliano Osorio Date: Fri, 12 Jul 2024 13:03:09 -0400 Subject: [PATCH 06/15] add: node-fetch types --- package-lock.json | 11 +++++++++++ package.json | 1 + 2 files changed, 12 insertions(+) diff --git a/package-lock.json b/package-lock.json index 3d6bf5d..18340d5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -70,6 +70,7 @@ "@types/js-cookie": "^2.2.7", "@types/lodash.clonedeep": "^4.5.6", "@types/node": "^18.0.0", + "@types/node-fetch": "^2.6.11", "@types/normalize-path": "^3.0.2", "@types/react": "^17.0.2", "@types/react-css-modules": "^4.6.3", @@ -4066,6 +4067,16 @@ "undici-types": "~5.26.4" } }, + "node_modules/@types/node-fetch": { + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.11.tgz", + "integrity": "sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==", + "dev": true, + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.0" + } + }, "node_modules/@types/normalize-path": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/@types/normalize-path/-/normalize-path-3.0.2.tgz", diff --git a/package.json b/package.json index bd7b1a5..a7a4882 100644 --- a/package.json +++ b/package.json @@ -90,6 +90,7 @@ "@types/js-cookie": "^2.2.7", "@types/lodash.clonedeep": "^4.5.6", "@types/node": "^18.0.0", + "@types/node-fetch": "^2.6.11", "@types/normalize-path": "^3.0.2", "@types/react": "^17.0.2", "@types/react-css-modules": "^4.6.3", From 9d2c0ed1d20332e3fbbf252628af4767af661fd3 Mon Sep 17 00:00:00 2001 From: Maximiliano Osorio Date: Fri, 12 Jul 2024 14:41:53 -0400 Subject: [PATCH 07/15] fix: authenticator --- src/upstream-api/authenticator/login.ts | 6 +++--- src/upstream-hooks/authenticator/useLogin.ts | 2 +- src/upstream-hooks/context/useUpstreamConfig.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/upstream-api/authenticator/login.ts b/src/upstream-api/authenticator/login.ts index f32d8b4..d45fcc4 100644 --- a/src/upstream-api/authenticator/login.ts +++ b/src/upstream-api/authenticator/login.ts @@ -11,12 +11,12 @@ export interface UpstreamNewAccessTokenResponse { token_type: string; } // This helper takes the username and password and assembles an API call -const login = ( +const login = async ( username: string, password: string, basePath: string ): Promise => { - const promise: Promise = fetch(`${basePath}/token`, { + const promise = await fetch(`${basePath}/token`, { method: 'POST', headers: { accept: 'application/json', @@ -27,7 +27,7 @@ const login = ( password: password, }), }); - return promise; + return promise.json(); }; export default login; diff --git a/src/upstream-hooks/authenticator/useLogin.ts b/src/upstream-hooks/authenticator/useLogin.ts index cfba625..584690b 100644 --- a/src/upstream-hooks/authenticator/useLogin.ts +++ b/src/upstream-hooks/authenticator/useLogin.ts @@ -2,7 +2,7 @@ import { useMutation } from 'react-query'; import { login } from 'upstream-api/authenticator'; import QueryKeys from './queryKeys'; import { UpstreamRespCreateToken } from 'upstream-api/authenticator/login'; -import useUpstreamConfig from 'upstream-hooks/context/useUpstreamConfig'; +import { useUpstreamConfig } from 'upstream-hooks/context'; type LoginHookParams = { username: string; diff --git a/src/upstream-hooks/context/useUpstreamConfig.ts b/src/upstream-hooks/context/useUpstreamConfig.ts index 78f61c9..18bb907 100644 --- a/src/upstream-hooks/context/useUpstreamConfig.ts +++ b/src/upstream-hooks/context/useUpstreamConfig.ts @@ -29,7 +29,7 @@ const useUpstreamConfig = () => { return; } const expires = new Date(0); - Cookies.set('upstream-token', JSON.stringify(resp), { expires }); + Cookies.set('upstream-token', JSON.stringify(resp)); await refetch(); }; From 47c6e0a22aef864a7a4106f0169ddb20d9d4d6b4 Mon Sep 17 00:00:00 2001 From: Maximiliano Osorio Date: Fri, 12 Jul 2024 14:42:15 -0400 Subject: [PATCH 08/15] add: hooks and api projects --- src/upstream-api/projects/index.ts | 1 + src/upstream-api/projects/list.ts | 11 ++++++++++ src/upstream-hooks/projects/index.ts | 1 + src/upstream-hooks/projects/queryKeys.ts | 5 +++++ src/upstream-hooks/projects/useList.ts | 27 ++++++++++++++++++++++++ 5 files changed, 45 insertions(+) create mode 100644 src/upstream-api/projects/index.ts create mode 100644 src/upstream-api/projects/list.ts create mode 100644 src/upstream-hooks/projects/index.ts create mode 100644 src/upstream-hooks/projects/queryKeys.ts create mode 100644 src/upstream-hooks/projects/useList.ts diff --git a/src/upstream-api/projects/index.ts b/src/upstream-api/projects/index.ts new file mode 100644 index 0000000..77cf7bf --- /dev/null +++ b/src/upstream-api/projects/index.ts @@ -0,0 +1 @@ +export { default as list } from './list'; diff --git a/src/upstream-api/projects/list.ts b/src/upstream-api/projects/list.ts new file mode 100644 index 0000000..4711fcd --- /dev/null +++ b/src/upstream-api/projects/list.ts @@ -0,0 +1,11 @@ +const list = (basePath: string, jwt: string) => { + console.log('basePath:', basePath); + return fetch(`${basePath}/projects`, { + headers: { + accept: 'application/json', + Authorization: 'Bearer ' + jwt, + }, + }); +}; + +export default list; diff --git a/src/upstream-hooks/projects/index.ts b/src/upstream-hooks/projects/index.ts new file mode 100644 index 0000000..a1016bc --- /dev/null +++ b/src/upstream-hooks/projects/index.ts @@ -0,0 +1 @@ +export { default as useList } from './useList'; diff --git a/src/upstream-hooks/projects/queryKeys.ts b/src/upstream-hooks/projects/queryKeys.ts new file mode 100644 index 0000000..26ebd00 --- /dev/null +++ b/src/upstream-hooks/projects/queryKeys.ts @@ -0,0 +1,5 @@ +const QueryKeys = { + list: 'jobs/list', +}; + +export default QueryKeys; diff --git a/src/upstream-hooks/projects/useList.ts b/src/upstream-hooks/projects/useList.ts new file mode 100644 index 0000000..385d242 --- /dev/null +++ b/src/upstream-hooks/projects/useList.ts @@ -0,0 +1,27 @@ +import { useQuery, QueryObserverOptions } from 'react-query'; +import { list } from 'upstream-api/projects'; +import { Jobs } from '@tapis/tapis-typescript'; +import QueryKeys from './queryKeys'; +import { useUpstreamConfig } from 'upstream-hooks/context'; + +export const defaultParams: Jobs.GetJobListRequest = { + orderBy: 'created(desc)', +}; + +const useList = () => { + const { accessToken, basePath } = useUpstreamConfig(); + const result = useQuery( + [QueryKeys.list, accessToken], + // Default to no token. This will generate a 403 when calling the list function + // which is expected behavior for not having a token + () => { + return list(basePath, accessToken?.access_token ?? ''); + }, + { + enabled: !!accessToken, + } + ); + return result; +}; + +export default useList; From ada7a73006b03db41cb5b3e594b52098d0158704 Mon Sep 17 00:00:00 2001 From: Maximiliano Osorio Date: Fri, 12 Jul 2024 14:58:14 -0400 Subject: [PATCH 09/15] fix: add project selector --- .../Toolbar/ShareModal/ShareModal.tsx | 8 +++-- .../ShareModal/components/ProjectSelector.tsx | 33 +++++++++++++++++++ src/upstream-api/projects/list.ts | 5 +-- 3 files changed, 42 insertions(+), 4 deletions(-) create mode 100644 src/tapis-app/Apps/_components/Toolbar/ShareModal/components/ProjectSelector.tsx diff --git a/src/tapis-app/Apps/_components/Toolbar/ShareModal/ShareModal.tsx b/src/tapis-app/Apps/_components/Toolbar/ShareModal/ShareModal.tsx index ea66d08..b4d4773 100644 --- a/src/tapis-app/Apps/_components/Toolbar/ShareModal/ShareModal.tsx +++ b/src/tapis-app/Apps/_components/Toolbar/ShareModal/ShareModal.tsx @@ -1,7 +1,7 @@ import { useEffect, useCallback, useState } from 'react'; import { Button } from 'reactstrap'; import { DropdownSelector, FormikInput, GenericModal } from 'tapis-ui/_common'; -import { SubmitWrapper } from 'tapis-ui/_wrappers'; +import { QueryWrapper, SubmitWrapper } from 'tapis-ui/_wrappers'; import { ToolbarModalProps } from '../Toolbar'; import { focusManager } from 'react-query'; import { Column } from 'react-table'; @@ -18,6 +18,8 @@ import useUnsharePublic from 'tapis-hooks/apps/useUnsharePublic'; import useShare, { ShareUserHookParams } from 'tapis-hooks/apps/useShare'; import { Form, Formik } from 'formik'; import { MuiChipsInput } from 'tapis-ui/_common/MuiChipsInput'; +import { useList } from 'upstream-hooks/projects'; +import ProjectSelector from './components/ProjectSelector'; const ShareModel: React.FC = ({ toggle }) => { const { selectedApps, unselect } = useAppsSelect(); @@ -177,8 +179,10 @@ const ShareModel: React.FC = ({ toggle }) => { -

Add users

+

Share with users

+

Share with allocation

+ diff --git a/src/tapis-app/Apps/_components/Toolbar/ShareModal/components/ProjectSelector.tsx b/src/tapis-app/Apps/_components/Toolbar/ShareModal/components/ProjectSelector.tsx new file mode 100644 index 0000000..59cd9f2 --- /dev/null +++ b/src/tapis-app/Apps/_components/Toolbar/ShareModal/components/ProjectSelector.tsx @@ -0,0 +1,33 @@ +import { Container, FormGroup, Input, Label } from 'reactstrap'; +import { QueryWrapper } from 'tapis-ui/_wrappers'; +import { useList } from 'upstream-hooks/projects'; + +const ProjectSelector = () => { + const { data, isLoading, error } = useList(); + return ( + + + + {data && + data?.map((project, index) => ( + + + {/* */} + + ))} + + + + ); +}; + +export default ProjectSelector; diff --git a/src/upstream-api/projects/list.ts b/src/upstream-api/projects/list.ts index 4711fcd..f34cf93 100644 --- a/src/upstream-api/projects/list.ts +++ b/src/upstream-api/projects/list.ts @@ -1,11 +1,12 @@ -const list = (basePath: string, jwt: string) => { +const list = async (basePath: string, jwt: string) => { console.log('basePath:', basePath); - return fetch(`${basePath}/projects`, { + const response = await fetch(`${basePath}/projects`, { headers: { accept: 'application/json', Authorization: 'Bearer ' + jwt, }, }); + return response.json(); }; export default list; From 495466ef0b1297e0dc37128506c4dc21dc0f4ba3 Mon Sep 17 00:00:00 2001 From: Maximiliano Osorio Date: Fri, 12 Jul 2024 15:00:25 -0400 Subject: [PATCH 10/15] fix: remove console --- src/upstream-api/projects/list.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/upstream-api/projects/list.ts b/src/upstream-api/projects/list.ts index f34cf93..0789fdf 100644 --- a/src/upstream-api/projects/list.ts +++ b/src/upstream-api/projects/list.ts @@ -1,5 +1,4 @@ const list = async (basePath: string, jwt: string) => { - console.log('basePath:', basePath); const response = await fetch(`${basePath}/projects`, { headers: { accept: 'application/json', From 566cb55479779d12fe21a79518979e73dc2e5485 Mon Sep 17 00:00:00 2001 From: Maximiliano Osorio Date: Fri, 12 Jul 2024 15:00:54 -0400 Subject: [PATCH 11/15] add: list members --- src/upstream-api/projects/members.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/upstream-api/projects/members.ts diff --git a/src/upstream-api/projects/members.ts b/src/upstream-api/projects/members.ts new file mode 100644 index 0000000..e476254 --- /dev/null +++ b/src/upstream-api/projects/members.ts @@ -0,0 +1,11 @@ +const members = async (project_id: number, basePath: string, jwt: string) => { + const response = await fetch(`${basePath}/projects/${project_id}/members`, { + headers: { + accept: 'application/json', + Authorization: 'Bearer ' + jwt, + }, + }); + return response.json(); +}; + +export default members; From 7aa68e271985b9b5bb259d88437fcd5786c95c65 Mon Sep 17 00:00:00 2001 From: Maximiliano Osorio Date: Fri, 12 Jul 2024 16:19:57 -0400 Subject: [PATCH 12/15] fix: share and unshare --- src/tapis-api/apps/unShare.ts | 18 ++++ .../Toolbar/ShareModal/ShareModal.tsx | 92 +++++++++++++++---- .../ShareModal/components/ProjectSelector.tsx | 53 +++++++---- .../ShareModal/components/UserSelector.tsx | 51 ++++++++++ src/tapis-hooks/apps/useUnShare.ts | 60 ++++++++++++ src/upstream-api/projects/list.ts | 10 +- src/upstream-hooks/projects/useList.ts | 1 + src/upstream-hooks/projects/useListMembers.ts | 28 ++++++ 8 files changed, 274 insertions(+), 39 deletions(-) create mode 100644 src/tapis-api/apps/unShare.ts create mode 100644 src/tapis-app/Apps/_components/Toolbar/ShareModal/components/UserSelector.tsx create mode 100644 src/tapis-hooks/apps/useUnShare.ts create mode 100644 src/upstream-hooks/projects/useListMembers.ts diff --git a/src/tapis-api/apps/unShare.ts b/src/tapis-api/apps/unShare.ts new file mode 100644 index 0000000..f1defb7 --- /dev/null +++ b/src/tapis-api/apps/unShare.ts @@ -0,0 +1,18 @@ +import { Apps } from '@tapis/tapis-typescript'; +import { apiGenerator, errorDecoder } from 'tapis-api/utils'; + +const unShareApp = ( + request: Apps.UnShareAppRequest, + basePath: string, + jwt: string +) => { + const api: Apps.SharingApi = apiGenerator( + Apps, + Apps.SharingApi, + basePath, + jwt + ); + return errorDecoder(() => api.unShareApp(request)); +}; + +export default unShareApp; diff --git a/src/tapis-app/Apps/_components/Toolbar/ShareModal/ShareModal.tsx b/src/tapis-app/Apps/_components/Toolbar/ShareModal/ShareModal.tsx index b4d4773..32dc49b 100644 --- a/src/tapis-app/Apps/_components/Toolbar/ShareModal/ShareModal.tsx +++ b/src/tapis-app/Apps/_components/Toolbar/ShareModal/ShareModal.tsx @@ -1,5 +1,5 @@ import { useEffect, useCallback, useState } from 'react'; -import { Button } from 'reactstrap'; +import { Button, Container } from 'reactstrap'; import { DropdownSelector, FormikInput, GenericModal } from 'tapis-ui/_common'; import { QueryWrapper, SubmitWrapper } from 'tapis-ui/_wrappers'; import { ToolbarModalProps } from '../Toolbar'; @@ -20,17 +20,25 @@ import { Form, Formik } from 'formik'; import { MuiChipsInput } from 'tapis-ui/_common/MuiChipsInput'; import { useList } from 'upstream-hooks/projects'; import ProjectSelector from './components/ProjectSelector'; +import UserSelector from './components/UserSelector'; +import { remove } from 'js-cookie'; +import unShareApp from 'tapis-api/apps/unShare'; +import useUnShare from 'tapis-hooks/apps/useUnShare'; const ShareModel: React.FC = ({ toggle }) => { const { selectedApps, unselect } = useAppsSelect(); const { shareAppPublicAsync, reset } = useSharePublic(); const { unShareAppPublicAsync, reset: resetUnshare } = useUnsharePublic(); const { shareAppAsync, reset: resetShare } = useShare(); + const { unShareAppAsync, reset: resetUnShare } = useUnShare(); const [isPublishedApp, setIsPublishedApp] = useState(false); const getAllUsers = selectedApps.map((app) => app.sharedWithUsers); - const [users, setUsers] = useState>( + const [existingUsers, setExistingUsers] = useState>( getAllUsers.filter(String).flat() as Array ); + const [newUsers, setNewUsers] = useState>([]); + const [usersFromProjects, setUsersFromProjects] = useState>([]); + const [removedUsers, setRemovedUsers] = useState>([]); useEffect(() => { reset(); }, [reset]); @@ -43,6 +51,10 @@ const ShareModel: React.FC = ({ toggle }) => { resetShare(); }, [resetShare]); + useEffect(() => { + resetUnShare(); + }, [resetUnShare]); + const onComplete = useCallback(() => { // Calling the focus manager triggers react-query's // automatic refetch on window focus @@ -61,11 +73,11 @@ const ShareModel: React.FC = ({ toggle }) => { }); const { - run: runUnshare, - state: stateUnshare, - isLoading: isLoadingUnshare, - isSuccess: isSuccessUnshare, - error: errorUnshare, + run: runUnsharePublic, + state: stateUnsharePublic, + isLoading: isLoadingUnsharePublic, + isSuccess: isSuccessUnsharePublic, + error: errorUnsharePublic, } = useAppsOperations({ fn: unShareAppPublicAsync, onComplete, @@ -82,6 +94,17 @@ const ShareModel: React.FC = ({ toggle }) => { onComplete, }); + const { + run: unShare, + state: stateUnshare, + isLoading: isLoadingUnshare, + isSuccess: isSuccessUnshare, + error: errorUnshare, + } = useAppsOperations({ + fn: unShareAppAsync, + onComplete, + }); + const onSubmit = useCallback(() => { const operations: Array = selectedApps.map((app) => ({ id: app.id!, @@ -90,20 +113,34 @@ const ShareModel: React.FC = ({ toggle }) => { runSharePublic(operations); } if (!isPublishedApp) { - runUnshare(operations); + runUnsharePublic(operations); } - if (users.length > 0) { + const newMergedUsers = [...usersFromProjects, ...newUsers]; + if (newMergedUsers.length > 0) { + //merge users and usersFromProjects + const mergedUsers = [...existingUsers, ...usersFromProjects, ...newUsers]; const userOperations: Array = selectedApps.map( (app) => ({ id: app.id!, reqShareUpdate: { - users, + users: mergedUsers, }, }) ); runShare(userOperations); } - }, [selectedApps, runSharePublic, runUnshare]); + if (removedUsers.length > 0) { + const userOperations: Array = selectedApps.map( + (app) => ({ + id: app.id!, + reqShareUpdate: { + users: removedUsers, + }, + }) + ); + unShare(userOperations); + } + }, [selectedApps, runSharePublic, runUnsharePublic]); const removeApps = useCallback( (file: Apps.TapisApp) => { @@ -153,6 +190,13 @@ const ShareModel: React.FC = ({ toggle }) => { title={`Share apps`} body={
+
+ The following actions will be applied to the selected apps: +
    +
  • Unshare: {removedUsers.length} users
  • +
  • Share: {newUsers.length} users
  • +
+
= ({ toggle }) => { -

Share with users

- -

Share with allocation

- +

Add new users

+ +

Add new an allocation

+ +
@@ -190,9 +242,11 @@ const ShareModel: React.FC = ({ toggle }) => { footer={ @@ -203,8 +257,8 @@ const ShareModel: React.FC = ({ toggle }) => { isSuccessSharePublic || isLoadingShare || isSuccessShare || - isLoadingUnshare || - isSuccessUnshare || + isLoadingUnsharePublic || + isSuccessUnsharePublic || selectedApps.length === 0 } aria-label="Submit" diff --git a/src/tapis-app/Apps/_components/Toolbar/ShareModal/components/ProjectSelector.tsx b/src/tapis-app/Apps/_components/Toolbar/ShareModal/components/ProjectSelector.tsx index 59cd9f2..840ff91 100644 --- a/src/tapis-app/Apps/_components/Toolbar/ShareModal/components/ProjectSelector.tsx +++ b/src/tapis-app/Apps/_components/Toolbar/ShareModal/components/ProjectSelector.tsx @@ -1,31 +1,46 @@ +import { useState } from 'react'; import { Container, FormGroup, Input, Label } from 'reactstrap'; import { QueryWrapper } from 'tapis-ui/_wrappers'; import { useList } from 'upstream-hooks/projects'; -const ProjectSelector = () => { +interface ProjectSelectorProps { + setUsers: (users: Array) => void; + users: Array; +} + +const ProjectSelector = ({ setUsers, users }: ProjectSelectorProps) => { + const [selectedProjects, setSelectedProjects] = useState([]); const { data, isLoading, error } = useList(); + const handleCheckboxChange = (project) => { + if (selectedProjects.includes(project.id)) { + setSelectedProjects(selectedProjects.filter((p) => p !== project.id)); + } else { + setSelectedProjects([...selectedProjects, project.id]); + const usernames = project.members.map((member) => member.username); + setUsers([...users, ...usernames]); + } + }; + return ( - - - {data && - data?.map((project, index) => ( - - - {/* */} - - ))} - - + + ))} + ); }; diff --git a/src/tapis-app/Apps/_components/Toolbar/ShareModal/components/UserSelector.tsx b/src/tapis-app/Apps/_components/Toolbar/ShareModal/components/UserSelector.tsx new file mode 100644 index 0000000..c64de3c --- /dev/null +++ b/src/tapis-app/Apps/_components/Toolbar/ShareModal/components/UserSelector.tsx @@ -0,0 +1,51 @@ +import { useState } from 'react'; +import { Container, FormGroup, Input, Label } from 'reactstrap'; +import { QueryWrapper } from 'tapis-ui/_wrappers'; +import { useList } from 'upstream-hooks/projects'; + +interface UserSelectorProps { + initialUsers: Array; + removedUsers: Array; + setRemovedUsers: (users: Array) => void; +} + +const UserSelector = ({ + initialUsers, + removedUsers, + setRemovedUsers, +}: UserSelectorProps) => { + const [users, setUsers] = useState(initialUsers); + const handleCheckboxChange = (user) => { + if (users.includes(user)) { + setUsers(users.filter((u) => u !== user)); + setRemovedUsers([...removedUsers, user]); + } + }; + + return ( +
+

Manage permissions

+ Shared with {initialUsers.length} users + + {users && + users?.map((user, index) => ( + + + {/* */} + + ))} + +
+ ); +}; + +export default UserSelector; diff --git a/src/tapis-hooks/apps/useUnShare.ts b/src/tapis-hooks/apps/useUnShare.ts new file mode 100644 index 0000000..96e3f0a --- /dev/null +++ b/src/tapis-hooks/apps/useUnShare.ts @@ -0,0 +1,60 @@ +import { useMutation, MutateOptions } from 'react-query'; +import { Apps } from '@tapis/tapis-typescript'; +import { useTapisConfig } from 'tapis-hooks/context'; +import QueryKeys from './queryKeys'; +import unShareApp from 'tapis-api/apps/unShare'; + +export type ShareUserHookParams = { + id: string; + reqShareUpdate: Apps.ReqShareUpdate; +}; + +const useUnShare = () => { + const { basePath, accessToken } = useTapisConfig(); + const jwt = accessToken?.access_token || ''; + + // The useMutation react-query hook is used to call operations that make server-side changes + // (Other hooks would be used for data retrieval) + // + // In this case, _share helper is called to perform the operation + const { + mutate, + mutateAsync, + isLoading, + isError, + isSuccess, + data, + error, + reset, + } = useMutation( + [QueryKeys.list, basePath, jwt], + (params) => + unShareApp( + { appId: params.id, reqShareUpdate: params.reqShareUpdate }, + basePath, + jwt + ) + ); + + // Return hook object with loading states and login function + return { + isLoading, + isError, + isSuccess, + data, + error, + reset, + unShareApp: ( + params: ShareUserHookParams, + options?: MutateOptions + ) => { + return mutate(params, options); + }, + unShareAppAsync: ( + params: ShareUserHookParams, + options?: MutateOptions + ) => mutateAsync(params, options), + }; +}; + +export default useUnShare; diff --git a/src/upstream-api/projects/list.ts b/src/upstream-api/projects/list.ts index 0789fdf..84531b9 100644 --- a/src/upstream-api/projects/list.ts +++ b/src/upstream-api/projects/list.ts @@ -1,3 +1,5 @@ +import members from './members'; + const list = async (basePath: string, jwt: string) => { const response = await fetch(`${basePath}/projects`, { headers: { @@ -5,7 +7,13 @@ const list = async (basePath: string, jwt: string) => { Authorization: 'Bearer ' + jwt, }, }); - return response.json(); + const projects = await response.json(); + return Promise.all( + projects.map(async (project: any) => { + project.members = await members(project.id, basePath, jwt); + return project; + }) + ); }; export default list; diff --git a/src/upstream-hooks/projects/useList.ts b/src/upstream-hooks/projects/useList.ts index 385d242..9993e81 100644 --- a/src/upstream-hooks/projects/useList.ts +++ b/src/upstream-hooks/projects/useList.ts @@ -21,6 +21,7 @@ const useList = () => { enabled: !!accessToken, } ); + console.log(result); return result; }; diff --git a/src/upstream-hooks/projects/useListMembers.ts b/src/upstream-hooks/projects/useListMembers.ts new file mode 100644 index 0000000..49fd709 --- /dev/null +++ b/src/upstream-hooks/projects/useListMembers.ts @@ -0,0 +1,28 @@ +import { useQuery, QueryObserverOptions } from 'react-query'; +import { list } from 'upstream-api/projects'; +import { Jobs } from '@tapis/tapis-typescript'; +import QueryKeys from './queryKeys'; +import { useUpstreamConfig } from 'upstream-hooks/context'; +import members from 'upstream-api/projects/members'; + +export const defaultParams: Jobs.GetJobListRequest = { + orderBy: 'created(desc)', +}; + +const useList = (project_id: number) => { + const { accessToken, basePath } = useUpstreamConfig(); + const result = useQuery( + [QueryKeys.list, accessToken], + // Default to no token. This will generate a 403 when calling the list function + // which is expected behavior for not having a token + () => { + return members(project_id, basePath, accessToken?.access_token ?? ''); + }, + { + enabled: !!accessToken, + } + ); + return result; +}; + +export default useList; From 2f4a31d9f1eac9b9a6871a0e716bc8dfaabc097e Mon Sep 17 00:00:00 2001 From: Maximiliano Osorio Date: Fri, 12 Jul 2024 16:27:35 -0400 Subject: [PATCH 13/15] add: css link --- .../Toolbar/ShareModal/components/UserSelector.css | 14 ++++++++++++++ .../Toolbar/ShareModal/components/UserSelector.tsx | 14 +++++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 src/tapis-app/Apps/_components/Toolbar/ShareModal/components/UserSelector.css diff --git a/src/tapis-app/Apps/_components/Toolbar/ShareModal/components/UserSelector.css b/src/tapis-app/Apps/_components/Toolbar/ShareModal/components/UserSelector.css new file mode 100644 index 0000000..d2483d9 --- /dev/null +++ b/src/tapis-app/Apps/_components/Toolbar/ShareModal/components/UserSelector.css @@ -0,0 +1,14 @@ +.link-button { + background: none; + color: blue; + border: none; + padding: 0; + font: inherit; + cursor: pointer; + text-decoration: underline; + margin-left: 10px; +} + +.link-button:hover { + color: darkblue; +} diff --git a/src/tapis-app/Apps/_components/Toolbar/ShareModal/components/UserSelector.tsx b/src/tapis-app/Apps/_components/Toolbar/ShareModal/components/UserSelector.tsx index c64de3c..34dc6a9 100644 --- a/src/tapis-app/Apps/_components/Toolbar/ShareModal/components/UserSelector.tsx +++ b/src/tapis-app/Apps/_components/Toolbar/ShareModal/components/UserSelector.tsx @@ -1,7 +1,8 @@ import { useState } from 'react'; -import { Container, FormGroup, Input, Label } from 'reactstrap'; +import { Button, Container, FormGroup, Input, Label } from 'reactstrap'; import { QueryWrapper } from 'tapis-ui/_wrappers'; import { useList } from 'upstream-hooks/projects'; +import './UserSelector.css'; interface UserSelectorProps { initialUsers: Array; @@ -26,6 +27,17 @@ const UserSelector = ({

Manage permissions

Shared with {initialUsers.length} users + {removedUsers.length > 0 && ( + + )} {users && users?.map((user, index) => ( From 2f268601fdc3da0a2bccda80da454cbc2c23b830 Mon Sep 17 00:00:00 2001 From: Maximiliano Osorio Date: Mon, 15 Jul 2024 08:50:02 -0400 Subject: [PATCH 14/15] fix: css --- .../Toolbar/ShareModal/ShareModal.tsx | 58 +++++++++++-------- .../ShareModal/components/UserSelector.tsx | 2 +- .../TextFieldChip/TextFieldChips.styled.ts | 5 +- 3 files changed, 37 insertions(+), 28 deletions(-) diff --git a/src/tapis-app/Apps/_components/Toolbar/ShareModal/ShareModal.tsx b/src/tapis-app/Apps/_components/Toolbar/ShareModal/ShareModal.tsx index 32dc49b..5902015 100644 --- a/src/tapis-app/Apps/_components/Toolbar/ShareModal/ShareModal.tsx +++ b/src/tapis-app/Apps/_components/Toolbar/ShareModal/ShareModal.tsx @@ -1,5 +1,5 @@ import { useEffect, useCallback, useState } from 'react'; -import { Button, Container } from 'reactstrap'; +import { Button, Container, FormGroup } from 'reactstrap'; import { DropdownSelector, FormikInput, GenericModal } from 'tapis-ui/_common'; import { QueryWrapper, SubmitWrapper } from 'tapis-ui/_wrappers'; import { ToolbarModalProps } from '../Toolbar'; @@ -207,29 +207,39 @@ const ShareModel: React.FC = ({ toggle }) => {
-

General access

- { - const value = e.target.value; - if (value === 'public') { - setIsPublishedApp(true); - } - if (value === 'private') { - setIsPublishedApp(false); - } - }} - > - - - -

Add new users

- -

Add new an allocation

- + +

General access

+ { + const value = e.target.value; + if (value === 'public') { + setIsPublishedApp(true); + } + if (value === 'private') { + setIsPublishedApp(false); + } + }} + > + + + +
+ +

Add new users

+ +
+ +

Add new an allocation

+ +
-

Manage permissions

+

Manage existing permissions

Shared with {initialUsers.length} users {removedUsers.length > 0 && ( */} ))} diff --git a/src/tapis-app/Apps/_components/Toolbar/ShareModal/components/UserSelector.css b/src/tapis-app/Apps/_components/Toolbar/ShareModal/components/UserSelector.css index d2483d9..6d45a29 100644 --- a/src/tapis-app/Apps/_components/Toolbar/ShareModal/components/UserSelector.css +++ b/src/tapis-app/Apps/_components/Toolbar/ShareModal/components/UserSelector.css @@ -1,5 +1,6 @@ .link-button { background: none; + background-color: none; color: blue; border: none; padding: 0; diff --git a/src/tapis-app/Apps/_components/Toolbar/ShareModal/components/UserSelector.tsx b/src/tapis-app/Apps/_components/Toolbar/ShareModal/components/UserSelector.tsx index 7515e59..28b605b 100644 --- a/src/tapis-app/Apps/_components/Toolbar/ShareModal/components/UserSelector.tsx +++ b/src/tapis-app/Apps/_components/Toolbar/ShareModal/components/UserSelector.tsx @@ -1,7 +1,5 @@ import { useState } from 'react'; -import { Button, Container, FormGroup, Input, Label } from 'reactstrap'; -import { QueryWrapper } from 'tapis-ui/_wrappers'; -import { useList } from 'upstream-hooks/projects'; +import { FormGroup, Input, Label, Button } from 'reactstrap'; import './UserSelector.css'; interface UserSelectorProps { @@ -16,7 +14,7 @@ const UserSelector = ({ setRemovedUsers, }: UserSelectorProps) => { const [users, setUsers] = useState(initialUsers); - const handleCheckboxChange = (user) => { + const handleCheckboxChange = (user: string) => { if (users.includes(user)) { setUsers(users.filter((u) => u !== user)); setRemovedUsers([...removedUsers, user]); @@ -27,8 +25,10 @@ const UserSelector = ({

Manage existing permissions

Shared with {initialUsers.length} users - {removedUsers.length > 0 && ( - + )} {users && diff --git a/src/upstream-api/projects/types.ts b/src/upstream-api/projects/types.ts new file mode 100644 index 0000000..4223b59 --- /dev/null +++ b/src/upstream-api/projects/types.ts @@ -0,0 +1,69 @@ +export interface Project { + id: number; + title: string; + description: string; + chargeCode: string; + gid: number; + source: any; + fieldId: number; + field: string; + typeId: number; + type: string; + piId: number; + pi: Pi; + allocations: Allocation[]; + nickname: any; + members: User[]; +} +interface User { + id: number; + username: string; + role: string; + firstName: string; + lastName: string; + email: string; +} + +export interface Pi { + id: number; + username: string; + email: string; + firstName: string; + lastName: string; + institution: string; + institutionId: number; + department: string; + departmentId: number; + citizenship: string; + citizenshipId: number; + source: string; + uid: number; + homeDirectory: string; + gid: number; +} + +export interface Allocation { + id: number; + start: string; + end: string; + status: string; + justification: string; + decisionSummary: any; + dateRequested: string; + dateReviewed: any; + computeRequested: number; + computeAllocated: number; + storageRequested: number; + storageAllocated: number; + memoryRequested: number; + memoryAllocated: number; + resourceId: number; + resource: string; + projectId: number; + project: string; + requestorId: number; + requestor: string; + reviewerId: number; + reviewer: any; + computeUsed: number; +} diff --git a/src/upstream-hooks/projects/useList.ts b/src/upstream-hooks/projects/useList.ts index 9993e81..5e67183 100644 --- a/src/upstream-hooks/projects/useList.ts +++ b/src/upstream-hooks/projects/useList.ts @@ -3,6 +3,7 @@ import { list } from 'upstream-api/projects'; import { Jobs } from '@tapis/tapis-typescript'; import QueryKeys from './queryKeys'; import { useUpstreamConfig } from 'upstream-hooks/context'; +import { Project } from 'upstream-api/projects/types'; export const defaultParams: Jobs.GetJobListRequest = { orderBy: 'created(desc)', @@ -10,7 +11,7 @@ export const defaultParams: Jobs.GetJobListRequest = { const useList = () => { const { accessToken, basePath } = useUpstreamConfig(); - const result = useQuery( + const result = useQuery( [QueryKeys.list, accessToken], // Default to no token. This will generate a 403 when calling the list function // which is expected behavior for not having a token @@ -21,7 +22,6 @@ const useList = () => { enabled: !!accessToken, } ); - console.log(result); return result; };