diff --git a/src/App.js b/src/App.js index 474dfe4f..1e6ea594 100644 --- a/src/App.js +++ b/src/App.js @@ -15,6 +15,7 @@ import Experiments from "./pages/Experiments"; import SliceViewer from "./pages/SliceViewer"; import NewSliceForm from "./pages/NewSliceForm"; import User from "./pages/User"; +import PublicUserProfile from "./components/UserProfile/PublicUserProfile.jsx"; import NotFound from "./pages/NotFound"; import Help from "./pages/Help"; import Header from "./components/Header"; @@ -89,6 +90,7 @@ class App extends React.Component { + diff --git a/src/components/Project/NewProjectForm.jsx b/src/components/Project/NewProjectForm.jsx index ef247b02..871ca89f 100644 --- a/src/components/Project/NewProjectForm.jsx +++ b/src/components/Project/NewProjectForm.jsx @@ -31,12 +31,9 @@ class NewProjectForm extends Form { uuid: "", description: "", facility: portalData.defaultFacility, - is_public: "Yes", + is_public: "Yes" }, - publicOptions: [ - { "_id": 1, "name": "Yes" }, - { "_id": 2, "name": "No" } - ], + publicOptions: ["Yes", "No"], errors: {}, owners: [], addedOwners: [], @@ -121,6 +118,13 @@ class NewProjectForm extends Form { } }; + raiseInputKeyDown = (e) => { + const query = e.target.value; + if ((e.key === "Enter") && query) { + this.handleSearch(query); + } + }; + handleInputChange = (input, type) => { if (type === "po") { this.setState({ ownerSearchInput: input }); @@ -176,7 +180,7 @@ class NewProjectForm extends Form { {this.renderInput("name", "Name", true)} {this.renderTextarea("description", "Description", true)} {this.renderSelect("facility", "Facility", true, portalData.defaultFacility, portalData.facilityOptions)} - {this.renderSelect("is_public", "Public", true, "", publicOptions)} + {this.renderSelect("is_public", "Public", true, "Yes", publicOptions, portalData.helperText.publicProjectDescription)} {this.renderButton("Create")}
@@ -211,8 +215,9 @@ class NewProjectForm extends Form { this.handleInputChange(e.currentTarget.value, "po")} + onKeyDown={this.raiseInputKeyDown} />

- {user.email} + { + user.email && {user.email} + } ); })} diff --git a/src/components/Project/ProjectPersonnel.jsx b/src/components/Project/ProjectPersonnel.jsx index d0f1fc2f..516c924b 100644 --- a/src/components/Project/ProjectPersonnel.jsx +++ b/src/components/Project/ProjectPersonnel.jsx @@ -39,6 +39,13 @@ class ProjectPersonnel extends Component { } }; + raiseInputKeyDown = (e) => { + const query = e.target.value; + if ((e.key === "Enter") && query) { + this.handleSearch(query); + } + }; + handleDeleteUser = (user) => { const { personnelType } = this.props; this.props.onSinglePersonnelUpdate(personnelType, user, "remove"); @@ -65,8 +72,9 @@ class ProjectPersonnel extends Component { this.handleInputChange(e.currentTarget.value)} + onKeyDown={this.raiseInputKeyDown} />
- +
@@ -60,30 +59,18 @@ class ProjectProfile extends Component { /> - - - - {basicInfoRows.map((row, index) => { return ( + project[row.path] && @@ -91,6 +78,48 @@ class ProjectProfile extends Component { })}
Project ID
- Project Permissions - - - - { project.tags.length > 0 ? this.renderTags(project.tags) : "No permissions assigned" } -
{row.label} { - row.path === "created" && toLocaleTime(_.get(project, row.path)) - } - { - row.path !== "created" && _.get(project, row.path) + row.label.includes("Time") ? + toLocaleTime(_.get(project, row.path)) : + _.get(project, row.path) }
+
+

Project Owners

+ { + !project.project_owners &&
+ The Project Owners information is set as private. +
+ } + { + project.project_owners && project.project_owners.length === 0 &&
+ This project doesn't have Project Owner. +
+ } + { + project.project_owners && project.project_owners.length > 0 && + + } + +
+

Project Members

+ { + !project.project_members &&
+ The Project Members information is set as private. +
+ } + { + project.project_members && project.project_members.length === 0 &&
+ This project doesn't have Project Member. +
+ } + { + project.project_members && project.project_members.length > 0 && +
+ } + ); } diff --git a/src/components/Project/ProjectUserTable.jsx b/src/components/Project/ProjectUserTable.jsx index 7966cdc5..ff7b79c8 100644 --- a/src/components/Project/ProjectUserTable.jsx +++ b/src/components/Project/ProjectUserTable.jsx @@ -3,12 +3,16 @@ import Table from "../common/Table"; import Pagination from "../common/Pagination"; import _ from "lodash"; import paginate from "../../utils/paginate"; +import { Link } from "react-router-dom"; class ProjectUserTable extends Component { columns = [ { path: "name", label: "Name", + content: (user) => ( + {user.name} + ) }, { path: "email", label: "Email" }, { path: "uuid", label: "ID" }, diff --git a/src/components/Project/ProjectsTable.jsx b/src/components/Project/ProjectsTable.jsx index 93b9f639..f9d9f2e7 100644 --- a/src/components/Project/ProjectsTable.jsx +++ b/src/components/Project/ProjectsTable.jsx @@ -4,18 +4,7 @@ import Table from "../common/Table"; import _ from "lodash"; class ProjectsTable extends Component { - hasAccessToProject = (project) => { - const membership = project.membership; - if (membership) { - return project.is_public || membership.is_creator - || membership.is_owner || membership.is_member; - } else { - return project.is_public; - } - } - - columns = { - "alwaysShowLinks": [ + columns = [ { path: "name", label: "Project Name", @@ -48,54 +37,15 @@ class ProjectsTable extends Component { ), - }, - ], - "onlyShowPublicLinks": [ - { - path: "name", - label: "Project Name", - content: (project) => ( - this.hasAccessToProject(project) ? {project.name} : {project.name} - ) - }, - { - path: "description", - label: "Description", - content: (project) => ( - - {_.truncate(project.description, { - 'length': 250, - 'separator': ' ' - })} - - ) - }, - { path: "facility", label: "Facility" }, - { - path: "created_time", - label: "Created Time", - }, - { - content: (project) => ( - - - - ), } ] - } render() { - const { projects, type, isFacilityOperator } = this.props; - const cols = (isFacilityOperator || type === "myProjects") ? this.columns["alwaysShowLinks"] : this.columns["onlyShowPublicLinks"] ; + const { projects } = this.props; + return (
diff --git a/src/components/SshKey/GenerateKey.jsx b/src/components/SshKey/GenerateKey.jsx index 1f2adf8f..c58da98c 100644 --- a/src/components/SshKey/GenerateKey.jsx +++ b/src/components/SshKey/GenerateKey.jsx @@ -47,18 +47,11 @@ class GenerateKey extends Form { getKeyTypeDropdown = (maxSliver, maxBastion) => { let dropdownItems = []; if (maxSliver) { - dropdownItems = [ - { "_id": 1, "name": "bastion" }, - ] + dropdownItems = ["bastion"] } else if (maxBastion) { - dropdownItems = [ - { "_id": 1, "name": "sliver" }, - ] + dropdownItems = ["sliver"] } else { - dropdownItems = [ - { "_id": 1, "name": "sliver" }, - { "_id": 2, "name": "bastion" } - ] + dropdownItems = ["sliver", "bastion"] } return dropdownItems; @@ -104,7 +97,7 @@ class GenerateKey extends Form { {this.renderInput("name", "Name", true, nameTooltip)} {this.renderTextarea("description", "Description", true, descriptionTooltip)} - {this.renderSelect("keyType", "Key Type", true, "", this.getKeyTypeDropdown(maxSliver, maxBastion))} + {this.renderSelect("keyType", "Key Type", true, this.getKeyTypeDropdown(maxSliver, maxBastion)[0], this.getKeyTypeDropdown(maxSliver, maxBastion))} {this.renderButton("Generate Key Pair")} } diff --git a/src/components/SshKey/UploadKey.jsx b/src/components/SshKey/UploadKey.jsx index 347e02ea..8ba9b77f 100644 --- a/src/components/SshKey/UploadKey.jsx +++ b/src/components/SshKey/UploadKey.jsx @@ -44,18 +44,11 @@ class UploadKey extends Form { getKeyTypeDropdown = (maxSliver, maxBastion) => { let dropdownItems = []; if (maxSliver) { - dropdownItems = [ - { "_id": 1, "name": "bastion" }, - ] + dropdownItems = ["bastion"] } else if (maxBastion) { - dropdownItems = [ - { "_id": 1, "name": "sliver" }, - ] + dropdownItems = ["sliver"] } else { - dropdownItems = [ - { "_id": 1, "name": "sliver" }, - { "_id": 2, "name": "bastion" } - ] + dropdownItems = ["sliver", "bastion"] } return dropdownItems; @@ -106,7 +99,7 @@ class UploadKey extends Form { {this.renderTextarea("publickey", "Public Key", true, publickeyTooltip)} {this.renderTextarea("description", "Description", true, descriptionTooltip)} - {this.renderSelect("keyType", "Key Type", true, "", this.getKeyTypeDropdown(maxSliver, maxBastion))} + {this.renderSelect("keyType", "Key Type", true, this.getKeyTypeDropdown(maxSliver, maxBastion)[0], this.getKeyTypeDropdown(maxSliver, maxBastion))} {this.renderButton("Upload Public Key")} diff --git a/src/components/UserProfile/AccountInfo.jsx b/src/components/UserProfile/AccountInfo.jsx index c2f152e0..cd19cf04 100644 --- a/src/components/UserProfile/AccountInfo.jsx +++ b/src/components/UserProfile/AccountInfo.jsx @@ -3,69 +3,32 @@ import React from "react"; class AccountInfo extends React.Component { state = { visibleRows: [ - { display: "Name", field: "cilogon_name" }, - { display: "Email", field: "email" }, + { display: "CILogon Name", field: "cilogon_name" }, + { display: "CILogon Email", field: "cilogon_email" }, { display: "Affiliation", field: "affiliation" }, - ], - toggledRows: [ { display: "FABRIC ID", field: "fabric_id" }, { display: "Bastion Login", field: "bastion_login" }, { display: "EPPN", field: "eppn" }, { display: "UUID", field: "uuid" }, - { display: "CILogon ID", field: "cilogon_id"}, - ], + { display: "CILogon ID", field: "cilogon_id"} + ] }; render() { const { user } = this.props; return ( -
-

Account Information

-
- - {this.state.visibleRows.map((row, index) => { - return ( - - - - - ); - })} - -
{row.display}{user[row.field]}
- -
- - - {this.state.toggledRows.map((row, index) => { - return ( - - - - - ); - })} - -
{row.display}{user[row.field] ? user[row.field] : "N/A"}
-
-
+ + + {this.state.visibleRows.map((row, index) => { + return ( + + + + + ); + })} + +
{row.display}{user[row.field]}
); } } diff --git a/src/components/UserProfile/MyProfile.jsx b/src/components/UserProfile/MyProfile.jsx index 6bd6cd72..3895978c 100644 --- a/src/components/UserProfile/MyProfile.jsx +++ b/src/components/UserProfile/MyProfile.jsx @@ -1,21 +1,66 @@ import React from "react"; import Joi from "joi-browser"; +import { default as portalData } from "../../services/portalData.json"; import Form from "../common/Form/Form"; import SpinnerWithText from "../common/SpinnerWithText"; -import { getCurrentUser, updatePeopleProfile } from "../../services/peopleService.js"; +import AccountInfo from "./AccountInfo"; +import { getCurrentUser, updatePeopleProfile, updatePeoplePreference } from "../../services/peopleService.js"; import { toast } from "react-toastify"; class MyProfile extends Form { state = { data: { + name: "", + email: "", bio: "", pronouns: "", job: "", - website: "" + website: "", + allOptions: [ + "show_email", + "show_roles", + "show_sshkeys", + "show_bio", + "show_pronouns", + "show_job", + "show_website" + ], + selectedOptions: [] + }, + allOptions: [ + "show_email", + "show_roles", + "show_sshkeys", + "show_bio", + "show_pronouns", + "show_job", + "show_website" + ], + optionsDisplayMapping: { + "show_email": "Email", + "show_roles": "Roles", + "show_sshkeys": "SSH Keys", + "show_bio": "Bio", + "show_pronouns": "Pronouns", + "show_job": "Job Title", + "show_website": "Website" + }, + user: { + email: "", + email_addresses: [] }, - user: {}, errors: {}, showSpinner: false, + staticInfoRows: [ + { display: "Name", field: "cilogon_name" }, + { display: "Email", field: "email" }, + { display: "Affiliation", field: "affiliation" }, + { display: "FABRIC ID", field: "fabric_id" }, + { display: "Bastion Login", field: "bastion_login" }, + { display: "EPPN", field: "eppn" }, + { display: "UUID", field: "uuid" }, + { display: "CILogon ID", field: "cilogon_id"}, + ], } async componentDidMount () { @@ -23,10 +68,26 @@ class MyProfile extends Form { const { data: res } = await getCurrentUser(); const user = res.results[0]; const profile = { + name: user.name, + email: user.email, bio: user.profile.bio, pronouns: user.profile.pronouns, job: user.profile.job, website: user.profile.website, + allOptions: [ + "show_email", + "show_roles", + "show_sshkeys", + "show_bio", + "show_pronouns", + "show_job", + "show_website" + ], + selectedOptions: Object.keys(user.profile.preferences).filter(key => + user.profile.preferences[key] && this.state.allOptions.includes(key)).concat( + Object.keys(user.preferences).filter(key => + user.preferences[key] && this.state.allOptions.includes(key)) + ) } this.setState({ data: profile, user }); } catch (err) { @@ -35,24 +96,67 @@ class MyProfile extends Form { } schema = { + name: Joi.string().required().label("Name"), + email: Joi.string().required().label("Preferred Email"), bio: Joi.string().allow("").label("Bio"), pronouns: Joi.string().allow("").label("Pronouns"), job: Joi.string().allow("").label("Job Title"), website: Joi.string().allow("").label("Website"), + allOptions: Joi.array(), + selectedOptions: Joi.array() }; + parsePreferences = () => { + // from array of ["show_bio", "show_website", ...] + // to object { "show_bio": true, "show_website": true } + // true for the existing items in array, others false. + const preferenceType1 = ["show_email", "show_eppn", "show_roles", "show_sshkeys"]; + const preferenceType2 = ["show_bio", "show_pronouns", "show_job", "show_website"]; + + const preferences1 = {}; + const preferences2 = {}; + + for (const option of preferenceType1) { + preferences1[option] = this.state.data.selectedOptions.includes(option); + } + + for (const option of preferenceType2) { + preferences2[option] = this.state.data.selectedOptions.includes(option); + } + + return [preferences1, preferences2]; + } + doSubmit = async () => { this.setState({ showSpinner: true }); const { data, user } = this.state; try { - await updatePeopleProfile(user.uuid, data); + const parsedPreferences = this.parsePreferences(); + await updatePeoplePreference(user.uuid, data, parsedPreferences[0]); + await updatePeopleProfile(user.uuid, data, parsedPreferences[1]); const { data: res } = await getCurrentUser(); const updatedUser = res.results[0]; const profile = { + name: updatedUser.name, + email: updatedUser.email, bio: updatedUser.profile.bio, pronouns: updatedUser.profile.pronouns, job: updatedUser.profile.job, website: updatedUser.profile.website, + allOptions: [ + "show_email", + "show_roles", + "show_sshkeys", + "show_bio", + "show_pronouns", + "show_job", + "show_website" + ], + selectedOptions: Object.keys(updatedUser.profile.preferences).filter(key => + updatedUser.profile.preferences[key] && this.state.allOptions.includes(key)).concat( + Object.keys(updatedUser.preferences).filter(key => + updatedUser.preferences[key] && this.state.allOptions.includes(key)) + ) } this.setState({ data: profile, user: updatedUser, showSpinner: false }); toast.success("You've successfully updated profile."); @@ -63,7 +167,7 @@ class MyProfile extends Form { }; render() { - const { showSpinner } = this.state; + const { showSpinner, user, optionsDisplayMapping } = this.state; return (
@@ -71,13 +175,26 @@ class MyProfile extends Form { { showSpinner ? :
+ {this.renderInput("name", "Name", true)} + { + this.renderSelect("email", "Preferred Email", true, + user.email, user.email_addresses, + portalData.helperText.preferredEmailDescription) + } {this.renderTextarea("bio", "Bio", true)} {this.renderInput("pronouns", "Pronouns", true)} {this.renderInput("job", "Job Title", true)} {this.renderInput("website", "Website", true)} + { + this.renderInputCheckBoxes("preferences", "Privacy Preferences", + true, optionsDisplayMapping, + portalData.helperText.privacyPreferencesDescription + ) + } {this.renderButton("Save")}
} +
); } diff --git a/src/components/UserProfile/ProjectRoles.jsx b/src/components/UserProfile/ProjectRoles.jsx index 24f8a8d3..78fdb685 100644 --- a/src/components/UserProfile/ProjectRoles.jsx +++ b/src/components/UserProfile/ProjectRoles.jsx @@ -88,6 +88,13 @@ class ProjectRoles extends React.Component { }); } + raiseInputKeyDown = (e) => { + const val = e.target.value; + if ((e.key === "Enter") && val) { + this.handleProjectsSearch(); + } + }; + render() { const { projects, projectsCount, pageSize, showSpinner, currentPage, searchQuery } = this.state; @@ -123,9 +130,10 @@ class ProjectRoles extends React.Component { type="text" name="query" className="form-control" - placeholder={"Search by Project Name (at least 3 letters)..."} + placeholder={"Search by Project Name (at least 3 letters) or Project UUID..."} value={searchQuery} onChange={this.handleInputChange} + onKeyDown={this.raiseInputKeyDown} />