@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 CHANGED
@@ -1,70 +1,38 @@
1
- # Getting Started with Create React App
1
+ # RCSB Documentation Module (`@rcsb/rcsb-documentation`)
2
2
 
3
- This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
3
+ This project is a React application that has been configured with Webpack for building and development.
4
4
 
5
- ## Available Scripts
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
- See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
7
+ ## Installation
31
8
 
32
- ### `npm run eject`
9
+ You can install the RCSB Documentation Module via npm:
33
10
 
34
- **Note: this is a one-way operation. Once you `eject`, you can't go back!**
11
+ npm install @rcsb/rcsb-documentation
35
12
 
36
- If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
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
- This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
15
+ Inside the project directory, you can run the following commands:
55
16
 
56
- ### Making a Progressive Web App
17
+ ### `npm start`
57
18
 
58
- This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
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
- ### Advanced Configuration
23
+ ### `npm run build`
61
24
 
62
- This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
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
- ### Deployment
29
+ ## Project Structure
65
30
 
66
- This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
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
- ### `npm run build` fails to minify
36
+ ## License
69
37
 
70
- This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
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.02",
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
+ };