Skip to content

Commit

Permalink
Merge pull request #206 from kbase/URO-206
Browse files Browse the repository at this point in the history
URO-206 getting started with orcidlink integration
  • Loading branch information
eapearson authored May 2, 2024
2 parents c903c20 + bcbf09a commit 38d1743
Show file tree
Hide file tree
Showing 13 changed files with 431 additions and 22 deletions.
6 changes: 6 additions & 0 deletions src/app/Routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
useFilteredParams,
usePageTracking,
} from '../common/hooks';
import ORCIDLinkFeature from '../features/orcidlink';

export const LOGIN_ROUTE = '/legacy/login';
export const ROOT_REDIRECT_ROUTE = '/narratives';
Expand Down Expand Up @@ -77,6 +78,11 @@ const Routes: FC = () => {
<Route path="*" element={<PageNotFound />} />
</Route>

{/* orcidlink */}
<Route path="/orcidlink">
<Route index element={<Authed element={<ORCIDLinkFeature />} />} />
</Route>

{/* IFrame Fallback Routes */}
<Route path="/fallback">
<Route
Expand Down
2 changes: 2 additions & 0 deletions src/app/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import collections from '../features/collections/collectionsSlice';
import icons from '../features/icons/iconSlice';
import layout from '../features/layout/layoutSlice';
import navigator from '../features/navigator/navigatorSlice';
import orcidlink from '../features/orcidlink/orcidlinkSlice';
import params from '../features/params/paramsSlice';
import profile from '../features/profile/profileSlice';

Expand All @@ -16,6 +17,7 @@ const everyReducer = combineReducers({
navigator,
params,
profile,
orcidlink,
[baseApi.reducerPath]: baseApi.reducer,
});

Expand Down
93 changes: 93 additions & 0 deletions src/common/api/orcidlinkAPI.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { baseApi } from '.';
import { jsonRpcService } from './utils/serviceHelpers';

// orcidlink system types

export interface ORCIDAuthPublic {
expires_in: number;
name: string;
orcid: string;
scope: string;
}

export interface LinkRecordPublic {
created_at: number;
expires_at: number;
retires_at: number;
username: string;
orcid_auth: ORCIDAuthPublic;
}

// Method types

export interface StatusResult {
status: string;
current_time: number;
start_time: number;
}

export interface InfoResult {
'service-description': {
name: string;
title: string;
version: string;
};
}

// is-linked

export interface IsLinkedParams {
username: string;
}

export type IsLinkedResult = boolean;

// owner-link
export interface OwnerLinkParams {
username: string;
}

export type OwnerLinkResult = LinkRecordPublic;

// It is mostly a JSONRPC 2.0 service, although the oauth flow is rest-ish.
const orcidlinkService = jsonRpcService({
url: '/services/orcidlink/api/v1',
version: '2.0',
});

/**
* orcidlink service api
*/
export const orcidlinkAPI = baseApi
.enhanceEndpoints({ addTagTypes: ['ORCIDLink'] })
.injectEndpoints({
endpoints: ({ query }) => ({
orcidlinkStatus: query<StatusResult, {}>({
query: () => {
return orcidlinkService({
method: 'status',
});
},
}),
orcidlinkIsLinked: query<IsLinkedResult, IsLinkedParams>({
query: ({ username }) => {
return orcidlinkService({
method: 'is-linked',
params: {
username,
},
});
},
}),
orcidlinkOwnerLink: query<OwnerLinkResult, OwnerLinkParams>({
query: ({ username }) => {
return orcidlinkService({
method: 'owner-link',
params: {
username,
},
});
},
}),
}),
});
49 changes: 41 additions & 8 deletions src/common/api/utils/kbaseBaseQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
} from '@reduxjs/toolkit/query/react';
import { RootState } from '../../../app/store';
import { serviceWizardApi } from '../serviceWizardApi';
import { KBaseBaseQueryError, isJsonRpcError } from './common';
import { isJsonRpcError, KBaseBaseQueryError } from './common';

export interface DynamicService {
name: string;
Expand All @@ -16,16 +16,33 @@ export interface DynamicService {

export interface StaticService {
url: string;
version?: '1.1' | '2.0';
}

export interface JsonRpcQueryArgs {
apiType: 'JsonRpc';
service: StaticService | DynamicService;
method: string;
params: unknown;
params?: unknown;
fetchArgs?: FetchArgs;
}

export interface JSONRPC11Body {
version: string;
id: string;
method: string;
params?: unknown;
}

export interface JSONRPC20Body {
jsonrpc: string;
id: string;
method: string;
params?: unknown;
}

export type JSONRPCBody = JSONRPC11Body | JSONRPC20Body;

export interface HttpQueryArgs extends FetchArgs {
apiType: 'Http';
service: StaticService;
Expand Down Expand Up @@ -172,19 +189,35 @@ export const kbaseBaseQuery: (
// Generate JsonRpc request id
const reqId = Math.random().toString();

const body = ((): JSONRPCBody => {
switch (kbQueryArgs.service.version || '1.1') {
case '1.1':
return {
version: '1.1',
id: reqId,
method: kbQueryArgs.method,
};
case '2.0':
return {
jsonrpc: '2.0',
id: reqId,
method: kbQueryArgs.method,
};
}
})();

if (kbQueryArgs.params) {
body.params = kbQueryArgs.params;
}

// generate request body
const fetchArgs = {
url: new URL(
kbQueryArgs.service.url,
fetchBaseQueryArgs.baseUrl
).toString(),
method: 'POST',
body: {
version: '1.1', // TODO: conditionally implement JsonRpc 2.0
id: reqId,
method: kbQueryArgs.method,
params: kbQueryArgs.params,
},
body,
...kbQueryArgs.fetchArgs, // Allow overriding JsonRpc defaults
};

Expand Down
14 changes: 7 additions & 7 deletions src/features/layout/LeftNavBar.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
/* LeftNavBar */
import { FontAwesomeIcon as FAIcon } from '@fortawesome/react-fontawesome';
import {
faCompass,
faUsers,
faBook,
faBullhorn,
faCompass,
faIdCard,
faLayerGroup,
faSearch,
faSuitcase,
faIdCard,
faBullhorn,
faUsers,
IconDefinition,
faLayerGroup,
} from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon as FAIcon } from '@fortawesome/react-fontawesome';
import { FC } from 'react';
import { Link, useLocation } from 'react-router-dom';
import { getFeedsUnseenCount } from '../../common/api/feedsService';
import { useAppSelector } from '../../common/hooks';
import { authMe, authToken } from '../auth/authSlice';
import { useAuthMe } from '../auth/hooks';
import classes from './LeftNavBar.module.scss';
import { getFeedsUnseenCount } from '../../common/api/feedsService';

const LeftNavBar: FC = () => {
const token = useAppSelector(authToken);
Expand Down
14 changes: 7 additions & 7 deletions src/features/layout/TopBar.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { FontAwesomeIcon as FAIcon } from '@fortawesome/react-fontawesome';
import {
faBars,
faEnvelope,
Expand All @@ -17,20 +16,21 @@ import {
faUser,
faWrench,
} from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon as FAIcon } from '@fortawesome/react-fontawesome';
import { FC, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';

import { toast } from 'react-hot-toast';
import { Link } from 'react-router-dom';
import { resetStateAction } from '../../app/store';
import { revokeToken } from '../../common/api/authService';
import { getUserProfile } from '../../common/api/userProfileApi';
import logo from '../../common/assets/logo/46_square.png';
import { Dropdown } from '../../common/components';
import { useAppDispatch, useAppSelector } from '../../common/hooks';
import { authUsername, setAuth } from '../auth/authSlice';
import classes from './TopBar.module.scss';
import { Link } from 'react-router-dom';
import { getUserProfile } from '../../common/api/userProfileApi';
import { revokeToken } from '../../common/api/authService';
import { toast } from 'react-hot-toast';
import { noOp } from '../common';
import { resetStateAction } from '../../app/store';
import classes from './TopBar.module.scss';

export default function TopBar() {
const username = useAppSelector(authUsername);
Expand Down
36 changes: 36 additions & 0 deletions src/features/orcidlink/ErrorMessage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import Alert from '@mui/material/Alert';
import { SerializedError } from '@reduxjs/toolkit';
import { KBaseBaseQueryError } from '../../common/api/utils/common';
import styles from './orcidlink.module.scss';

export interface ErrorMessageProps {
error: KBaseBaseQueryError | SerializedError;
}

export default function ErrorMessage({ error }: ErrorMessageProps) {
const message = (() => {
if ('status' in error) {
switch (error.status) {
case 'JSONRPC_ERROR':
return error.data.error.message;
case 'FETCH_ERROR':
return 'Fetch Error';
case 'CUSTOM_ERROR':
return error.error;
case 'PARSING_ERROR':
return error.error;
case 'TIMEOUT_ERROR':
return error.error;
}
} else {
return error.message || 'Unknown Error';
}
})();
return (
<div className={styles['error-message']}>
<Alert severity="error" title="Error">
{message}
</Alert>
</div>
);
}
73 changes: 73 additions & 0 deletions src/features/orcidlink/Home.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { Alert, AlertTitle, CircularProgress } from '@mui/material';
import { SerializedError } from '@reduxjs/toolkit';
import { orcidlinkAPI } from '../../common/api/orcidlinkAPI';
import { KBaseBaseQueryError } from '../../common/api/utils/common';
import { useAppSelector } from '../../common/hooks';
import { authUsername } from '../auth/authSlice';
import { usePageTitle } from '../layout/layoutSlice';
import ErrorMessage from './ErrorMessage';
import Linked from './Linked';
import styles from './orcidlink.module.scss';
import Unlinked from './Unlinked';

export default function Home() {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const username = useAppSelector(authUsername)!;

function renderLoading(title: string, description: string) {
return (
<div className={styles.loading}>
<Alert icon={<CircularProgress size="1rem" />}>
<AlertTitle>
<span className={styles['loading-title']}>{title}</span>
</AlertTitle>
<p>{description}</p>
</Alert>
</div>
);
}

function renderError(error: KBaseBaseQueryError | SerializedError) {
return <ErrorMessage error={error} />;
}

usePageTitle('KBase ORCID Link');

const {
data: isLInked,
error,
isLoading,
isError,
isFetching,
isSuccess,
isUninitialized,
} = orcidlinkAPI.useOrcidlinkIsLinkedQuery({ username });

if (isUninitialized) {
return renderLoading('Uninitialized...', 'Loading the ORCID Link App...');
} else if (isLoading) {
return renderLoading('Loading...', 'Loading the ORCID Link App...');
} else if (isFetching) {
return renderLoading('Fetching...', 'Loading the ORCID Link App...');
} else if (isError) {
return renderError(error);
} else if (isSuccess) {
if (isLInked) {
return (
<div className={styles.box}>
<Linked />
</div>
);
}
return (
<div className={styles.box}>
<Unlinked />
</div>
);
} else {
return renderError({
status: 'CUSTOM_ERROR',
error: 'Unknown State',
});
}
}
Loading

0 comments on commit 38d1743

Please sign in to comment.