From 908d1e37c997214a51cda0126608835da2f29d90 Mon Sep 17 00:00:00 2001 From: PintoGideon <gideonpinto123@gmail.com> Date: Thu, 15 Feb 2024 14:51:00 -0500 Subject: [PATCH 1/3] Remove the delete option and only allow it if the is_staff property on the user payload is true --- src/api/common.ts | 25 ++- src/components/CreateFeed/CreateFeed.tsx | 2 +- src/components/CreateFeed/Pipelines.tsx | 241 +++++++++++----------- src/components/Feeds/FeedListView.tsx | 47 ++--- src/components/Feeds/FeedView.tsx | 32 +-- src/components/Feeds/usePaginate.tsx | 9 +- src/components/Feeds/utilties.ts | 2 - src/components/LibraryCopy/FolderCard.tsx | 47 +++-- src/components/Login/index.tsx | 50 +++-- src/components/Signup/SignUpForm.tsx | 60 +++--- src/store/feed/reducer.ts | 17 +- src/store/user/actions.ts | 1 + src/store/user/reducer.ts | 2 + src/store/user/types.ts | 1 + 14 files changed, 291 insertions(+), 245 deletions(-) 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() { )} </WizardStep> <WizardStep id={3} name="Pipelines"> - <PipelineContainer /> + <PipelineContainer justDisplay={true} /> </WizardStep> <WizardStep id={4} diff --git a/src/components/CreateFeed/Pipelines.tsx b/src/components/CreateFeed/Pipelines.tsx index b0d7cc4a9..d86407130 100644 --- a/src/components/CreateFeed/Pipelines.tsx +++ b/src/components/CreateFeed/Pipelines.tsx @@ -20,6 +20,7 @@ import { import { useQuery } from "@tanstack/react-query"; import { ErrorAlert } from "../Common"; import type { Pipeline } from "@fnndsc/chrisapi"; +import { EmptyStateComponent } from "../Common"; import SearchIcon from "@patternfly/react-icons/dist/esm/icons/search-icon"; import { @@ -37,6 +38,7 @@ import { } from "../../api/common"; import type { PipelinesProps } from "./types/pipeline"; import { Alert } from "antd"; +import { useTypedSelector } from "../../store/hooks"; export const PIPELINEQueryTypes = { NAME: ["Name", "Match plugin name containing this string"], @@ -75,6 +77,7 @@ const Pipelines = ({ handleSetCurrentComputeEnv, handleFormParameters, }: PipelinesProps) => { + 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={<SearchIcon />} aria-label="search" onChange={(_event, value: string) => { - handlePipelineSearch && handlePipelineSearch(value.toLowerCase()); + handlePipelineSearch?.(value.toLowerCase()); }} /> </div> @@ -368,69 +372,64 @@ const Pipelines = ({ onPerPageSelect={onPerPageSelect} /> </div> - <DataList aria-label="pipeline list"> {isError && ( <Alert type="error" description={<div>{error.message}</div>} /> )} - {isLoading ? ( <SpinContainer title="Fetching Pipelines" /> - ) : ( - pipelines.length > 0 && - pipelines.map((pipeline: any) => { - return ( - <DataListItem - isExpanded={ - expanded && expanded[pipeline.data.id] ? true : false - } - key={pipeline.data.id} - > - <DataListItemRow> - <DataListToggle - id={pipeline.data.id} - aria-controls="expand" - onKeyDown={(e) => handleKeyDown(e, pipeline)} - onClick={() => handleOnExpand(pipeline)} - /> - <DataListItemCells - dataListCells={[ - <DataListCell key={pipeline.data.name}> - <div - className="plugin-table-row" - key={pipeline.data.name} - > - <span className="plugin-table-row__plugin-name"> - {pipeline.data.name} - </span> - <span - className="plugin-table-row__plugin-description" - id={`${pipeline.data.description}`} - > - <em>{pipeline.data.description}</em> - </span> - </div> - </DataListCell>, - ]} - /> - <DataListAction - aria-labelledby="select a pipeline" - id={pipeline.data.id} - aria-label="actions" - className="pipelines" - > - {!justDisplay && ( - <Button - variant="primary" - key="select-action" - onClick={() => handleOnButtonClick(pipeline)} + ) : pipelines.length > 0 ? ( + pipelines.map((pipeline: any) => ( + <DataListItem + isExpanded={expanded?.[pipeline.data.id] ? true : false} + key={pipeline.data.id} + > + <DataListItemRow> + <DataListToggle + id={pipeline.data.id} + aria-controls="expand" + onKeyDown={(e) => handleKeyDown(e, pipeline)} + onClick={() => handleOnExpand(pipeline)} + /> + <DataListItemCells + dataListCells={[ + <DataListCell key={pipeline.data.name}> + <div + className="plugin-table-row" + key={pipeline.data.name} > - {selectedPipeline === pipeline.data.id - ? "Deselect" - : "Select"} - </Button> - )} - + <span className="plugin-table-row__plugin-name"> + {pipeline.data.name} + </span> + <span + className="plugin-table-row__plugin-description" + id={`${pipeline.data.description}`} + > + <em>{pipeline.data.description}</em> + </span> + </div> + </DataListCell>, + ]} + /> + <DataListAction + aria-labelledby="select a pipeline" + id={pipeline.data.id} + aria-label="actions" + className="pipelines" + > + {!justDisplay && ( + <Button + variant="primary" + key="select-action" + onClick={() => handleOnButtonClick(pipeline)} + > + {selectedPipeline === pipeline.data.id + ? "Deselect" + : "Select"} + </Button> + )} + {/*Hardcode to only allow the user 'chris' to delete the pipelines*/} + {showDelete && ( <Button key="delete-action" onClick={async () => { @@ -453,61 +452,65 @@ const Pipelines = ({ > Delete </Button> - </DataListAction> - </DataListItemRow> - <DataListContent - id={pipeline.data.id} - aria-label="PrimaryContent" - isHidden={!(expanded && expanded[pipeline.data.id])} - > - {((expanded && expanded[pipeline.data.id]) || - state.pipelineData[pipeline.data.id]) && - !isResourcesLoading && - !isResourceError ? ( - <> - <div - style={{ - display: "flex", - justifyContent: "space-between", - }} - > - <Tree - state={state.pipelineData[pipeline.data.id]} - currentPipelineId={pipeline.data.id} - handleNodeClick={handleNodeClick} - handleSetCurrentNode={handleSetCurrentNode} - handleSetPipelineEnvironments={ - handleSetPipelineEnvironments - } - handleSetCurrentNodeTitle={handleSetCurrentNodeTitle} - /> - <GeneralCompute - handleSetGeneralCompute={handleSetGeneralCompute} - currentPipelineId={pipeline.data.id} - /> - </div> + )} + </DataListAction> + </DataListItemRow> - <ConfigurationPage - justDisplay={justDisplay} - pipelines={pipelines} - pipeline={pipeline} - currentPipelineId={pipeline.data.id} + <DataListContent + id={pipeline.data.id} + aria-label="PrimaryContent" + isHidden={!expanded?.[pipeline.data.id]} + > + {(expanded?.[pipeline.data.id] || + state.pipelineData[pipeline.data.id]) && + !isResourcesLoading && + !isResourceError ? ( + <> + <div + style={{ + display: "flex", + justifyContent: "space-between", + }} + > + <Tree state={state.pipelineData[pipeline.data.id]} + currentPipelineId={pipeline.data.id} + handleNodeClick={handleNodeClick} + handleSetCurrentNode={handleSetCurrentNode} + handleSetPipelineEnvironments={ + handleSetPipelineEnvironments + } handleSetCurrentNodeTitle={handleSetCurrentNodeTitle} - handleDispatchPipelines={handleDispatchPipelines} - handleSetCurrentComputeEnv={handleSetCurrentComputeEnv} - handleFormParameters={handleFormParameters} /> - </> - ) : ( - <SpinContainer title="Fetching Pipeline Resources" /> - )} - </DataListContent> - </DataListItem> - ); - }) + <GeneralCompute + handleSetGeneralCompute={handleSetGeneralCompute} + currentPipelineId={pipeline.data.id} + /> + </div> + + <ConfigurationPage + justDisplay={justDisplay} + pipelines={pipelines} + pipeline={pipeline} + currentPipelineId={pipeline.data.id} + state={state.pipelineData[pipeline.data.id]} + handleSetCurrentNodeTitle={handleSetCurrentNodeTitle} + handleDispatchPipelines={handleDispatchPipelines} + handleSetCurrentComputeEnv={handleSetCurrentComputeEnv} + handleFormParameters={handleFormParameters} + /> + </> + ) : ( + <SpinContainer title="Fetching Pipeline Resources" /> + )} + </DataListContent> + </DataListItem> + )) + ) : ( + <EmptyStateComponent title="No Pipelines were found registered to your ChRIS instance" /> )} </DataList> + <div id="error"> {Object.keys(errors).length > 0 && ( <ErrorAlert errors={errors} cleanUpErrors={() => setErrors({})} /> diff --git a/src/components/Feeds/FeedListView.tsx b/src/components/Feeds/FeedListView.tsx index f2bc8f351..a2d2880f3 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,7 +133,7 @@ const TableSelectable: React.FunctionComponent = () => { activeItem: "analyses", }), ); - if (bulkData && bulkData.current) { + if (bulkData?.current) { dispatch(removeAllSelect(bulkData.current)); } }, [dispatch]); @@ -187,7 +181,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 +280,7 @@ const TableSelectable: React.FunctionComponent = () => { bulkSelect={bulkSelect} columnNames={columnNames} allFeeds={feedsToDisplay} + type={type} /> ); })} @@ -316,9 +311,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 +354,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 +375,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 +385,7 @@ function TableRow({ feed, allFeeds, bulkSelect, columnNames }: TableRowProps) { color = "#00ff00"; threshold = progress; } - if (progress == 100) { + if (progress === 100) { title = "✔️"; } @@ -413,7 +414,7 @@ function TableRow({ feed, allFeeds, bulkSelect, columnNames }: TableRowProps) { <Button variant="link" onClick={() => { - navigate(`/feeds/${id}`); + navigate(`/feeds/${id}?type=${type}`); }} > {feedName} diff --git a/src/components/Feeds/FeedView.tsx b/src/components/Feeds/FeedView.tsx index b4d4248a2..726285288 100644 --- a/src/components/Feeds/FeedView.tsx +++ b/src/components/Feeds/FeedView.tsx @@ -34,8 +34,12 @@ import WrapperConnect from "../Wrapper"; import { resetActiveResources } from "../../store/resources/actions"; import FeedOutputBrowser from "../FeedOutputBrowser/FeedOutputBrowser"; import { fetchAuthenticatedFeed, fetchPublicFeed } from "./utilties"; +import { useSearchQueryParams } from "./usePaginate"; export default function FeedView() { + const query = useSearchQueryParams(); + const type = query.get("type"); + const params = useParams(); const dispatch = useDispatch(); const navigate = useNavigate(); @@ -61,38 +65,36 @@ export default function FeedView() { const { data: publicFeed } = useQuery({ queryKey: ["publicFeed", id], queryFn: () => fetchPublicFeed(id), - enabled: !isLoggedIn, + enabled: type === "public", }); - const { data: feed } = useQuery({ + const { data: privateFeed } = useQuery({ queryKey: ["authenticatedFeed", id], queryFn: () => fetchAuthenticatedFeed(id), - enabled: isLoggedIn, + enabled: type === "private", }); React.useEffect(() => { - if (isLoggedIn && feed) { - dispatch(getFeedSuccess(feed as Feed)); - dispatch(getPluginInstancesRequest(feed)); + if (!type) { + navigate("/feeds?type=public"); } + }, [type, navigate]); - if (!isLoggedIn && publicFeed && Object.keys(publicFeed).length > 0) { - dispatch(getFeedSuccess(publicFeed as any as Feed)); - dispatch(getPluginInstancesRequest(publicFeed as Feed)); + React.useEffect(() => { + const feed: Feed | undefined = privateFeed || publicFeed; + if (feed) { + dispatch(getFeedSuccess(feed as Feed)); + dispatch(getPluginInstancesRequest(feed)); } if (!isLoggedIn && publicFeed && Object.keys(publicFeed).length === 0) { navigate("/feeds?type=public"); } - }, [isLoggedIn, feed, publicFeed, dispatch, navigate]); + }, [isLoggedIn, privateFeed, publicFeed, dispatch, navigate]); React.useEffect(() => { return () => { - if ( - dataRef.current && - dataRef.current.selectedPlugin && - dataRef.current.data - ) { + if (dataRef.current?.selectedPlugin && dataRef.current.data) { dispatch(resetActiveResources(dataRef.current)); } dispatch(resetFeed()); diff --git a/src/components/Feeds/usePaginate.tsx b/src/components/Feeds/usePaginate.tsx index 4674a69f9..875bc0ab0 100644 --- a/src/components/Feeds/usePaginate.tsx +++ b/src/components/Feeds/usePaginate.tsx @@ -1,4 +1,5 @@ -import { useState, useCallback } from "react"; +import { useState, useCallback, useMemo } from "react"; +import { useLocation } from "react-router"; import { useDispatch } from "react-redux"; import { debounce } from "lodash"; @@ -61,3 +62,9 @@ export const usePaginate = () => { dispatch, }; }; + +export function useSearchQueryParams() { + const { search } = useLocation(); + + return useMemo(() => new URLSearchParams(search), [search]); +} diff --git a/src/components/Feeds/utilties.ts b/src/components/Feeds/utilties.ts index b961a14f2..2acf99a08 100644 --- a/src/components/Feeds/utilties.ts +++ b/src/components/Feeds/utilties.ts @@ -116,7 +116,5 @@ export async function fetchPublicFeed(id?: string) { if (publicFeed && publicFeed.getItems().length > 0) { //@ts-ignore return publicFeed.getItems()[0] as any as Feed; - } else { - return {}; } } diff --git a/src/components/LibraryCopy/FolderCard.tsx b/src/components/LibraryCopy/FolderCard.tsx index 898cdcae2..72bdf121b 100644 --- a/src/components/LibraryCopy/FolderCard.tsx +++ b/src/components/LibraryCopy/FolderCard.tsx @@ -8,10 +8,10 @@ import { import { useQuery } from "@tanstack/react-query"; import { Link } from "react-router-dom"; import FaFolder from "@patternfly/react-icons/dist/esm/icons/folder-icon"; -import ChrisAPIClient from "../../api/chrisapiclient"; import ExternalLinkSquareIcon from "@patternfly/react-icons/dist/esm/icons/external-link-square-alt-icon"; import useLongPress, { elipses } from "./utils"; +import { fetchAuthenticatedFeed, fetchPublicFeed } from "../Feeds/utilties"; function FolderCard({ folder, @@ -31,18 +31,30 @@ function FolderCard({ const isRoot = folder.startsWith("feed"); - const fetchFeedDetails = async (id: number) => { + const fetchFeedDetails = async (id: string) => { if (!id) return; - const client = ChrisAPIClient.getClient(); - const feed = await client.getFeed(id); - return feed; + + const publicFeed = await fetchPublicFeed(id); + + if (publicFeed) { + return { + feed: publicFeed, + type: "public", + }; + } + const authenticatedFeed = await fetchAuthenticatedFeed(id); + + return { + feed: authenticatedFeed, + type: "private", + }; }; const id = folder.split("_")[1]; - const { data: feed } = useQuery({ + const { data } = useQuery({ queryKey: ["feed", id], - queryFn: () => fetchFeedDetails(+id), + queryFn: () => fetchFeedDetails(id), enabled: !!folder.startsWith("feed"), }); @@ -58,15 +70,14 @@ function FolderCard({ > <CardHeader actions={{ - actions: - feed && feed.data.id ? ( - <span> - <Link to={`/feeds/${feed.data.id}`}> - {" "} - <ExternalLinkSquareIcon /> - </Link> - </span> - ) : null, + actions: data?.feed?.data.id ? ( + <span> + <Link to={`/feeds/${data?.feed?.data.id}?type=${data?.type}`}> + {" "} + <ExternalLinkSquareIcon /> + </Link> + </span> + ) : null, }} > <Split style={{ overflow: "hidden" }}> @@ -83,10 +94,10 @@ function FolderCard({ } }} > - {feed ? elipses(feed.data.name, 40) : elipses(folder, 40)} + {data ? elipses(data?.feed?.data.name, 40) : elipses(folder, 40)} </Button> <div> - {feed && new Date(feed.data.creation_date).toDateString()} + {data && new Date(data?.feed?.data.creation_date).toDateString()} </div> </SplitItem> </Split> 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<SignUpFormProps> = ({ 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<SignUpFormProps> = ({ 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<SignUpFormProps> = ({ } } } - - 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<SignUpFormProps> = ({ username: value, }) } - ></TextInput> + /> <HelperText> <HelperTextItem variant="error"> {userState.invalidText} @@ -302,8 +307,11 @@ const SignUpForm: React.FC<SignUpFormProps> = ({ }; 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/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<IFeedState> = ( 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<IUserState> = ( 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({ From b5154d122acc0d122c480648d4136ea68b945fa8 Mon Sep 17 00:00:00 2001 From: PintoGideon <gideonpinto123@gmail.com> Date: Thu, 15 Feb 2024 15:04:16 -0500 Subject: [PATCH 2/3] Adding a loading spinner while loading the tree --- src/components/FeedTree/ParentComponent.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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 ? ( - <div>Loading...</div> + <SpinContainer title="Loading the tree" /> ) : error ? ( - <div className="feed-tree"> - <div>There was an error</div> - </div> + <Alert type="error" description={error} /> ) : null; }; From 1662f76d76ad6a9bf9faaaacac94a9eef78a235e Mon Sep 17 00:00:00 2001 From: PintoGideon <gideonpinto123@gmail.com> Date: Thu, 15 Feb 2024 15:17:19 -0500 Subject: [PATCH 3/3] Providing a better experience of the context switching between public and private feeds when a user sign's in and out --- src/components/Feeds/FeedListView.tsx | 5 ++--- src/components/Feeds/FeedView.tsx | 4 ++-- src/components/Wrapper/Sidebar.tsx | 2 +- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/components/Feeds/FeedListView.tsx b/src/components/Feeds/FeedListView.tsx index a2d2880f3..0c7f90b62 100644 --- a/src/components/Feeds/FeedListView.tsx +++ b/src/components/Feeds/FeedListView.tsx @@ -139,10 +139,9 @@ const TableSelectable: React.FunctionComponent = () => { }, [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]); diff --git a/src/components/Feeds/FeedView.tsx b/src/components/Feeds/FeedView.tsx index 726285288..26d5c096e 100644 --- a/src/components/Feeds/FeedView.tsx +++ b/src/components/Feeds/FeedView.tsx @@ -75,10 +75,10 @@ export default function FeedView() { }); React.useEffect(() => { - if (!type) { + if (!type || (type === "private" && !isLoggedIn)) { navigate("/feeds?type=public"); } - }, [type, navigate]); + }, [type, navigate, isLoggedIn]); React.useEffect(() => { const feed: Feed | undefined = privateFeed || publicFeed; 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<AllProps> = ({ itemId="analyses" isActive={sidebarActiveItem === "analyses"} > - <Link to="/feeds">New and Existing Analyses</Link> + <Link to="/feeds?type=public">New and Existing Analyses</Link> </NavItem> <NavItem itemId="catalog" isActive={sidebarActiveItem === "catalog"}> <Link to="/catalog">Plugins</Link>