@rcsb/rcsb-documentation 0.0.1 → 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/build/bundle.js +2 -0
- package/build/bundle.js.LICENSE.txt +51 -0
- package/build/index.html +1 -1
- package/build/static/js/main.a51206b6.js.map +1 -1
- package/package.json +21 -12
- package/server/docsApi.js +299 -0
- package/src/index.js +0 -48
package/package.json
CHANGED
|
@@ -1,23 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rcsb/rcsb-documentation",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.0.
|
|
5
|
-
"main": "
|
|
4
|
+
"version": "0.0.03",
|
|
5
|
+
"main": "build/bundle.js",
|
|
6
6
|
"files": [
|
|
7
|
-
"build"
|
|
7
|
+
"build",
|
|
8
|
+
"server/docsApi.js"
|
|
8
9
|
],
|
|
9
10
|
"dependencies": {
|
|
11
|
+
"node-fetch": "^3.3.2",
|
|
10
12
|
"react": "^18.3.1",
|
|
11
13
|
"react-dom": "^18.3.1",
|
|
12
|
-
"react-router-dom": "^6.25.1"
|
|
13
|
-
"react-scripts": "^5.0.1"
|
|
14
|
+
"react-router-dom": "^6.25.1"
|
|
14
15
|
},
|
|
15
16
|
"scripts": {
|
|
16
|
-
"start": "
|
|
17
|
-
"build": "
|
|
18
|
-
"
|
|
19
|
-
"test": "react-scripts test",
|
|
20
|
-
"eject": "react-scripts eject"
|
|
17
|
+
"start": "webpack serve --mode development",
|
|
18
|
+
"build": "webpack --mode production",
|
|
19
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
21
20
|
},
|
|
22
21
|
"eslintConfig": {
|
|
23
22
|
"extends": [
|
|
@@ -38,10 +37,20 @@
|
|
|
38
37
|
]
|
|
39
38
|
},
|
|
40
39
|
"devDependencies": {
|
|
40
|
+
"@babel/core": "^7.25.2",
|
|
41
41
|
"@babel/plugin-transform-modules-commonjs": "^7.24.7",
|
|
42
|
+
"@babel/plugin-transform-runtime": "^7.25.4",
|
|
43
|
+
"@babel/preset-env": "^7.25.4",
|
|
44
|
+
"@babel/preset-react": "^7.24.7",
|
|
45
|
+
"@babel/runtime": "^7.25.6",
|
|
46
|
+
"@babel/runtime-corejs3": "^7.25.6",
|
|
47
|
+
"babel-loader": "^9.1.3",
|
|
42
48
|
"css-loader": "^7.1.2",
|
|
43
|
-
"
|
|
49
|
+
"html-webpack-plugin": "^5.6.0",
|
|
44
50
|
"style-loader": "^4.0.0",
|
|
45
|
-
"webpack
|
|
51
|
+
"webpack": "^5.94.0",
|
|
52
|
+
"webpack-cli": "^5.1.4",
|
|
53
|
+
"webpack-dev-server": "^5.1.0",
|
|
54
|
+
"webpack-node-externals": "^3.0.0"
|
|
46
55
|
}
|
|
47
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
|
+
};
|
package/src/index.js
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import ReactDOM from 'react-dom/client';
|
|
3
|
-
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
|
|
4
|
-
import routes from './routes';
|
|
5
|
-
import ErrorBoundary from './components/ErrorBoundary';
|
|
6
|
-
|
|
7
|
-
const NotFound = () => (
|
|
8
|
-
<div>
|
|
9
|
-
Oops, looks like this page is not exist. Try going back to the homepage.
|
|
10
|
-
</div>
|
|
11
|
-
);
|
|
12
|
-
|
|
13
|
-
const AppContent = () => {
|
|
14
|
-
return (
|
|
15
|
-
<main className="doc-container mt-5">
|
|
16
|
-
<Routes>
|
|
17
|
-
{routes.map((route, index) => (
|
|
18
|
-
<Route
|
|
19
|
-
key={index}
|
|
20
|
-
path={route.path}
|
|
21
|
-
element={route.element}
|
|
22
|
-
/>
|
|
23
|
-
))}
|
|
24
|
-
<Route path="*" element={<NotFound />} />
|
|
25
|
-
</Routes>
|
|
26
|
-
</main>
|
|
27
|
-
);
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
//Set the global variable for basename
|
|
31
|
-
const basename =window.__DOCUMENTATION_BASEROUTE__ || '';
|
|
32
|
-
const App = () => {
|
|
33
|
-
return (
|
|
34
|
-
<ErrorBoundary>
|
|
35
|
-
<Router basename={basename}>
|
|
36
|
-
<AppContent />
|
|
37
|
-
</Router>
|
|
38
|
-
</ErrorBoundary>
|
|
39
|
-
);
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
const root = ReactDOM.createRoot(document.getElementById('root'));
|
|
43
|
-
root.render(
|
|
44
|
-
<React.StrictMode>
|
|
45
|
-
<App basename={basename} />
|
|
46
|
-
</React.StrictMode>
|
|
47
|
-
);
|
|
48
|
-
|