frontend/app/projectsearch/ProjectLockerSearchBar.tsx (231 lines of code) (raw):
import React, { useEffect, useState } from "react";
import { RouteChildrenProps } from "react-router";
import { authenticatedFetch } from "../auth";
import { Grid, makeStyles, Typography } from "@material-ui/core";
import FilterableList from "../common/FilterableList";
interface ProjectLockerSearchBarProps {
projectLockerBaseUrl: string;
projectSelectionChanged: (newProject: string | undefined) => void;
size: number;
className?: string;
}
/**
* wrapper component that contains an error boundary
*/
class ProjectLockerSearchBar extends React.Component<
ProjectLockerSearchBarProps,
any
> {
constructor(props: ProjectLockerSearchBarProps) {
super(props);
this.state = {
lastError: undefined,
};
}
static getDerivedStateFromError(error: Error) {
return {
lastError: error.toString(),
};
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.error("ProjectLockerSearchBar failed: ", error);
console.log(errorInfo);
}
render() {
return this.state.lastError ? (
<Typography>{this.state.lastError}</Typography>
) : (
<ProjectLockerSearchBarImplementation {...this.props} />
);
}
}
const useStyles = makeStyles({});
const ProjectLockerSearchBarImplementation: React.FC<ProjectLockerSearchBarProps> = (
props
) => {
const [currentWorkingGroupId, setCurrentWorkingGroupId] = useState<
string | undefined
>(undefined);
const [currentCommissionVsid, setCurrentCommissionVsid] = useState<
string | undefined
>(undefined);
const [currentProjectVsid, setCurrentProjectVsid] = useState<
string | undefined
>(undefined);
const [lastError, setLastError] = useState<string | undefined>(undefined);
const [projectLockerLoggedIn, setProjectLockerLoggedIn] = useState(false);
const [projectLockerUsername, setProjectLockerUsername] = useState("");
const [knownWorkingGroups, setKnownWorkingGroups] = useState<NameValuePair[]>(
[]
);
const [commSearchCounter, setCommSearchCounter] = useState<number>(0);
const [projSearchCounter, setProjSearchCounter] = useState<number>(0);
const workingGroupFetchUrl = "/api/pluto/workinggroup";
const commissionFetchUrl = "/api/pluto/commission/list";
const projectFetchUrl = "/api/project/list";
const classes = useStyles();
async function checkPLLogin() {
if (props.projectLockerBaseUrl === "") {
console.error("No project locker base URL set in the configuration");
return;
}
try {
const response = await authenticatedFetch(
props.projectLockerBaseUrl + "/api/isLoggedIn",
{ credentials: "include" }
);
const bodyContent = await response.json();
if (response.ok) {
setProjectLockerUsername(bodyContent.uid);
setProjectLockerLoggedIn(true);
} else if (response.status === 403) {
setProjectLockerLoggedIn(false);
setLastError("Could not log in");
} else {
setLastError(JSON.stringify(bodyContent));
setProjectLockerLoggedIn(false);
}
} catch (err) {
setLastError("Could not contact pluto-core, please see browser console");
return new Promise((resolve, reject) => reject(err));
}
}
/**
* callback function for FilterableList that takes content from a PlutoWorkingGroupResponse and converts it into
* name/value pairs for the list
* @param incomingData the PlutoWorkingGroupResponse
*/
const workingGroupContentConverter: ValueConverterFunc<PlutoWorkingGroupResponse> = (
incomingData
) => {
console.log("workingGroupContentConverter: ", incomingData);
return incomingData.result.map((entry) => {
return { name: entry.name, value: entry.id };
});
};
/**
* callback function for FilterableList that takes content from a PlutoCommissionResponse and converts it into
* name/value pairs for the list
* @param incomingData the PlutoCommissionResponse
*/
const commissionContentConverter: ValueConverterFunc<PlutoCommissionResponse> = (
incomingData
) => {
return incomingData.result.map((entry) => {
return {
name: entry.title,
value: entry.id.toString(),
};
});
};
/**
* callback function for FilterableList that takes content from a PlutoProjectResponse and converts it into
* name/value pairs for the list
* @param incomingData the PlutoProjectResponse
*/
const projectContentConverter: ValueConverterFunc<PlutoProjectResponse> = (
incomingData
) => {
return incomingData.result.map((entry) => {
return { name: entry.title, value: entry.id.toString() };
});
};
/**
* function that is called at startup to load in the working groups from pluto-core and store them in the component
* state
*/
async function initialWorkingGroupLoad() {
if (props.projectLockerBaseUrl === "") {
console.log(
"ProjectLockerSearchBar can't load initial working group because projectLockerBaseUrl is not set"
);
return;
}
try {
const response = await authenticatedFetch(
props.projectLockerBaseUrl + workingGroupFetchUrl,
{ credentials: "include" }
);
const bodyText = await response.text();
if (response.ok) {
const bodyContent = JSON.parse(bodyText);
setKnownWorkingGroups(
workingGroupContentConverter(bodyContent as PlutoWorkingGroupResponse)
);
} else {
setLastError(bodyText);
}
} catch (err: any) {
console.error("Could not load initial working group: ", err);
setLastError(err.toString);
}
}
/**
* once we have a base URL set, then verify that we can connect.
* checkPLLogin sets projectLockerLoggedIn to true/false and sets lastError
* if it can't connect
*/
useEffect(() => {
checkPLLogin();
}, [props.projectLockerBaseUrl]);
/**
* once we have a valid connection, load in the working groups
*/
useEffect(() => {
if (projectLockerLoggedIn) {
initialWorkingGroupLoad();
}
}, [projectLockerLoggedIn]);
/**
* update the commission search box if the working group id changes
*/
useEffect(() => {
setCommSearchCounter((prevValue) => prevValue + 1);
setCurrentCommissionVsid(undefined);
setProjSearchCounter((prevValue) => prevValue + 1);
}, [currentWorkingGroupId]);
/**
update the project search box if the commission id changes
*/
useEffect(() => {
setCurrentProjectVsid(undefined);
setProjSearchCounter((prevValue) => prevValue + 1);
}, [currentCommissionVsid]);
/**
* tell the parent if the project selection changes
*/
useEffect(() => {
props.projectSelectionChanged(currentProjectVsid);
}, [currentProjectVsid]);
/**
* this is a callback for FilterableList which generates a commission search request based on the contents
* of the filterable list search box (passed in as enteredText) and the currently selected working group id
* @param enteredText content of filterable list search box
*/
const makeCommissionSearch = (enteredText: string) => {
return currentWorkingGroupId
? {
title: enteredText,
match: "W_CONTAINS",
workingGroupId: parseInt(currentWorkingGroupId),
}
: null;
};
/**
* this is a callback for FilterableList which generates a project search request based on the contents of the
* filterable list seach box (passed in as enteredText) and the currently selected commission id
* @param enteredText content of the filterable list search box
*/
const makeProjectSearch = (enteredText: string) => {
return currentCommissionVsid
? {
title: enteredText,
match: "W_CONTAINS",
commissionId: parseInt(currentCommissionVsid),
}
: null;
};
/**
* render the search bar
*/
return lastError ? (
<Typography className={props.className}>{lastError}</Typography>
) : (
<Grid container spacing={3} className={props.className}>
<Grid item xs={4}>
<h3>Working Group</h3>
<FilterableList
value={currentWorkingGroupId}
size={props.size}
unfilteredContent={knownWorkingGroups}
onChange={(newWorkingGroup: string) => {
console.log("new working group set: ", newWorkingGroup);
setCurrentWorkingGroupId(newWorkingGroup);
}}
/>
</Grid>
<Grid item xs={4}>
<h3>Commission</h3>
<FilterableList
onChange={(newComm) => {
console.log("new commission id set: ", newComm);
setCurrentCommissionVsid(newComm);
}}
size={props.size}
value={currentCommissionVsid}
unfilteredContentFetchUrl={
props.projectLockerBaseUrl + commissionFetchUrl
}
makeSearchDoc={makeCommissionSearch}
unfilteredContentConverter={commissionContentConverter}
triggerRefresh={commSearchCounter}
allowCredentials={true}
/>
</Grid>
<Grid item xs={4}>
<h3>Project</h3>
<FilterableList
size={props.size}
value={currentProjectVsid}
unfilteredContentFetchUrl={
props.projectLockerBaseUrl + projectFetchUrl
}
makeSearchDoc={makeProjectSearch}
unfilteredContentConverter={projectContentConverter}
triggerRefresh={projSearchCounter}
allowCredentials={true}
onChange={(newProj) => {
console.log("new project id set: ", newProj);
setCurrentProjectVsid(newProj);
}}
/>
</Grid>
</Grid>
);
};
export default ProjectLockerSearchBar;