@rcsb/rcsb-documentation 0.0.2 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -55
- package/package.json +8 -3
- package/server/docsApi.js +299 -0
package/README.md
CHANGED
|
@@ -1,70 +1,38 @@
|
|
|
1
|
-
#
|
|
1
|
+
# RCSB Documentation Module (`@rcsb/rcsb-documentation`)
|
|
2
2
|
|
|
3
|
-
This project
|
|
3
|
+
This project is a React application that has been configured with Webpack for building and development.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
In the project directory, you can run:
|
|
8
|
-
|
|
9
|
-
### `npm start`
|
|
10
|
-
|
|
11
|
-
Runs the app in the development mode.\
|
|
12
|
-
Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
|
|
13
|
-
|
|
14
|
-
The page will reload when you make changes.\
|
|
15
|
-
You may also see any lint errors in the console.
|
|
16
|
-
|
|
17
|
-
### `npm test`
|
|
18
|
-
|
|
19
|
-
Launches the test runner in the interactive watch mode.\
|
|
20
|
-
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
|
21
|
-
|
|
22
|
-
### `npm run build`
|
|
23
|
-
|
|
24
|
-
Builds the app for production to the `build` folder.\
|
|
25
|
-
It correctly bundles React in production mode and optimizes the build for the best performance.
|
|
26
|
-
|
|
27
|
-
The build is minified and the filenames include the hashes.\
|
|
28
|
-
Your app is ready to be deployed!
|
|
5
|
+
The **RCSB Documentation Module** is a React-based project designed to serve as a dynamic and user-friendly landing page for hosting documentation related to [RCSB.org](https://www.rcsb.org/). This module is intended to be integrated within the **RCSB Sierra** web application, providing an enhanced user experience and streamlining access to information pertinent to the features and data available on RCSB.org.
|
|
29
6
|
|
|
30
|
-
|
|
7
|
+
## Installation
|
|
31
8
|
|
|
32
|
-
|
|
9
|
+
You can install the RCSB Documentation Module via npm:
|
|
33
10
|
|
|
34
|
-
|
|
11
|
+
npm install @rcsb/rcsb-documentation
|
|
35
12
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own.
|
|
39
|
-
|
|
40
|
-
You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it.
|
|
41
|
-
|
|
42
|
-
## Learn More
|
|
43
|
-
|
|
44
|
-
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
|
45
|
-
|
|
46
|
-
To learn React, check out the [React documentation](https://reactjs.org/).
|
|
47
|
-
|
|
48
|
-
### Code Splitting
|
|
49
|
-
|
|
50
|
-
This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
|
|
51
|
-
|
|
52
|
-
### Analyzing the Bundle Size
|
|
13
|
+
## Available Scripts
|
|
53
14
|
|
|
54
|
-
|
|
15
|
+
Inside the project directory, you can run the following commands:
|
|
55
16
|
|
|
56
|
-
###
|
|
17
|
+
### `npm start`
|
|
57
18
|
|
|
58
|
-
|
|
19
|
+
- Launches the development server with Webpack.
|
|
20
|
+
- Open [http://localhost:3000](http://localhost:3000) to view the app in your browser.
|
|
21
|
+
- The app supports hot reloading, so any changes to the source files will automatically reflect in the browser.
|
|
59
22
|
|
|
60
|
-
###
|
|
23
|
+
### `npm run build`
|
|
61
24
|
|
|
62
|
-
|
|
25
|
+
- Builds the project for production.
|
|
26
|
+
- Creates an optimized build in the `build` folder.
|
|
27
|
+
- The output is minified, and the filenames include content hashes for cache busting.
|
|
63
28
|
|
|
64
|
-
|
|
29
|
+
## Project Structure
|
|
65
30
|
|
|
66
|
-
|
|
31
|
+
- **`src/`**: Contains the source code of the application, including components, styles, and assets.
|
|
32
|
+
- **`build/`**: The production-ready output generated by Webpack after running `npm run build`.
|
|
33
|
+
- **`webpack.config.js`**: The Webpack configuration file that manages the build and bundling process.
|
|
34
|
+
- **`.babelrc`**: Babel configuration file for JavaScript transpilation.
|
|
67
35
|
|
|
68
|
-
|
|
36
|
+
## License
|
|
69
37
|
|
|
70
|
-
This
|
|
38
|
+
This project is licensed under the [MIT License](LICENSE).
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rcsb/rcsb-documentation",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.03",
|
|
5
5
|
"main": "build/bundle.js",
|
|
6
6
|
"files": [
|
|
7
|
-
"build"
|
|
7
|
+
"build",
|
|
8
|
+
"server/docsApi.js"
|
|
8
9
|
],
|
|
9
10
|
"dependencies": {
|
|
10
11
|
"node-fetch": "^3.3.2",
|
|
@@ -38,14 +39,18 @@
|
|
|
38
39
|
"devDependencies": {
|
|
39
40
|
"@babel/core": "^7.25.2",
|
|
40
41
|
"@babel/plugin-transform-modules-commonjs": "^7.24.7",
|
|
42
|
+
"@babel/plugin-transform-runtime": "^7.25.4",
|
|
41
43
|
"@babel/preset-env": "^7.25.4",
|
|
42
44
|
"@babel/preset-react": "^7.24.7",
|
|
45
|
+
"@babel/runtime": "^7.25.6",
|
|
46
|
+
"@babel/runtime-corejs3": "^7.25.6",
|
|
43
47
|
"babel-loader": "^9.1.3",
|
|
44
48
|
"css-loader": "^7.1.2",
|
|
45
49
|
"html-webpack-plugin": "^5.6.0",
|
|
46
50
|
"style-loader": "^4.0.0",
|
|
47
51
|
"webpack": "^5.94.0",
|
|
48
52
|
"webpack-cli": "^5.1.4",
|
|
49
|
-
"webpack-dev-server": "^5.1.0"
|
|
53
|
+
"webpack-dev-server": "^5.1.0",
|
|
54
|
+
"webpack-node-externals": "^3.0.0"
|
|
50
55
|
}
|
|
51
56
|
}
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
// Check if fetch is available; otherwise, use node-fetch
|
|
2
|
+
if (typeof fetch === 'undefined') {
|
|
3
|
+
global.fetch = require('node-fetch');
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
// Use native fetch in the browser environment
|
|
7
|
+
const CONTENT_URL = 'https://cs.rcsb.org/content';
|
|
8
|
+
const GROUP = 'group';
|
|
9
|
+
const ITEM = 'item';
|
|
10
|
+
const ROOT_ID = 'rcsb-documentation';
|
|
11
|
+
|
|
12
|
+
// Cached objects
|
|
13
|
+
let dbLastUpdated = null;
|
|
14
|
+
let initialized = false;
|
|
15
|
+
|
|
16
|
+
let index = [],
|
|
17
|
+
group_idMap = {},
|
|
18
|
+
groupNameMap = {},
|
|
19
|
+
item_idMap = {},
|
|
20
|
+
hrefMap = {},
|
|
21
|
+
itemMap = {};
|
|
22
|
+
|
|
23
|
+
// Fetch the latest 'lastUpdated' value for the top-level id
|
|
24
|
+
async function fetchLastUpdated(env) {
|
|
25
|
+
const url = `${CONTENT_URL}/${env}/by-top-id/${ROOT_ID}/last-updated`;
|
|
26
|
+
const response = await fetch(url, { method: 'GET' });
|
|
27
|
+
const data = await response.json();
|
|
28
|
+
return data.lastUpdated;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Fetch all items for the top-level id
|
|
32
|
+
async function fetchIndex(env) {
|
|
33
|
+
const url = `${CONTENT_URL}/${env}/by-top-id/${ROOT_ID}`;
|
|
34
|
+
const response = await fetch(url, { method: 'GET' });
|
|
35
|
+
const data = await response.json();
|
|
36
|
+
if (data && data.length) {
|
|
37
|
+
setIndex(data);
|
|
38
|
+
}
|
|
39
|
+
return data;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Fetch a specific item by its id
|
|
43
|
+
async function fetchItem(env, id) {
|
|
44
|
+
const url = `${CONTENT_URL}/${env}/${id}`;
|
|
45
|
+
const response = await fetch(url, { method: 'GET' });
|
|
46
|
+
const item = await response.json();
|
|
47
|
+
return item;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Initialize the index or check if it needs to be updated
|
|
51
|
+
async function initializeIndex(req) {
|
|
52
|
+
const env = getEnv(req);
|
|
53
|
+
let loadIndex = false;
|
|
54
|
+
|
|
55
|
+
const lastUpdated = await fetchLastUpdated(env);
|
|
56
|
+
|
|
57
|
+
if (!initialized || dbLastUpdated === null || lastUpdated > dbLastUpdated) {
|
|
58
|
+
dbLastUpdated = lastUpdated;
|
|
59
|
+
loadIndex = true;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (loadIndex) {
|
|
63
|
+
await fetchIndex(env);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Process and return the requested item
|
|
68
|
+
async function processItem(req, res) {
|
|
69
|
+
const env = getEnv(req);
|
|
70
|
+
await initializeIndex(req);
|
|
71
|
+
getItem(req, res, env);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Get environment from the request
|
|
75
|
+
function getEnv(req) {
|
|
76
|
+
return req.app.locals.instance === 'production' ? 'production' : 'staging';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Fetch and return the requested item
|
|
80
|
+
async function getItem(req, res, env) {
|
|
81
|
+
const instance = req.app.locals.instance;
|
|
82
|
+
let loadItem = false;
|
|
83
|
+
let reqUrl = req.originalUrl;
|
|
84
|
+
|
|
85
|
+
if (reqUrl.substring(reqUrl.length - 1) === '/') {
|
|
86
|
+
reqUrl = reqUrl.substring(0, reqUrl.length - 1);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (reqUrl === '/docs') {
|
|
90
|
+
const href = getFirstItemHref();
|
|
91
|
+
res.redirect(href);
|
|
92
|
+
} else if (group_idMap[reqUrl]) {
|
|
93
|
+
const href = getFirstItemHref(group_idMap[reqUrl]);
|
|
94
|
+
if (href) res.redirect(href);
|
|
95
|
+
else res.status(404).render('error', { statusCode: 404 });
|
|
96
|
+
} else {
|
|
97
|
+
const _id = item_idMap[reqUrl];
|
|
98
|
+
if (_id) {
|
|
99
|
+
if (!itemMap[_id]) {
|
|
100
|
+
loadItem = true;
|
|
101
|
+
} else {
|
|
102
|
+
const url = `${CONTENT_URL}/${env}/last-updated/${_id}`;
|
|
103
|
+
const response = await fetch(url, { method: 'GET' });
|
|
104
|
+
const data = await response.json();
|
|
105
|
+
|
|
106
|
+
if (data.lastUpdated > itemMap[_id].lastUpdated) loadItem = true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (loadItem) {
|
|
110
|
+
const item = await fetchItem(env, _id);
|
|
111
|
+
const imgPath = getImagePath(env, item, _id);
|
|
112
|
+
item.html = item.html.replace(/<IMG_PATH>/g, imgPath).replace(/https:\/\/www.rcsb.org/g, '');
|
|
113
|
+
item.lastUpdatedStr = new Date(item.lastUpdated).toLocaleDateString('en-US');
|
|
114
|
+
item.href = hrefMap[_id];
|
|
115
|
+
itemMap[_id] = item;
|
|
116
|
+
|
|
117
|
+
const menuPath = getMenuPath(item);
|
|
118
|
+
returnData(_id, instance, item, menuPath, res);
|
|
119
|
+
} else {
|
|
120
|
+
const item = itemMap[_id];
|
|
121
|
+
const menuPath = getMenuPath(item);
|
|
122
|
+
returnData(_id, instance, item, menuPath, res);
|
|
123
|
+
}
|
|
124
|
+
} else {
|
|
125
|
+
res.status(404).render('error', { statusCode: 404 });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Get image path based on environment and item properties
|
|
131
|
+
function getImagePath(env, item, _id) {
|
|
132
|
+
if (env === 'production') {
|
|
133
|
+
return item.useEast
|
|
134
|
+
? `https://cdn.rcsb.org/rcsb-pdb/content-east/${_id}/`
|
|
135
|
+
: `https://cdn.rcsb.org/rcsb-pdb/content/${_id}/`;
|
|
136
|
+
} else {
|
|
137
|
+
return `https://cms.rcsb.org/file-uploads/content/${_id}/`;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Get the first item href under a given group or the first item in the index if group_id is undefined
|
|
142
|
+
function getFirstItemHref(group_id) {
|
|
143
|
+
if (!group_id) {
|
|
144
|
+
for (let i = 0; i < index.length; i++) {
|
|
145
|
+
if (index[i].type === ITEM) return index[i].href;
|
|
146
|
+
}
|
|
147
|
+
return null;
|
|
148
|
+
} else {
|
|
149
|
+
let found = false;
|
|
150
|
+
for (let i = 0; i < index.length; i++) {
|
|
151
|
+
const node = index[i];
|
|
152
|
+
if (node.type === GROUP && node._id === group_id) found = true;
|
|
153
|
+
if (found && node.type === ITEM) return node.href;
|
|
154
|
+
}
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Set the index for fetched data
|
|
160
|
+
function setIndex(nodes) {
|
|
161
|
+
index.length = 0;
|
|
162
|
+
const groups = nodes.filter(node => node.type === GROUP);
|
|
163
|
+
const groupMap = {};
|
|
164
|
+
const depth = -1;
|
|
165
|
+
|
|
166
|
+
let root;
|
|
167
|
+
|
|
168
|
+
groups.forEach(group => {
|
|
169
|
+
groupMap[group._id] = group;
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
nodes.forEach(node => {
|
|
173
|
+
const { id, parent_id, type } = node;
|
|
174
|
+
const group = groupMap[parent_id];
|
|
175
|
+
|
|
176
|
+
if (type === GROUP && id === ROOT_ID) root = node;
|
|
177
|
+
|
|
178
|
+
if (group) {
|
|
179
|
+
if (!group.nodes) group.nodes = [];
|
|
180
|
+
group.nodes.push(node);
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
root.path = '/docs/';
|
|
185
|
+
rootToIndex(root, depth);
|
|
186
|
+
initialized = true;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Recursively convert root object to index array
|
|
190
|
+
function rootToIndex(group, depth) {
|
|
191
|
+
const { id, nodes, path } = group;
|
|
192
|
+
|
|
193
|
+
if (id !== ROOT_ID) {
|
|
194
|
+
group.numNodes = nodes ? nodes.length : 0;
|
|
195
|
+
const obj = getIndexObj(group, depth);
|
|
196
|
+
index.push(obj);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (nodes) {
|
|
200
|
+
nodes.forEach(node => {
|
|
201
|
+
node.href = path + node.id;
|
|
202
|
+
if (node.type === GROUP) {
|
|
203
|
+
const { _id, href, name } = node;
|
|
204
|
+
node.path = href + '/';
|
|
205
|
+
group_idMap[href] = _id;
|
|
206
|
+
groupNameMap[_id] = name;
|
|
207
|
+
rootToIndex(node, depth + 1);
|
|
208
|
+
} else {
|
|
209
|
+
hrefMap[node._id] = node.href;
|
|
210
|
+
item_idMap[node.href] = node._id;
|
|
211
|
+
index.push(getIndexObj(node, depth));
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Convert node to index node
|
|
218
|
+
function getIndexObj(node, depth) {
|
|
219
|
+
const { _id, href, name, parent_id, title, type } = node;
|
|
220
|
+
return type === GROUP ? { _id, depth, name, parent_id, type } : { _id, depth, href, parent_id, title, type };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Get menu path for the item
|
|
224
|
+
function getMenuPath(item) {
|
|
225
|
+
const { _ids } = item;
|
|
226
|
+
let menuPaths = [];
|
|
227
|
+
_ids.forEach(_id => {
|
|
228
|
+
if (groupNameMap[_id]) menuPaths.push(groupNameMap[_id]);
|
|
229
|
+
});
|
|
230
|
+
return menuPaths.join(' > ');
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Return data to be rendered in the template
|
|
234
|
+
function returnData(_id, instance, item, menuPath, res) {
|
|
235
|
+
const { description, title } = item;
|
|
236
|
+
const menuObj = getMenuObj(item);
|
|
237
|
+
const { groupMap, menu } = menuObj;
|
|
238
|
+
res.render('docs', { description, groupMap, instance, item, menu, menuPath, title });
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Get the menu object for rendering
|
|
242
|
+
function getMenuObj(item) {
|
|
243
|
+
const menu = JSON.parse(JSON.stringify(index));
|
|
244
|
+
const groupMap = {};
|
|
245
|
+
const groups = menu.filter(o => o.type === GROUP);
|
|
246
|
+
|
|
247
|
+
groups.forEach(group => {
|
|
248
|
+
group._ids = [];
|
|
249
|
+
groupMap[group._id] = group;
|
|
250
|
+
if (group.depth === 0) group.show = true;
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
item.show = true;
|
|
254
|
+
setParentGroupState(groupMap, item);
|
|
255
|
+
|
|
256
|
+
menu.forEach(o => {
|
|
257
|
+
const { _id, parent_id, type } = o;
|
|
258
|
+
const parentGroup = groupMap[parent_id];
|
|
259
|
+
|
|
260
|
+
if (parentGroup) {
|
|
261
|
+
o.show = parentGroup.open;
|
|
262
|
+
parentGroup._ids.push(_id);
|
|
263
|
+
}
|
|
264
|
+
if (type === ITEM && o._id === item._id) o.selected = true;
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
return { groupMap, menu };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Set parent group state for rendering
|
|
271
|
+
function setParentGroupState(groupMap, o) {
|
|
272
|
+
if (o.show) {
|
|
273
|
+
const parent = groupMap[o.parent_id];
|
|
274
|
+
if (parent) {
|
|
275
|
+
parent.open = true;
|
|
276
|
+
parent.show = true;
|
|
277
|
+
setParentGroupState(groupMap, parent);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Export all functions using CommonJS
|
|
283
|
+
module.exports = {
|
|
284
|
+
fetchLastUpdated,
|
|
285
|
+
fetchIndex,
|
|
286
|
+
fetchItem,
|
|
287
|
+
processItem,
|
|
288
|
+
getEnv,
|
|
289
|
+
initializeIndex,
|
|
290
|
+
getItem,
|
|
291
|
+
getFirstItemHref,
|
|
292
|
+
setIndex,
|
|
293
|
+
rootToIndex,
|
|
294
|
+
getIndexObj,
|
|
295
|
+
getMenuPath,
|
|
296
|
+
returnData,
|
|
297
|
+
getMenuObj,
|
|
298
|
+
setParentGroupState,
|
|
299
|
+
};
|