Skip to content

Commit

Permalink
Merge pull request #38 from briehl/master
Browse files Browse the repository at this point in the history
add link to org item
  • Loading branch information
briehl authored Jul 13, 2020
2 parents def8e68 + 4c512c5 commit 03d5d3c
Show file tree
Hide file tree
Showing 7 changed files with 363 additions and 12 deletions.
Original file line number Diff line number Diff line change
@@ -1,13 +1,271 @@
import React, { Component } from 'react';
import React, { Component, CSSProperties } from 'react';
import ControlMenuItemProps from './ControlMenuItemProps';
import { LoadingSpinner } from '../../../generic/LoadingSpinner';
import Select, { Styles } from 'react-select';
import DashboardButton from '../../../generic/DashboardButton';
import {
getLinkedOrgs,
GroupInfo,
lookupUserOrgs,
GroupIdentity,
linkNarrativeToOrg,
} from '../../../../utils/orgInfo';
import { getCurrentUserPermission } from '../../../../utils/narrativeData';
import Runtime from '../../../../utils/runtime';

interface State {}
/**
* Holds the state for the overall Link Organizations item popup.
*/
interface RequestResult {
error: boolean;
text: string | null;
requestSent: boolean;
}

interface State {
isLoading: boolean;
perm: string;
linkedOrgs: Array<GroupInfo>;
userOrgs: Array<GroupIdentity>;
request: RequestResult;
}

export default class LinkOrgItem extends Component<
ControlMenuItemProps,
State
> {
state = {
isLoading: true,
perm: 'n',
linkedOrgs: [],
userOrgs: [],
request: {
error: false,
text: null,
requestSent: false,
},
};

/**
* Once the componenent mounts, it should look up the user's permissions
* on the Narrative, the list of orgs that the user belongs to, and any orgs
* that the Narrative is already linked to.
*
* Next, it filters the user's orgs to remove those that overlap with the orgs
* that this Narrative is linked to - so they don't show up in the dropdown
* selector.
*/
async componentDidMount() {
this.updateState();
}

async updateState() {
const sharePerms = await getCurrentUserPermission(
this.props.narrative.access_group
);
const linkedOrgs = await getLinkedOrgs(this.props.narrative.access_group);
let userOrgs = await lookupUserOrgs();

// reduce the set of userOrgs down to those that are not already linked.
// Don't want to give the illusion of being able to link again.
const linkedOrgIds: Set<string> = new Set();
for (const org of linkedOrgs) {
linkedOrgIds.add(org.id);
}
userOrgs = userOrgs.filter(org => {
return !linkedOrgIds.has(org.id);
});

this.setState({
perm: sharePerms,
linkedOrgs,
isLoading: false,
userOrgs,
});
}

async linkOrg(orgId: string): Promise<void> {
try {
this.setState({ isLoading: true });
const request = await linkNarrativeToOrg(
this.props.narrative.access_group,
orgId
);
let result: RequestResult = {
error: false,
text: null,
requestSent: false,
};
if (request.error) {
result.error = true;
switch (request.error.appcode) {
case 40010:
result.text =
'A request has already been made to add this Narrative to the group.';
break;
default:
result.text = 'An error was made while processing your request.';
break;
}
result.requestSent = false;
} else {
result.error = false;
result.text = request.complete
? ''
: 'A request has been sent to the group admins.';
result.requestSent = true;
}
this.setState({
isLoading: false,
request: result,
});
return this.updateState();
} catch (error) {
console.log(error);
}
}

makeLinkedOrgsList() {
let linkedOrgsText = 'This Narrative is not linked to any organizations.';
let linkedOrgsList = null;
if (this.state.linkedOrgs.length > 0) {
linkedOrgsList = this.state.linkedOrgs.map((org: GroupInfo) => (
<LinkedOrg {...org} key={org.id} />
));
linkedOrgsText = 'Organizations this Narrative is linked to:';
}
return (
<div className="pt2">
<div style={{ textAlign: 'center' }}>{linkedOrgsText}</div>
<div className="pt2">{linkedOrgsList}</div>
</div>
);
}

render() {
return <div>Link to Organizations function still in development.</div>;
if (this.state.isLoading) {
return (
<div style={{ width: '35rem', textAlign: 'center' }}>
<LoadingSpinner loading={true} />
</div>
);
} else if (this.state.perm !== 'a') {
return (
<div style={{ textAlign: 'center' }}>
Only users with share access can request to add their narrative to a
group.
</div>
);
} else {
const linkedOrgs = this.makeLinkedOrgsList();
let message = null;
if (this.state.request.error || this.state.request.requestSent) {
message = (
<div
className={`pa3 mb2 ba br2 b--gold bg-light-yellow`}
style={{ textAlign: 'center' }}
>
{this.state.request.text}
</div>
);
}
return (
<div style={{ width: '35rem', minHeight: '10rem' }}>
{message}
<OrgSelect
linkOrg={this.linkOrg.bind(this)}
orgs={this.state.userOrgs}
/>
<div>{linkedOrgs}</div>
</div>
);
}
}
}

interface LinkedOrgProps extends GroupInfo {
key: string;
}

const LinkedOrg = (props: LinkedOrgProps) => {
console.log(props);
return (
<div className="pl2 pt2">
<a
className="blue pointer no-underline dim"
href={`${Runtime.getConfig().view_routes.orgs}/${props.id}`}
target="_blank"
>
<span className="fa fa-external-link pr1" />
{props.name}
</a>
</div>
);
};

interface OrgListProps {
linkOrg: (orgId: string) => void;
orgs: Array<GroupIdentity>;
}

interface OrgOption {
value: string;
label: string;
}

interface OrgListState {
selectedOrgId: string;
}

class OrgSelect extends Component<OrgListProps, OrgListState> {
private orgOptions: Array<OrgOption> = [];
constructor(props: OrgListProps) {
super(props);
for (const org of props.orgs) {
this.orgOptions.push({
value: org.id,
label: org.name,
});
}
this.state = {
selectedOrgId: '',
};
}

handleOrgChange = (selected: any) => {
this.setState({ selectedOrgId: selected?.value || '' });
};

render() {
const selectStyles: Partial<Styles> = {
menuPortal: base => ({ ...base, zIndex: 9999 }),
};

return (
<div className="flex flex-row flex-nowrap">
<Select
defaultOptions
isClearable
isSearchable
placeholder={'Organizations you belong to...'}
styles={{
...selectStyles,
container: base => ({ ...base, flex: 2 }),
}}
menuPortalTarget={document.body}
className="basic-single"
classNamePrefix="select"
options={this.orgOptions}
onChange={this.handleOrgChange}
/>
<DashboardButton
disabled={this.state.selectedOrgId.length === 0}
onClick={() => this.props.linkOrg(this.state.selectedOrgId)}
bgcolor={'lightblue'}
>
Link
</DashboardButton>
</div>
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ export default class SharingItem extends Component<
render() {
if (this.state.isLoading) {
return (
<div style={{ textAlign: 'center' }}>
<div style={{ width: '35rem', textAlign: 'center' }}>
<LoadingSpinner loading={true} />
</div>
);
Expand Down Expand Up @@ -220,27 +220,25 @@ interface GlobalProps {

function GlobalPerms(props: GlobalProps): React.ReactElement {
const globalStyle: CSSProperties = {
// backgroundColor: props.isGlobal ? 'lightgreen' : 'lightblue',
// color: props.isGlobal ? 'green' : 'blue',
textAlign: 'center',
};
const icon = props.isGlobal ? 'fa-unlock' : 'fa-lock';

let className = 'pa2 mb2 br2';
let className = 'pa2 mb2 br2 ba';
if (props.isAdmin) {
className += ' dim pointer';
}

let text = '';
if (props.isGlobal) {
text = 'Public';
className += ' bg-light-green dark-green';
className += ' bg-light-green dark-green b--green';
if (props.isAdmin) {
text += ' (click to lock)';
}
} else {
text = 'Private';
className += ' bg-lightest-blue dark-blue';
className += ' bg-lightest-blue dark-blue b--dark-blue';
if (props.isAdmin) {
text += ' (click to unlock)';
}
Expand Down Expand Up @@ -372,7 +370,6 @@ class PermSearch extends Component<PermSearchProps> {
render() {
const selectStyles: Partial<Styles> = {
menuPortal: base => ({ ...base, zIndex: 9999 }),
// container: base => ({ ...base, flex: 1 })
};
return (
<div className="flex flex-row flex-nowrap">
Expand Down
1 change: 1 addition & 0 deletions src/client/components/dashboard/NarrativeList/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ export class NarrativeList extends Component<Props, State> {
const searchParams = this.state.searchParams;
return searchNarratives(searchParams)
.then((resp: SearchResults) => {
console.log(resp);
if (resp && resp.hits) {
const total = resp.count;
const items = resp.hits.map(hit => hit.doc);
Expand Down
2 changes: 2 additions & 0 deletions src/client/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@ interface Urls {
narrative_method_store: string;
catalog: string;
service_wizard: string;
groups: string;
}

interface Routes {
narrative: string;
login: string;
orgs: string;
}

const LOADED_CONFIG: LoadedConfigFile = configFile;
Expand Down
22 changes: 22 additions & 0 deletions src/client/utils/narrativeData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,25 @@ export function fetchNarrative(upa: string) {
});
return client.call('get_objects2', [{'objects': [{'ref': upa}]}]);
}

/**
* Returns the current user's permissions for some narrative. This is either 'a', 'w', 'r', or 'n';
* @param wsId workspace id for a narrative of interest
*/
export async function getCurrentUserPermission(wsId: number): Promise<string> {
const client = new KBaseServiceClient({
module: 'Workspace',
url: Runtime.getConfig().service_routes.workspace,
authToken: Runtime.token()
});

let perms = await client.call('get_permissions_mass', [
{ workspaces: [{ id: wsId }] },
]);
perms = perms.perms[0];
const user = Runtime.username();
if (user) {
return perms[user];
}
return 'n';
}
Loading

0 comments on commit 03d5d3c

Please sign in to comment.