Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

URO-208: orcidlink main view #210

Merged
merged 21 commits into from
May 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
0411e83
add tests and light refactoring in response to tests [URO-208]
eapearson May 3, 2024
7a2fdd8
add icons-material dependency for accordion icons [URO-208]
eapearson May 10, 2024
fd3fc8a
add orcid icon [URO-208]
eapearson May 10, 2024
5f98eff
udpate orcidlink api with multi-call endpoints [URO-208]
eapearson May 10, 2024
fe890cd
improve JSON-RPC 2.0 support [URO-208]
eapearson May 10, 2024
13cf808
add support and tests for view for linked and unlinked user [URO-208]
eapearson May 10, 2024
cf8d665
Merge remote-tracking branch 'origin/main' into URO-208
eapearson May 10, 2024
153bd48
remove material icons, use fa chevron icon instead [URO-208]
eapearson May 16, 2024
bc1a3a3
remove upstream code reference (redux-toolkit) comment [URO-208]
eapearson May 16, 2024
77c60a8
remove uiURL [URO-208]
eapearson May 16, 2024
559e0da
remove card content padding top override [URO-208]
eapearson May 16, 2024
97a6a14
remove types - one unused, one replaced with the aliased type [URO-208]
eapearson May 16, 2024
6f18524
replace non-null assertion with thrown exception [URO-208]
eapearson May 17, 2024
bd504a6
remove comments [URO-208]
eapearson May 17, 2024
56a1dc4
updated package-lock [URO-208]
eapearson May 17, 2024
95421b6
remove commented out code [URO-208]
eapearson May 17, 2024
2b511ae
remove responseHandler property [URO-208]
eapearson May 17, 2024
d108f83
remove jsonRpc2Service function [URO-208]
eapearson May 18, 2024
5a5d41f
return react fragment rather than null [URO-208]
eapearson May 18, 2024
decebf5
move orcid link render function to misc components module [URO-208]
eapearson May 18, 2024
04defd8
break render functions into standalone components [URO-208]
eapearson May 20, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 17 additions & 0 deletions public/assets/images/ORCID-iD_icon-vector.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
73 changes: 73 additions & 0 deletions src/common/api/orcidLinkCommon.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
export interface ORCIDAuthPublic {
dauglyon marked this conversation as resolved.
Show resolved Hide resolved
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;
}

export interface ORCIDAuthPublicNonOwner {
orcid: string;
name: string;
}

export interface LinkRecordPublicNonOwner {
username: string;
orcid_auth: ORCIDAuthPublicNonOwner;
}

// ORCID User Profile (our version)

export interface Affiliation {
name: string;
role: string;
startYear: string;
endYear: string | null;
}

export interface ORCIDFieldGroupBase {
private: boolean;
}

export interface ORCIDFieldGroupPrivate extends ORCIDFieldGroupBase {
private: true;
fields: null;
}

export interface ORCIDFieldGroupAccessible<T> extends ORCIDFieldGroupBase {
private: false;
fields: T;
}

export type ORCIDFieldGroup<T> =
| ORCIDFieldGroupPrivate
| ORCIDFieldGroupAccessible<T>;

export interface ORCIDNameFieldGroup {
firstName: string;
lastName: string | null;
creditName: string | null;
}

export interface ORCIDBiographyFieldGroup {
bio: string;
}

export interface ORCIDEmailFieldGroup {
emailAddresses: Array<string>;
}

export interface ORCIDProfile {
orcidId: string;
nameGroup: ORCIDFieldGroup<ORCIDNameFieldGroup>;
biographyGroup: ORCIDFieldGroup<ORCIDBiographyFieldGroup>;
emailGroup: ORCIDFieldGroup<ORCIDEmailFieldGroup>;
employment: Array<Affiliation>;
}
148 changes: 102 additions & 46 deletions src/common/api/orcidlinkAPI.ts
Original file line number Diff line number Diff line change
@@ -1,53 +1,64 @@
import { baseApi } from '.';
import { LinkRecordPublic, ORCIDProfile } from './orcidLinkCommon';
import { jsonRpcService } from './utils/serviceHelpers';

// orcidlink system types
// system info

export interface ORCIDAuthPublic {
expires_in: number;
export interface ServiceDescription {
name: string;
orcid: string;
scope: string;
title: string;
version: string;
language: string;
description: string;
repoURL: string;
}

export interface LinkRecordPublic {
created_at: number;
expires_at: number;
retires_at: number;
username: string;
orcid_auth: ORCIDAuthPublic;
export interface GitInfo {
commit_hash: string;
commit_hash_abbreviated: string;
author_name: string;
committer_name: string;
committer_date: number;
url: string;
branch: string;
tag: string | null;
}

// Method types

export interface StatusResult {
status: string;
export interface RuntimeInfo {
current_time: number;
start_time: number;
orcid_api_url: string;
orcid_oauth_url: string;
orcid_site_url: string;
}

// TODO: normalize to either kebab or underscore. Pref underscore.
dauglyon marked this conversation as resolved.
Show resolved Hide resolved
export interface InfoResult {
'service-description': {
name: string;
title: string;
version: string;
};
'service-description': ServiceDescription;
'git-info': GitInfo;
runtime_info: RuntimeInfo;
}

// is-linked
// combined api calls for initial view

export interface ORCIDLinkInitialStateResult {
isLinked: boolean;
info: InfoResult;
}

export interface IsLinkedParams {
export interface ORCIDLinkInitialStateParams {
username: string;
}

export type IsLinkedResult = boolean;
// combined api call for linked user info

// owner-link
export interface OwnerLinkParams {
username: string;
export interface ORCIDLinkLinkedUserInfoResult {
linkRecord: LinkRecordPublic;
profile: ORCIDProfile;
}

export type OwnerLinkResult = LinkRecordPublic;
export interface ORCIDLinkLinkedUserInfoParams {
username: string;
}

// It is mostly a JSONRPC 2.0 service, although the oauth flow is rest-ish.
const orcidlinkService = jsonRpcService({
Expand All @@ -62,31 +73,76 @@ 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,
orcidlinkInitialState: query<
dauglyon marked this conversation as resolved.
Show resolved Hide resolved
ORCIDLinkInitialStateResult,
ORCIDLinkInitialStateParams
>({
async queryFn({ username }, _queryApi, _extraOptions, fetchWithBQ) {
const [isLinked, info] = await Promise.all([
fetchWithBQ(
orcidlinkService({
method: 'is-linked',
params: {
username,
},
})
),
fetchWithBQ(
orcidlinkService({
method: 'info',
})
),
]);
if (isLinked.error) {
return { error: isLinked.error };
}
if (info.error) {
return { error: info.error };
}
return {
data: {
isLinked: isLinked.data as boolean,
info: info.data as InfoResult,
},
});
};
},
}),
orcidlinkOwnerLink: query<OwnerLinkResult, OwnerLinkParams>({
query: ({ username }) => {
return orcidlinkService({
method: 'owner-link',
orcidlinkLinkedUserInfo: query<
ORCIDLinkLinkedUserInfoResult,
ORCIDLinkLinkedUserInfoParams
>({
async queryFn({ username }, _queryApi, _extraOptions, fetchWithBQ) {
const profileQuery = orcidlinkService({
method: 'get-orcid-profile',
params: {
username,
},
});

const [linkRecord, profile] = await Promise.all([
fetchWithBQ(
orcidlinkService({
method: 'owner-link',
params: {
username,
},
})
),
fetchWithBQ(profileQuery),
]);
if (linkRecord.error) {
return { error: linkRecord.error };
}

if (profile.error) {
return { error: profile.error };
}
return {
data: {
linkRecord: linkRecord.data as LinkRecordPublic,
profile: profile.data as ORCIDProfile,
},
};
},
}),
}),
Expand Down
21 changes: 21 additions & 0 deletions src/common/api/utils/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,27 @@ export const isJsonRpcError = (obj: unknown): obj is JsonRpcError => {
return false;
};

export const isJsonRpc20Error = (obj: unknown): obj is JsonRpcError => {
if (
typeof obj === 'object' &&
obj !== null &&
['jsonrpc', 'error', 'id'].every((k) => k in obj)
) {
const { jsonrpc, error } = obj as { jsonrpc: string; error: unknown };
if (jsonrpc !== '2.0') {
return false;
}
if (
typeof error === 'object' &&
error !== null &&
['code', 'message'].every((k) => k in error)
) {
return true;
}
}
return false;
};

/**
* Type predicate to narrow an unknown error to `FetchBaseQueryError`
*/
Expand Down
30 changes: 29 additions & 1 deletion src/common/api/utils/kbaseBaseQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ import {
} from '@reduxjs/toolkit/query/react';
import { RootState } from '../../../app/store';
import { serviceWizardApi } from '../serviceWizardApi';
import { isJsonRpcError, KBaseBaseQueryError } from './common';
import {
isJsonRpc20Error,
isJsonRpcError,
KBaseBaseQueryError,
} from './common';

export interface DynamicService {
name: string;
Expand Down Expand Up @@ -41,6 +45,12 @@ export interface JSONRPC20Body {
params?: unknown;
}

export interface JSONRPC20Error {
code: number;
message: string;
data: unknown;
}

export type JSONRPCBody = JSONRPC11Body | JSONRPC20Body;

export interface HttpQueryArgs extends FetchArgs {
Expand Down Expand Up @@ -226,6 +236,7 @@ export const kbaseBaseQuery: (
const response = await request;

// identify and better differentiate jsonRpc errors
// This is for KBase's version of JSON-RPC 1.1
if (response.error && response.error.status === 500) {
if (isJsonRpcError(response.error.data)) {
if (response.error.data.id && response.error.data.id !== reqId) {
Expand All @@ -245,6 +256,23 @@ export const kbaseBaseQuery: (
};
}
}
if (isJsonRpc20Error(response.data)) {
if (response.data.id && response.data.id !== reqId) {
return {
error: {
status: 'CUSTOM_ERROR',
error: 'JsonRpcProtocolError',
data: `Response ID "${response.data.id}" !== Request ID "${reqId}"`,
},
};
}
return {
error: {
status: 'JSONRPC_ERROR',
data: response.data,
},
};
}

// If another error has occurred preventing a response, return default rtk-query response.
// This appropriately handles rtk-query internal errors
Expand Down
2 changes: 1 addition & 1 deletion src/common/api/utils/serviceHelpers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { JsonRpcQueryArgs, HttpQueryArgs } from './kbaseBaseQuery';
import { HttpQueryArgs, JsonRpcQueryArgs } from './kbaseBaseQuery';

// Helpers for adding service info to each query,
export const jsonRpcService = (
Expand Down
Loading
Loading