Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add: connect with upstream api to obtain allocation #50

Merged
merged 15 commits into from
Jul 15, 2024
11 changes: 11 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
10 changes: 7 additions & 3 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<React.StrictMode>
<TapisProvider basePath={resolveBasePath()}>
<Router>
<App />
</Router>
<UpstreamProvider basePath={upstreamBasePath}>
<Router>
<App />
</Router>
</UpstreamProvider>
</TapisProvider>
</React.StrictMode>,
document.getElementById('react-root')
Expand Down
18 changes: 18 additions & 0 deletions src/tapis-api/apps/unShare.ts
Original file line number Diff line number Diff line change
@@ -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.SharingApi>(
Apps,
Apps.SharingApi,
basePath,
jwt
);
return errorDecoder<Apps.RespBasic>(() => api.unShareApp(request));
};

export default unShareApp;
139 changes: 105 additions & 34 deletions src/tapis-app/Apps/_components/Toolbar/ShareModal/ShareModal.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useEffect, useCallback, useState } from 'react';
import { Button } from 'reactstrap';
import { Button, Container, FormGroup } 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';
Expand All @@ -18,17 +18,27 @@ 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';
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<ToolbarModalProps> = ({ 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<Array<string>>(
const [existingUsers, setExistingUsers] = useState<Array<string>>(
getAllUsers.filter(String).flat() as Array<string>
);
const [newUsers, setNewUsers] = useState<Array<string>>([]);
const [usersFromProjects, setUsersFromProjects] = useState<Array<string>>([]);
const [removedUsers, setRemovedUsers] = useState<Array<string>>([]);
useEffect(() => {
reset();
}, [reset]);
Expand All @@ -41,6 +51,10 @@ const ShareModel: React.FC<ToolbarModalProps> = ({ toggle }) => {
resetShare();
}, [resetShare]);

useEffect(() => {
resetUnShare();
}, [resetUnShare]);

const onComplete = useCallback(() => {
// Calling the focus manager triggers react-query's
// automatic refetch on window focus
Expand All @@ -59,11 +73,11 @@ const ShareModel: React.FC<ToolbarModalProps> = ({ toggle }) => {
});

const {
run: runUnshare,
state: stateUnshare,
isLoading: isLoadingUnshare,
isSuccess: isSuccessUnshare,
error: errorUnshare,
run: runUnsharePublic,
state: stateUnsharePublic,
isLoading: isLoadingUnsharePublic,
isSuccess: isSuccessUnsharePublic,
error: errorUnsharePublic,
} = useAppsOperations<ShareHookParams, Apps.RespChangeCount>({
fn: unShareAppPublicAsync,
onComplete,
Expand All @@ -80,6 +94,17 @@ const ShareModel: React.FC<ToolbarModalProps> = ({ toggle }) => {
onComplete,
});

const {
run: unShare,
state: stateUnshare,
isLoading: isLoadingUnshare,
isSuccess: isSuccessUnshare,
error: errorUnshare,
} = useAppsOperations<ShareUserHookParams, Apps.RespChangeCount>({
fn: unShareAppAsync,
onComplete,
});

const onSubmit = useCallback(() => {
const operations: Array<ShareHookParams> = selectedApps.map((app) => ({
id: app.id!,
Expand All @@ -88,20 +113,34 @@ const ShareModel: React.FC<ToolbarModalProps> = ({ 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<ShareUserHookParams> = selectedApps.map(
(app) => ({
id: app.id!,
reqShareUpdate: {
users,
users: mergedUsers,
},
})
);
runShare(userOperations);
}
}, [selectedApps, runSharePublic, runUnshare]);
if (removedUsers.length > 0) {
const userOperations: Array<ShareUserHookParams> = selectedApps.map(
(app) => ({
id: app.id!,
reqShareUpdate: {
users: removedUsers,
},
})
);
unShare(userOperations);
}
}, [selectedApps, runSharePublic, runUnsharePublic]);

const removeApps = useCallback(
(file: Apps.TapisApp) => {
Expand Down Expand Up @@ -142,6 +181,7 @@ const ShareModel: React.FC<ToolbarModalProps> = ({ toggle }) => {
visibility: 'private',
};

const sharedCounter = newUsers.length + usersFromProjects.length;
return (
<GenericModal
toggle={() => {
Expand All @@ -151,6 +191,15 @@ const ShareModel: React.FC<ToolbarModalProps> = ({ toggle }) => {
title={`Share apps`}
body={
<div>
<div>
Pending Actions for Selected Apps:
<ul>
<li>Unshare with: {removedUsers.length} users </li>
<li>
Share with: {newUsers.length + usersFromProjects.length} users
</li>
</ul>
</div>
<div className={styles['files-list-container']}>
<AppListingTable
apps={selectedApps}
Expand All @@ -161,34 +210,56 @@ const ShareModel: React.FC<ToolbarModalProps> = ({ toggle }) => {
</div>
<Formik initialValues={initialValues} onSubmit={onSubmit}>
<Form id="share-form">
<h3> General access </h3>
<DropdownSelector
type={undefined}
onChange={(e: any) => {
const value = e.target.value;
if (value === 'public') {
setIsPublishedApp(true);
}
if (value === 'private') {
setIsPublishedApp(false);
}
}}
>
<option value="private">Private</option>
<option value="public">Public</option>
</DropdownSelector>
<h3> Add users </h3>
<MuiChipsInput value={users} onChange={setUsers} />
<FormGroup>
<h3> General access </h3>
<DropdownSelector
type={undefined}
onChange={(e: any) => {
const value = e.target.value;
if (value === 'public') {
setIsPublishedApp(true);
}
if (value === 'private') {
setIsPublishedApp(false);
}
}}
>
<option value="private">Private</option>
<option value="public">Public</option>
</DropdownSelector>
</FormGroup>
<FormGroup>
<h3> Add new users </h3>
<MuiChipsInput
size="small"
value={newUsers}
onChange={setNewUsers}
/>
</FormGroup>
<FormGroup>
<h3> Add new an allocation </h3>
<ProjectSelector
users={usersFromProjects}
setUsers={setUsersFromProjects}
/>
</FormGroup>
<UserSelector
initialUsers={existingUsers}
removedUsers={removedUsers}
setRemovedUsers={setRemovedUsers}
/>
</Form>
</Formik>
</div>
}
footer={
<SubmitWrapper
isLoading={false}
error={errorSharePublic || errorShare || errorUnshare}
error={errorSharePublic || errorShare || errorUnsharePublic}
success={
isSuccessSharePublic || isSuccessUnshare ? `Visibility changed` : ''
isSuccessSharePublic || isSuccessUnsharePublic
? `Visibility changed`
: ''
}
reverse={true}
>
Expand All @@ -199,8 +270,8 @@ const ShareModel: React.FC<ToolbarModalProps> = ({ toggle }) => {
isSuccessSharePublic ||
isLoadingShare ||
isSuccessShare ||
isLoadingUnshare ||
isSuccessUnshare ||
isLoadingUnsharePublic ||
isSuccessUnsharePublic ||
selectedApps.length === 0
}
aria-label="Submit"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { useState } from 'react';
import { Container, FormGroup, Input, Label } from 'reactstrap';
import { QueryWrapper } from 'tapis-ui/_wrappers';
import { Project } from 'upstream-api/projects/types';
import { useList } from 'upstream-hooks/projects';

interface ProjectSelectorProps {
setUsers: (users: Array<string>) => void;
users: Array<string>;
}

const ProjectSelector = ({ setUsers, users }: ProjectSelectorProps) => {
const [selectedProjects, setSelectedProjects] = useState<number[]>([]);
const { data: projects, isLoading, error } = useList();
const handleCheckboxChange = (project: 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 (
<QueryWrapper isLoading={isLoading} error={error}>
<FormGroup>
{projects &&
projects?.map((project, index) => (
<FormGroup check key={index}>
<Label check>
<Input
type="checkbox"
checked={selectedProjects.includes(project.id)}
onChange={() => handleCheckboxChange(project)}
/>
{project.title}
</Label>
</FormGroup>
))}
</FormGroup>
</QueryWrapper>
);
};

export default ProjectSelector;
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
.link-button {
background: none;
background-color: none;
color: blue;
border: none;
padding: 0;
font: inherit;
cursor: pointer;
text-decoration: underline;
margin-left: 10px;
}

.link-button:hover {
color: darkblue;
}
Loading
Loading