Skip to content

Commit b94f6cf

Browse files
committed
Add more documentation
1 parent 2f0701b commit b94f6cf

9 files changed

+248
-51
lines changed

src/components/result/GenericResultView.tsx

Lines changed: 110 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -15,37 +15,98 @@ import { GenericResultViewTag, GenericResultViewTagProps } from "@/components/re
1515

1616
const HTTP_REGEX = /https?:\/\/[a-z]+\.[a-z]+.*/gm
1717

18-
export function GenericResultView({
19-
result,
20-
titleField,
21-
descriptionField,
22-
imageField,
23-
invertImageInDarkMode,
24-
digitalObjectLocationField,
25-
pidField,
26-
relatedItemPidsField,
27-
parentItemPidField,
28-
creationDateField,
29-
identifierField,
30-
relatedItemsPrefetch,
31-
tags,
32-
showOpenInFairDoScope
33-
}: {
18+
export interface GenericResultViewProps {
19+
/**
20+
* Search result that will be rendered in this view. Will be provided by FairDOElasticSearch
21+
*/
3422
result: SearchResult
23+
24+
/**
25+
* The elastic field where the title of the card will be read from
26+
*/
3527
titleField?: string
28+
29+
/**
30+
* The elastic field where the description of the card will be read from
31+
*/
3632
descriptionField?: string
33+
34+
/**
35+
* The elastic field where the image of the card will be read from. Will directly be passed to the `src` attribute of an `img` tag
36+
*/
3737
imageField?: string
38+
39+
/**
40+
* Enable this option to invert the image on dark mode. This is useful for greyscale schematics.
41+
*/
3842
invertImageInDarkMode?: boolean
43+
44+
/**
45+
* The elastic field where the digital object location (the target page of the `Open` button) will be read from
46+
*/
3947
digitalObjectLocationField?: string
48+
49+
/**
50+
* The elastic field where the PID of the current FDO will be read from. Can be omitted if you don't have a PID
51+
*/
4052
pidField?: string
53+
54+
/**
55+
* The elastic field where the related items of the current FDO will be read from. Should be an array of PIDs or otherwise unique identifiers. Will be displayed in a related items graph.
56+
*/
4157
relatedItemPidsField?: string
58+
59+
/**
60+
* Options for prefetching of related items in the relations graph. It is recommended to defined this if the default settings don't work properly.
61+
*/
4262
relatedItemsPrefetch?: { prefetchAmount?: number; searchFields?: Record<string, SearchFieldConfiguration> }
63+
64+
/**
65+
* The elastic field where the unique identifier of the parent item (metadata item) of the current FDO will be read from. Will be accessible via a `Find Metadata` button
66+
*/
4367
parentItemPidField?: string
68+
69+
/**
70+
* The elastic field where the creation date of the FDO will be read from. Will be parsed as an ISO String.
71+
*/
4472
creationDateField?: string
45-
identifierField?: string
73+
74+
/**
75+
* The elastic field where an additional identifier will be read from. You don't need to provide this if you don't have any additional identifiers apart from the PID.
76+
*/
77+
additionalIdentifierField?: string
78+
79+
/**
80+
* Define custom tags to display on the card. Each tag displays the information from one field.
81+
*/
4682
tags?: Omit<GenericResultViewTagProps, "result">[]
83+
84+
/**
85+
* Whether to show the open in FairDOScope button in the dropdown
86+
*/
4787
showOpenInFairDoScope?: boolean
48-
}) {
88+
}
89+
90+
/**
91+
* Configurable result view component that can be customized for specific use cases. Will display a card for each search result from elastic. If this component
92+
* doesn't fit your needs, feel free to implement your own result view.
93+
*/
94+
export function GenericResultView({
95+
result,
96+
titleField = "name",
97+
descriptionField = "description",
98+
imageField,
99+
invertImageInDarkMode = false,
100+
digitalObjectLocationField = "digitalObjectLocation",
101+
pidField = "pid",
102+
relatedItemPidsField = "isMetadataFor",
103+
parentItemPidField = "hasMetadata",
104+
creationDateField = "creationDate",
105+
additionalIdentifierField = "identifier",
106+
relatedItemsPrefetch = { prefetchAmount: 20, searchFields: { pid: {} } },
107+
tags = [],
108+
showOpenInFairDoScope = true
109+
}: GenericResultViewProps) {
49110
const { openRelationGraph } = useContext(RFS_GlobalModalContext)
50111
const { searchFor, searchTerm, elasticConnector } = useContext(FairDOSearchContext)
51112
const addToResultCache = useStore(resultCache, (s) => s.set)
@@ -66,13 +127,13 @@ export function GenericResultView({
66127
)
67128

68129
const pid = useMemo(() => {
69-
const _pid = getField("pid")
130+
const _pid = getField(pidField ?? "pid")
70131
if (_pid && _pid.startsWith("https://")) {
71132
return /https:\/\/.*?\/(.*)/.exec(_pid)?.[1] ?? _pid
72133
} else {
73134
return _pid
74135
}
75-
}, [getField])
136+
}, [getField, pidField])
76137

77138
const title = useMemo(() => {
78139
return getField(titleField ?? "name")
@@ -84,6 +145,7 @@ export function GenericResultView({
84145

85146
const doLocation = useMemo(() => {
86147
const value = getField(digitalObjectLocationField ?? "digitalObjectLocation")
148+
if (!value) return undefined
87149
if (HTTP_REGEX.test(value)) return value
88150
else return `https://doi.org/${value}`
89151
}, [digitalObjectLocationField, getField])
@@ -93,8 +155,8 @@ export function GenericResultView({
93155
}, [getField, imageField])
94156

95157
const identifier = useMemo(() => {
96-
return getField(identifierField ?? "identifier")
97-
}, [getField, identifierField])
158+
return getField(additionalIdentifierField ?? "identifier")
159+
}, [getField, additionalIdentifierField])
98160

99161
const isMetadataFor = useMemo(() => {
100162
return getArrayField(relatedItemPidsField ?? "isMetadataFor")
@@ -225,34 +287,36 @@ export function GenericResultView({
225287
</Button>
226288
)}
227289

228-
<div className="rfs-flex rfs-items-center">
229-
<a href={doLocation} target="_blank" className="grow">
230-
<Button size="sm" className="rfs-w-full rfs-rounded-r-none rfs-px-4">
231-
Open
232-
</Button>
233-
</a>
234-
<DropdownMenu>
235-
<DropdownMenuTrigger asChild>
236-
<Button size="sm" className="rfs-rounded-l-none rfs-border-l">
237-
<ChevronDown className="rfs-mr-1 rfs-size-4" />
290+
{doLocation && (
291+
<div className="rfs-flex rfs-items-center">
292+
<a href={doLocation} target="_blank" className="grow">
293+
<Button size="sm" className="rfs-w-full rfs-rounded-r-none rfs-px-4">
294+
Open
238295
</Button>
239-
</DropdownMenuTrigger>
240-
<DropdownMenuContent>
241-
<a href={doLocation} target="_blank">
242-
<DropdownMenuItem>
243-
<LinkIcon className="rfs-mr-1 rfs-size-4" /> Open Source
244-
</DropdownMenuItem>
245-
</a>
246-
{showOpenInFairDoScope && (
247-
<a href={`https://kit-data-manager.github.io/fairdoscope/?pid=${pid}`} target="_blank">
296+
</a>
297+
<DropdownMenu>
298+
<DropdownMenuTrigger asChild>
299+
<Button size="sm" className="rfs-rounded-l-none rfs-border-l">
300+
<ChevronDown className="rfs-mr-1 rfs-size-4" />
301+
</Button>
302+
</DropdownMenuTrigger>
303+
<DropdownMenuContent>
304+
<a href={doLocation} target="_blank">
248305
<DropdownMenuItem>
249-
<Microscope className="rfs-mr-1 rfs-size-4" /> Open in FAIR-DOscope
306+
<LinkIcon className="rfs-mr-1 rfs-size-4" /> Open Source
250307
</DropdownMenuItem>
251308
</a>
252-
)}
253-
</DropdownMenuContent>
254-
</DropdownMenu>
255-
</div>
309+
{showOpenInFairDoScope && (
310+
<a href={`https://kit-data-manager.github.io/fairdoscope/?pid=${pid}`} target="_blank">
311+
<DropdownMenuItem>
312+
<Microscope className="rfs-mr-1 rfs-size-4" /> Open in FAIR-DOscope
313+
</DropdownMenuItem>
314+
</a>
315+
)}
316+
</DropdownMenuContent>
317+
</DropdownMenu>
318+
</div>
319+
)}
256320
</div>
257321
</div>
258322
</div>

src/config/FairDOConfig.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ export interface FairDOConfig {
7373
*/
7474
host: string
7575
/**
76-
* @deprecated Only for testing! Using this in production will leak your API key
76+
* Authenticate against the elasticsearch backend using an API Key. Using this in a browser environment will leak this API key to all users!
7777
*/
7878
apiKey?: string
7979
/**

src/index.css

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,4 +74,10 @@
7474
body {
7575
@apply rfs-bg-background rfs-text-foreground;
7676
}
77+
78+
ul, ol {
79+
list-style: revert;
80+
margin: revert;
81+
padding: revert;
82+
}
7783
}

src/stories/Configure.mdx renamed to src/stories/0 Getting Started.mdx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Meta, Source } from "@storybook/blocks"
22

33
import "../index.css"
44

5-
<Meta title="Guide" />
5+
<Meta title="Getting Started" />
66

77
<div>
88
# React FairDO Search
@@ -31,6 +31,8 @@ import "../index.css"
3131

3232
### Example Configuration
3333

34+
Feel free to copy this example and make it work for your use case.
35+
3436
<Source code={`
3537
export default function Home() {
3638
// First we configure the search itself. Here we defined the elastic endpoint as well as the facets.

src/stories/1 Authentication.mdx

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { Meta, Source } from "@storybook/blocks"
2+
3+
import "../index.css"
4+
5+
<Meta title="Authentication" />
6+
7+
<div className="show-lists">
8+
# Authentication
9+
10+
Depending on your use case, you will have different needs for authenticating users. This package (as well as the underlying elasticsearch adapter) provide two ways to achieve this:
11+
12+
1. Authentication with an access code from an identity provider (like keycloak)
13+
2. Authentication with an API Key (not recommended)
14+
15+
While the first option requires significantly more work, the second option can't be considered secure, as your API Key will have to be provided to all users of the app. Feel free to use the second option in development.
16+
17+
### via Access Code
18+
19+
Set up authentication in your app depending on your identity provider. Then you can simply pass the access code to the search component:
20+
21+
<Source code={`
22+
function MyComponent() {
23+
const myAccessToken = useAccessToken() // This will depend on your authentication library
24+
25+
const config: FairDOConfig = useMemo(() => ({
26+
// ...
27+
host: "https://example.org/elastic-auth-proxy"
28+
connectionOptions: {
29+
headers: {
30+
Authentication: myAccessToken
31+
}
32+
}
33+
}), [myAccessToken])
34+
35+
// ...
36+
}
37+
`} />
38+
39+
You will have to run a proxy server that received the requests from the search component and checks the authentication header. The proxy server should then forward the request to a protected elasticsearch instance.
40+
41+
Depending on your authentication service, you have to make sure that the access token is refreshed when it becomes invalid.
42+
43+
### via API Key
44+
45+
Set up an API Key in the management interface of your elasticsearch instance. Then pass the API Key to the config:
46+
47+
<Source code={`
48+
function MyComponent() {
49+
const config: FairDOConfig = useMemo(() => ({
50+
// ...
51+
apiKey: "your key here"
52+
}), [])
53+
54+
// ...
55+
}
56+
`} />
57+
58+
The API Key will be visible to all users of your app. This approach is not recommended in production.
59+
</div>

src/stories/2 Custom Result View.mdx

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { Meta, Source } from "@storybook/blocks"
2+
3+
import "../index.css"
4+
5+
<Meta title="Custom Result View" />
6+
7+
# Custom Result View
8+
9+
To define your own result view, simply define a component with the following signature:
10+
11+
<Source code={`
12+
function MyResultView({ result }: ResultViewProps) {
13+
// Helper function to retrieve a field from the results
14+
const getField = useCallback(
15+
(field: string) => {
16+
return autoUnwrap(result[field])
17+
},
18+
[result]
19+
)
20+
21+
// Helper function to retrieve an array field from the results
22+
const getArrayField = useCallback(
23+
(field: string) => {
24+
return autoUnwrapArray(result[field])
25+
},
26+
[result]
27+
)
28+
29+
// Make sure to memoize everything! This is very important for performance
30+
const title = useMemo(() => {
31+
return getField("name")
32+
}, [getField])
33+
34+
return <div>{title}</div>
35+
}
36+
`} />
37+
38+
Then pass it to the search component:
39+
40+
<Source code={`
41+
<FairDOElasticSearch resultView={MyResultView} config={config} />
42+
`} />

src/stories/FairDOElasticSearch.stories.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ export const GenericResultRenderer: Story = {
105105
]}
106106
titleField="name"
107107
creationDateField="dateCreatedRfc3339"
108-
identifierField="identifier"
108+
additionalIdentifierField="identifier"
109109
digitalObjectLocationField="digitalObjectLocation"
110110
imageField="locationPreview/Sample"
111111
parentItemPidField="hasMetadata"

src/stories/GenericResultView.stories.tsx

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,31 @@ export default meta
1515

1616
type Story = StoryObj<typeof meta>
1717

18-
export const Default: Story = {
18+
export const Simple: Story = {
19+
decorators: [
20+
(Story) => (
21+
<TooltipProvider>
22+
<GlobalModalProvider>
23+
<div>
24+
<Story />
25+
</div>
26+
</GlobalModalProvider>
27+
</TooltipProvider>
28+
)
29+
],
30+
args: {
31+
result: {
32+
title: "GenericResultView",
33+
description: "This view can be customized"
34+
},
35+
titleField: "title",
36+
descriptionField: "description",
37+
imageField: undefined,
38+
invertImageInDarkMode: true
39+
}
40+
}
41+
42+
export const Full: Story = {
1943
decorators: [
2044
(Story) => (
2145
<TooltipProvider>

0 commit comments

Comments
 (0)