diff --git a/src/api/common.ts b/src/api/common.ts index 3d7511ff3..0c7d4e17f 100644 --- a/src/api/common.ts +++ b/src/api/common.ts @@ -1,7 +1,13 @@ import * as React from "react"; import axios, { AxiosProgressEvent } from "axios"; import ChrisAPIClient from "./chrisapiclient"; -import { Pipeline, PipelineList, PluginPiping, Feed } from "@fnndsc/chrisapi"; +import { + Pipeline, + type PipelineList, + PluginPiping, + Feed, + PipelineInstance, +} from "@fnndsc/chrisapi"; export function useSafeDispatch(dispatch: any) { const mounted = React.useRef(false); @@ -184,7 +190,8 @@ export const fetchPipelines = async ( } = { error_message: "", }; - let registeredPipelinesList, registeredPipelines; + let registeredPipelinesList: PipelineList; + let registeredPipelines: PipelineInstance[]; const offset = perPage * (page - 1); const client = ChrisAPIClient.getClient(); const params = { @@ -194,17 +201,17 @@ export const fetchPipelines = async ( }; try { registeredPipelinesList = await client.getPipelines(params); - registeredPipelines = registeredPipelinesList.getItems(); + registeredPipelines = + (registeredPipelinesList.getItems() as PipelineInstance[]) || []; + return { + registeredPipelines, + registeredPipelinesList, + error: errorPayload, + }; } catch (error) { const errorObj = catchError(error); errorPayload = errorObj; } - - return { - registeredPipelines, - registeredPipelinesList, - error: errorPayload, - }; }; export async function fetchResources(pipelineInstance: Pipeline) { diff --git a/src/components/CreateFeed/CreateFeed.tsx b/src/components/CreateFeed/CreateFeed.tsx index 740ad1d3b..e891e6b9e 100644 --- a/src/components/CreateFeed/CreateFeed.tsx +++ b/src/components/CreateFeed/CreateFeed.tsx @@ -251,7 +251,7 @@ export default function CreateFeed() { )} - + { + const showDelete = useTypedSelector((state) => state.user.isStaff); const { pipelineData, selectedPipeline, pipelines } = state; const [errors, setErrors] = React.useState({}); const { goToNextStep: onNext, goToPrevStep: onBack } = @@ -119,23 +122,21 @@ const Pipelines = ({ dropdownValue.toLowerCase(), ); - const { registeredPipelines, registeredPipelinesList, error } = data; - - if (registeredPipelines) { - handleDispatchWrap(registeredPipelines); + if (data?.registeredPipelines) { + handleDispatchWrap(data?.registeredPipelines); } - if (registeredPipelinesList) { + if (data?.registeredPipelinesList) { setPageState((pageState) => { return { ...pageState, - itemCount: registeredPipelinesList.totalCount, + itemCount: data?.registeredPipelinesList.totalCount, }; }); } - if (error.error_message) { - throw new Error(error.error_message); + if (data?.error.error_message) { + throw new Error(data?.error.error_message); } return data; }, @@ -188,7 +189,7 @@ const Pipelines = ({ if (el) { //@ts-ignore - el!.scrollIntoView({ block: "center", behavior: "smooth" }); + el?.scrollIntoView({ block: "center", behavior: "smooth" }); } }); @@ -238,7 +239,7 @@ const Pipelines = ({ } //Not already expanded or not previous fetched and cached in state if ( - !(expanded && expanded[pipeline.data.id]) || + !expanded?.[pipeline.data.id] || !state.pipelineData[pipeline.data.id] ) { setPipeline(pipeline); @@ -265,7 +266,10 @@ const Pipelines = ({ const handleKeyDown = useCallback( (e: any, pipeline: Pipeline) => { - if (e.code == "Enter" && e.target.closest("DIV.pf-c-data-list__toggle")) { + if ( + e.code === "Enter" && + e.target.closest("DIV.pf-c-data-list__toggle") + ) { e.preventDefaut(); handleOnExpand(pipeline); } @@ -275,9 +279,9 @@ const Pipelines = ({ const handleBrowserKeyDown = useCallback( (e: any) => { - if (e.code == "ArrowLeft") { + if (e.code === "ArrowLeft") { onBack(); - } else if (e.code == "ArrowRight") { + } else if (e.code === "ArrowRight") { onNext(); } }, @@ -356,7 +360,7 @@ const Pipelines = ({ customIcon={} aria-label="search" onChange={(_event, value: string) => { - handlePipelineSearch && handlePipelineSearch(value.toLowerCase()); + handlePipelineSearch?.(value.toLowerCase()); }} /> @@ -368,69 +372,64 @@ const Pipelines = ({ onPerPageSelect={onPerPageSelect} /> - {isError && ( {error.message}} /> )} - {isLoading ? ( - ) : ( - pipelines.length > 0 && - pipelines.map((pipeline: any) => { - return ( - - - handleKeyDown(e, pipeline)} - onClick={() => handleOnExpand(pipeline)} - /> - -
- - {pipeline.data.name} - - - {pipeline.data.description} - -
- , - ]} - /> - - {!justDisplay && ( - - )} - + + {pipeline.data.name} + + + {pipeline.data.description} + + + , + ]} + /> + + {!justDisplay && ( + + )} + {/*Hardcode to only allow the user 'chris' to delete the pipelines*/} + {showDelete && ( - -
- - {((expanded && expanded[pipeline.data.id]) || - state.pipelineData[pipeline.data.id]) && - !isResourcesLoading && - !isResourceError ? ( - <> -
- - -
+ )} + + - + {(expanded?.[pipeline.data.id] || + state.pipelineData[pipeline.data.id]) && + !isResourcesLoading && + !isResourceError ? ( + <> +
+ - - ) : ( - - )} - - - ); - }) + +
+ + + + ) : ( + + )} +
+
+ )) + ) : ( + )}
+
{Object.keys(errors).length > 0 && ( setErrors({})} /> diff --git a/src/components/FeedTree/ParentComponent.tsx b/src/components/FeedTree/ParentComponent.tsx index d3b8649ee..a4a7b5d52 100644 --- a/src/components/FeedTree/ParentComponent.tsx +++ b/src/components/FeedTree/ParentComponent.tsx @@ -1,10 +1,12 @@ import React from "react"; +import { Alert } from "antd"; import { useDispatch } from "react-redux"; -import { setFeedTreeProp } from "../../store/feed/actions"; import { PluginInstance } from "@fnndsc/chrisapi"; +import { setFeedTreeProp } from "../../store/feed/actions"; import FeedTree from "./FeedTree"; import { getFeedTree, TreeNodeDatum, getTsNodes } from "./data"; import { useTypedSelector } from "../../store/hooks"; +import { SpinContainer } from "../Common"; import "./FeedTree.css"; interface ParentComponentProps { @@ -60,11 +62,9 @@ const ParentComponent = (props: ParentComponentProps) => { changeOrientation={changeOrientation} /> ) : loading ? ( -
Loading...
+ ) : error ? ( -
-
There was an error
-
+ ) : null; }; diff --git a/src/components/Feeds/FeedListView.tsx b/src/components/Feeds/FeedListView.tsx index f2bc8f351..0c7f90b62 100644 --- a/src/components/Feeds/FeedListView.tsx +++ b/src/components/Feeds/FeedListView.tsx @@ -1,5 +1,5 @@ -import React, { useContext, useMemo } from "react"; -import { useLocation, useNavigate } from "react-router"; +import React, { useContext } from "react"; +import { useNavigate } from "react-router"; import { useDispatch } from "react-redux"; import { format } from "date-fns"; import { useQuery } from "@tanstack/react-query"; @@ -25,7 +25,7 @@ import SearchIcon from "@patternfly/react-icons/dist/esm/icons/search-icon"; import { Typography } from "antd"; import { cujs } from "chris-utility"; import { useTypedSelector } from "../../store/hooks"; -import { usePaginate } from "./usePaginate"; +import { usePaginate, useSearchQueryParams } from "./usePaginate"; import { setBulkSelect, removeBulkSelect, @@ -46,12 +46,6 @@ import { fetchPublicFeeds, fetchFeeds } from "./utilties"; const { Paragraph } = Typography; -function useSearchQueryParams() { - const { search } = useLocation(); - - return useMemo(() => new URLSearchParams(search), [search]); -} - function useSearchQuery(query: URLSearchParams) { const page = query.get("page") || 1; const search = query.get("search") || ""; @@ -79,7 +73,7 @@ const TableSelectable: React.FunctionComponent = () => { const { data, isLoading, isFetching } = useQuery({ queryKey: ["feeds", searchFolderData], queryFn: () => fetchFeeds(searchFolderData), - enabled: isLoggedIn && type === "private", + enabled: type === "private", }); const { @@ -139,16 +133,15 @@ const TableSelectable: React.FunctionComponent = () => { activeItem: "analyses", }), ); - if (bulkData && bulkData.current) { + if (bulkData?.current) { dispatch(removeAllSelect(bulkData.current)); } }, [dispatch]); React.useEffect(() => { - if (!type) { - const feedType = isLoggedIn ? "private" : "public"; + if (!type || (!isLoggedIn && type === "private")) { navigate( - `/feeds?search=${search}&searchType=${searchType}&page=${page}&perPage=${perPage}&type=${feedType}`, + `/feeds?search=${search}&searchType=${searchType}&page=${page}&perPage=${perPage}&type=public`, ); } }, [isLoggedIn, navigate, perPage, page, searchType, search, type]); @@ -187,7 +180,7 @@ const TableSelectable: React.FunctionComponent = () => { title={`New and Existing Analyses (${ type === "private" && data && data.totalFeedsCount ? data.totalFeedsCount - : publicFeeds && publicFeeds.totalFeedsCount + : publicFeeds?.totalFeedsCount ? publicFeeds.totalFeedsCount : 0 })`} @@ -286,6 +279,7 @@ const TableSelectable: React.FunctionComponent = () => { bulkSelect={bulkSelect} columnNames={columnNames} allFeeds={feedsToDisplay} + type={type} /> ); })} @@ -316,9 +310,16 @@ interface TableRowProps { size: string; status: string; }; + type: string; } -function TableRow({ feed, allFeeds, bulkSelect, columnNames }: TableRowProps) { +function TableRow({ + feed, + allFeeds, + bulkSelect, + columnNames, + type, +}: TableRowProps) { const navigate = useNavigate(); const [intervalMs, setIntervalMs] = React.useState(2000); const { isDarkTheme } = useContext(ThemeContext); @@ -352,20 +353,19 @@ function TableRow({ feed, allFeeds, bulkSelect, columnNames }: TableRowProps) { const { id, name: feedName, creation_date, creator_username } = feed.data; const { dispatch } = usePaginate(); - const progress = feedResources[id] && feedResources[id].details.progress; + const progress = feedResources[id]?.details.progress; - const size = feedResources[id] && feedResources[id].details.size; - const feedError = feedResources[id] && feedResources[id].details.error; - const runtime = feedResources[id] && feedResources[id].details.time; + const size = feedResources[id]?.details.size; + const feedError = feedResources[id]?.details.error; + const runtime = feedResources[id]?.details.time; - const feedProgressText = - feedResources[id] && feedResources[id].details.feedProgressText; + const feedProgressText = feedResources[id]?.details.feedProgressText; let threshold = Infinity; // If error in a feed => reflect in progres - let title = (progress ? progress : 0) + "%"; + let title = `${progress ? progress : 0}%`; let color = "blue"; if (feedError) { @@ -374,7 +374,7 @@ function TableRow({ feed, allFeeds, bulkSelect, columnNames }: TableRowProps) { } // If initial node in a feed fails - if (progress == 0 && feedError) { + if (progress === 0 && feedError) { color = "#00ff00"; title = "❌"; } @@ -384,7 +384,7 @@ function TableRow({ feed, allFeeds, bulkSelect, columnNames }: TableRowProps) { color = "#00ff00"; threshold = progress; } - if (progress == 100) { + if (progress === 100) { title = "✔️"; } @@ -413,7 +413,7 @@ function TableRow({ feed, allFeeds, bulkSelect, columnNames }: TableRowProps) {
- {feed && new Date(feed.data.creation_date).toDateString()} + {data && new Date(data?.feed?.data.creation_date).toDateString()}
diff --git a/src/components/Login/index.tsx b/src/components/Login/index.tsx index 6a8d32730..1c8a50d56 100644 --- a/src/components/Login/index.tsx +++ b/src/components/Login/index.tsx @@ -19,6 +19,7 @@ import { import ExclamationCircleIcon from "@patternfly/react-icons/dist/esm/icons/exclamation-circle-icon"; import { setAuthTokenSuccess } from "../../store/user/actions"; import "./Login.css"; +import ChrisAPIClient from "../../api/chrisapiclient"; export const SimpleLoginPage: React.FunctionComponent = () => { const dispatch = useDispatch(); @@ -33,8 +34,8 @@ export const SimpleLoginPage: React.FunctionComponent = () => { const navigate = useNavigate(); enum LoginErrorMessage { - invalidCredentials = `Invalid Credentials`, - serverError = `There was a problem connecting to the server!`, + invalidCredentials = "Invalid Credentials", + serverError = "There was a problem connecting to the server!", } async function handleSubmit( @@ -45,10 +46,34 @@ export const SimpleLoginPage: React.FunctionComponent = () => { event.preventDefault(); const authURL = import.meta.env.VITE_CHRIS_UI_AUTH_URL; - let token; + let token: string; try { token = await ChrisApiClient.getAuthToken(authURL, username, password); + if (token && username) { + const oneDayToSeconds = 24 * 60 * 60; + setCookie(`${username}_token`, token, { + path: "/", + maxAge: oneDayToSeconds, + }); + setCookie("username", username, { + path: "/", + maxAge: oneDayToSeconds, + }); + + const client = ChrisAPIClient.getClient(); + const user = await client.getUser(); + + dispatch( + setAuthTokenSuccess({ + token, + username: username, + isStaff: user.data.is_staff, + }), + ); + + navigate("/"); + } } catch (error: unknown) { setShowHelperText(true); // Allows error message to be displayed in red @@ -62,25 +87,6 @@ export const SimpleLoginPage: React.FunctionComponent = () => { : LoginErrorMessage.serverError, ); } - - if (token && username) { - dispatch( - setAuthTokenSuccess({ - token, - username: username, - }), - ); - const oneDayToSeconds = 24 * 60 * 60; - setCookie(`${username}_token`, token, { - path: "/", - maxAge: oneDayToSeconds, - }); - setCookie("username", username, { - path: "/", - maxAge: oneDayToSeconds, - }); - navigate("/"); - } } const handleUsernameChange = ( diff --git a/src/components/Signup/SignUpForm.tsx b/src/components/Signup/SignUpForm.tsx index 38687dc5f..c07fa5b22 100644 --- a/src/components/Signup/SignUpForm.tsx +++ b/src/components/Signup/SignUpForm.tsx @@ -14,7 +14,7 @@ import { HelperText, HelperTextItem, } from "@patternfly/react-core"; -import ChrisApiClient from "@fnndsc/chrisapi"; +import ChrisApiClient, { User } from "@fnndsc/chrisapi"; import { Link } from "react-router-dom"; import { has } from "lodash"; import { validate } from "email-validator"; @@ -27,7 +27,11 @@ type Validated = { }; interface SignUpFormProps { - setAuthTokenSuccess: (auth: { token: string; username: string }) => void; + setAuthTokenSuccess: (auth: { + token: string; + username: string; + isStaff: boolean; + }) => void; isShowPasswordEnabled?: boolean; showPasswordAriaLabel?: string; hidePasswordAriaLabel?: string; @@ -104,8 +108,8 @@ const SignUpForm: React.FC = ({ setLoading(true); const userURL = import.meta.env.VITE_CHRIS_UI_USERS_URL; const authURL = import.meta.env.VITE_CHRIS_UI_AUTH_URL; - let user; - let token; + let user: User; + let token: string; if (userURL) { try { @@ -121,6 +125,26 @@ const SignUpForm: React.FC = ({ userState.username, passwordState.password, ); + + if (user && token) { + const oneDayToSeconds = 24 * 60 * 60; + setCookie("username", user.data.username, { + path: "/", + maxAge: oneDayToSeconds, + }); + setCookie(`${user.data.username}_token`, token, { + path: "/", + maxAge: oneDayToSeconds, + }); + setAuthTokenSuccess({ + token, + username: user.data.username, + isStaff: user.data.is_staff, + }); + const then = new URLSearchParams(location.search).get("then"); + if (then) navigate(then); + else navigate("/"); + } } catch (error) { if (has(error, "response")) { if (has(error, "response.data.username")) { @@ -157,25 +181,6 @@ const SignUpForm: React.FC = ({ } } } - - if (user && token) { - const oneDayToSeconds = 24 * 60 * 60; - setCookie("username", user.data.username, { - path: "/", - maxAge: oneDayToSeconds, - }); - setCookie(`${user.data.username}_token`, token, { - path: "/", - maxAge: oneDayToSeconds, - }); - setAuthTokenSuccess({ - token, - username: user.data.username, - }); - const then = new URLSearchParams(location.search).get("then"); - if (then) navigate(then); - else navigate("/"); - } }; const passwordInput = ( @@ -225,7 +230,7 @@ const SignUpForm: React.FC = ({ username: value, }) } - > + /> {userState.invalidText} @@ -302,8 +307,11 @@ const SignUpForm: React.FC = ({ }; const mapDispatchToProps = (dispatch: Dispatch) => ({ - setAuthTokenSuccess: (auth: { token: string; username: string }) => - dispatch(setAuthTokenSuccess(auth)), + setAuthTokenSuccess: (auth: { + token: string; + username: string; + isStaff: boolean; + }) => dispatch(setAuthTokenSuccess(auth)), }); export default connect(null, mapDispatchToProps)(SignUpForm); diff --git a/src/components/Wrapper/Sidebar.tsx b/src/components/Wrapper/Sidebar.tsx index dcdf8127f..687fc5fb0 100644 --- a/src/components/Wrapper/Sidebar.tsx +++ b/src/components/Wrapper/Sidebar.tsx @@ -137,7 +137,7 @@ const AnonSidebarImpl: React.FC = ({ itemId="analyses" isActive={sidebarActiveItem === "analyses"} > - New and Existing Analyses + New and Existing Analyses Plugins diff --git a/src/store/feed/reducer.ts b/src/store/feed/reducer.ts index 4e8e8c14d..0e847d9c4 100644 --- a/src/store/feed/reducer.ts +++ b/src/store/feed/reducer.ts @@ -67,15 +67,14 @@ const reducer: Reducer = ( orientation: "vertical", }, }; - else { - return { - ...state, - feedTreeProp: { - ...state.feedTreeProp, - orientation: "horizontal", - }, - }; - } + + return { + ...state, + feedTreeProp: { + ...state.feedTreeProp, + orientation: "horizontal", + }, + }; } case FeedActionTypes.SET_ALL_SELECT: { diff --git a/src/store/user/actions.ts b/src/store/user/actions.ts index 1f94de2a2..b1b0bc3ca 100644 --- a/src/store/user/actions.ts +++ b/src/store/user/actions.ts @@ -4,6 +4,7 @@ import { UserActionTypes } from "./types"; export const setAuthTokenSuccess = (auth: { token: string; username: string; + isStaff: boolean; }) => action(UserActionTypes.SET_TOKEN_SUCCESS, auth); // NOTE: To be done: Save user token to cookie or session export const setUserLogout = (username: string) => action(UserActionTypes.LOGOUT_USER, username); diff --git a/src/store/user/reducer.ts b/src/store/user/reducer.ts index bc0fea80c..c84d2231a 100644 --- a/src/store/user/reducer.ts +++ b/src/store/user/reducer.ts @@ -12,6 +12,7 @@ const initialState: IUserState = { token: token, isRememberMe: false, isLoggedIn: token ? true : false, + isStaff: false, }; // ***** NOTE: Working ***** @@ -26,6 +27,7 @@ const reducer: Reducer = ( username: action.payload.username, token: action.payload.token, isLoggedIn: true, + isStaff: action.payload.isStaff, }; } case UserActionTypes.SET_TOKEN_ERROR: { diff --git a/src/store/user/types.ts b/src/store/user/types.ts index f97e984c6..42d100693 100644 --- a/src/store/user/types.ts +++ b/src/store/user/types.ts @@ -14,6 +14,7 @@ export interface IUserState { token?: string | null; isRememberMe?: boolean; isLoggedIn?: boolean; + isStaff: boolean; } export const UserActionTypes = keyMirror({