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

fix: share with users #32

Merged
merged 5 commits into from
May 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions src/tapis-api/apps/share.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 shareApp = (
request: Apps.ShareAppRequest,
basePath: string,
jwt: string
) => {
const api: Apps.SharingApi = apiGenerator<Apps.SharingApi>(
Apps,
Apps.SharingApi,
basePath,
jwt
);
return errorDecoder<Apps.RespBasic>(() => api.shareApp(request));
};

export default shareApp;
124 changes: 91 additions & 33 deletions src/tapis-app/Apps/_components/Toolbar/ShareModal/ShareModal.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { useEffect, useCallback, useState } from 'react';
import { Button } from 'reactstrap';
import { DropdownSelector, GenericModal } from 'tapis-ui/_common';
import { DropdownSelector, FormikInput, GenericModal } from 'tapis-ui/_common';
import { SubmitWrapper } from 'tapis-ui/_wrappers';
import { FileListingTable } from 'tapis-ui/components/files/FileListing/FileListing';
import { ToolbarModalProps } from '../Toolbar';
import { focusManager } from 'react-query';
import { Column } from 'react-table';
Expand All @@ -16,13 +15,20 @@ import useSharePublic, {
} from 'tapis-hooks/apps/useSharePublic';
import { AppListingTable } from 'tapis-ui/components/apps/AppListing';
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';

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 [isPublishedApp, setIsPublishedApp] = useState(false);

const getAllUsers = selectedApps.map((app) => app.sharedWithUsers);
const [users, setUsers] = useState<Array<string>>(
getAllUsers.filter(String).flat() as Array<string>
);
useEffect(() => {
reset();
}, [reset]);
Expand All @@ -31,16 +37,23 @@ const ShareModel: React.FC<ToolbarModalProps> = ({ toggle }) => {
resetUnshare();
}, [resetUnshare]);

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

const onComplete = useCallback(() => {
// Calling the focus manager triggers react-query's
// automatic refetch on window focus
focusManager.setFocused(true);
}, []);

const { run, state, isLoading, isSuccess, error } = useAppsOperations<
ShareHookParams,
Apps.RespChangeCount
>({
const {
run: runSharePublic,
state: stateSharePublic,
isLoading: isLoadingSharePublic,
isSuccess: isSuccessSharePublic,
error: errorSharePublic,
} = useAppsOperations<ShareHookParams, Apps.RespChangeCount>({
fn: shareAppPublicAsync,
onComplete,
});
Expand All @@ -56,17 +69,37 @@ const ShareModel: React.FC<ToolbarModalProps> = ({ toggle }) => {
onComplete,
});

const {
run: runShare,
state: stateShare,
isLoading: isLoadingShare,
isSuccess: isSuccessShare,
error: errorShare,
} = useAppsOperations<ShareUserHookParams, Apps.RespChangeCount>({
fn: shareAppAsync,
onComplete,
});

const onSubmit = useCallback(() => {
const operations: Array<ShareHookParams> = selectedApps.map((app) => ({
id: app.id!,
}));
if (isPublishedApp) {
run(operations);
runSharePublic(operations);
}
if (!isPublishedApp) {
runUnshare(operations);
}
}, [selectedApps, run, runUnshare]);
const userOperations: Array<ShareUserHookParams> = selectedApps.map(
(app) => ({
id: app.id!,
reqShareUpdate: {
users,
},
})
);
runShare(userOperations);
}, [selectedApps, runSharePublic, runUnshare]);

const removeApps = useCallback(
(file: Apps.TapisApp) => {
Expand All @@ -84,7 +117,7 @@ const ShareModel: React.FC<ToolbarModalProps> = ({ toggle }) => {
id: 'deleteStatus',
Cell: (el) => {
const file = selectedApps[el.row.index];
if (!state[file.id!]) {
if (!stateSharePublic[file.id!]) {
return (
<span
className={styles['remove-file']}
Expand All @@ -96,11 +129,17 @@ const ShareModel: React.FC<ToolbarModalProps> = ({ toggle }) => {
</span>
);
}
return <AppsOperationStatus status={state[file.id!].status} />;
return (
<AppsOperationStatus status={stateSharePublic[file.id!].status} />
);
},
},
];

const initialValues = {
visibility: 'private',
};

return (
<GenericModal
toggle={() => {
Expand All @@ -118,44 +157,63 @@ const ShareModel: React.FC<ToolbarModalProps> = ({ toggle }) => {
className={styles['file-list-table']}
/>
</div>
<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>
<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} />
</Form>
</Formik>
</div>
}
footer={
<SubmitWrapper
isLoading={false}
error={error}
success={isSuccess ? `Visibility changed` : ''}
error={errorSharePublic || errorShare || errorUnshare}
success={
isSuccessSharePublic || isSuccessUnshare ? `Visibility changed` : ''
}
reverse={true}
>
<Button
color="primary"
disabled={isLoading || isSuccess || selectedApps.length === 0}
disabled={
isLoadingSharePublic ||
isSuccessSharePublic ||
isLoadingShare ||
isSuccessShare ||
isLoadingUnshare ||
isSuccessUnshare ||
selectedApps.length === 0
}
aria-label="Submit"
onClick={onSubmit}
>
Confirm ({selectedApps.length})
</Button>
{!isSuccess && (
{!isSuccessSharePublic && (
<Button
color="danger"
disabled={isLoading || isSuccess || selectedApps.length === 0}
disabled={
isLoadingSharePublic ||
isSuccessSharePublic ||
selectedApps.length === 0
}
aria-label="Cancel"
onClick={() => {
toggle();
Expand Down
3 changes: 2 additions & 1 deletion src/tapis-hooks/apps/useList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import { useTapisConfig } from 'tapis-hooks';
import QueryKeys from './queryKeys';

export const defaultParams: Apps.GetAppsRequest = {
select: 'jobAttributes,version,updated,isPublic',
select: 'jobAttributes,version,updated,isPublic,owner,sharedWithUsers',
listType: Apps.ListTypeEnum.All,
};

const useList = (
Expand Down
60 changes: 60 additions & 0 deletions src/tapis-hooks/apps/useShare.ts
Original file line number Diff line number Diff line change
@@ -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 shareApp from 'tapis-api/apps/share';

export type ShareUserHookParams = {
id: string;
reqShareUpdate: Apps.ReqShareUpdate;
};

const useShare = () => {
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<Apps.RespBasic, Error, ShareUserHookParams>(
[QueryKeys.list, basePath, jwt],
(params) =>
shareApp(
{ appId: params.id, reqShareUpdate: params.reqShareUpdate },
basePath,
jwt
)
);

// Return hook object with loading states and login function
return {
isLoading,
isError,
isSuccess,
data,
error,
reset,
shareApp: (
params: ShareUserHookParams,
options?: MutateOptions<Apps.RespBasic, Error, ShareUserHookParams>
) => {
return mutate(params, options);
},
shareAppAsync: (
params: ShareUserHookParams,
options?: MutateOptions<Apps.RespBasic, Error, ShareUserHookParams>
) => mutateAsync(params, options),
};
};

export default useShare;
25 changes: 25 additions & 0 deletions src/tapis-ui/_common/Chip/Chip.styled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import Chip from '@mui/material/Chip';
import { styled } from '@mui/material/styles';

const ChipStyled = styled(Chip)(({ theme, size }) => {
return `
max-width: 100%;
margin: 2px 4px;
height: ${size === 'small' ? '26px' : '32px'};


&[aria-disabled="true"] > svg {
color: ${theme.palette.action.disabled};
cursor: default;
}

&.MuiChipsInput-Chip-Editing {
background-color: ${theme.palette.primary.light};
color: ${theme.palette.primary.contrastText};
}
`;
});

export default {
ChipStyled,
};
59 changes: 59 additions & 0 deletions src/tapis-ui/_common/Chip/Chip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import React from 'react';
import { KEYBOARD_KEY } from './constants/event';
import type { MuiChipsInputChipProps } from './index.types';
import Styled from './Chip.styled';

type ChipProps = MuiChipsInputChipProps;

const Chip = ({
className,
index,
onDelete,
disabled,
onEdit,
isEditing,
disableEdition,
...restChipProps
}: ChipProps) => {
const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === KEYBOARD_KEY.enter) {
onDelete(index);
}
};

const handleDelete = (event: MouseEvent) => {
event?.preventDefault?.();
event?.stopPropagation?.();
onDelete(index);
};

const handleDoubleClick = (event: React.MouseEvent) => {
const target = event.target as HTMLElement;

// Return if click on a svg icon
if (target.textContent !== restChipProps.label) {
return;
}

if (!disabled) {
onEdit(index);
}
};

return (
<Styled.ChipStyled
className={`MuiChipsInput-Chip ${
isEditing ? 'MuiChipsInput-Chip-Editing' : ''
} ${className || ''}`}
onKeyDown={handleKeyDown}
disabled={disabled}
onDoubleClick={disableEdition ? undefined : handleDoubleClick}
tabIndex={disabled ? -1 : 0}
aria-disabled={disabled}
onDelete={handleDelete}
{...restChipProps}
/>
);
};

export default Chip;
8 changes: 8 additions & 0 deletions src/tapis-ui/_common/Chip/constants/event.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export const KEYBOARD_KEY = {
enter: 'Enter',
backspace: 'Backspace',
};

export const KEYBOARD_KEYCODE = {
ime: 229,
};
Loading
Loading