nmdc-runtime 1.3.1__py3-none-any.whl → 2.12.0__py3-none-any.whl

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.
Files changed (143) hide show
  1. nmdc_runtime/Dockerfile +177 -0
  2. nmdc_runtime/api/analytics.py +90 -0
  3. nmdc_runtime/api/boot/capabilities.py +9 -0
  4. nmdc_runtime/api/boot/object_types.py +126 -0
  5. nmdc_runtime/api/boot/triggers.py +84 -0
  6. nmdc_runtime/api/boot/workflows.py +116 -0
  7. nmdc_runtime/api/core/auth.py +212 -0
  8. nmdc_runtime/api/core/idgen.py +200 -0
  9. nmdc_runtime/api/core/metadata.py +777 -0
  10. nmdc_runtime/api/core/util.py +114 -0
  11. nmdc_runtime/api/db/mongo.py +436 -0
  12. nmdc_runtime/api/db/s3.py +37 -0
  13. nmdc_runtime/api/endpoints/capabilities.py +25 -0
  14. nmdc_runtime/api/endpoints/find.py +634 -0
  15. nmdc_runtime/api/endpoints/jobs.py +206 -0
  16. nmdc_runtime/api/endpoints/lib/helpers.py +274 -0
  17. nmdc_runtime/api/endpoints/lib/linked_instances.py +193 -0
  18. nmdc_runtime/api/endpoints/lib/path_segments.py +165 -0
  19. nmdc_runtime/api/endpoints/metadata.py +260 -0
  20. nmdc_runtime/api/endpoints/nmdcschema.py +515 -0
  21. nmdc_runtime/api/endpoints/object_types.py +38 -0
  22. nmdc_runtime/api/endpoints/objects.py +277 -0
  23. nmdc_runtime/api/endpoints/operations.py +78 -0
  24. nmdc_runtime/api/endpoints/queries.py +701 -0
  25. nmdc_runtime/api/endpoints/runs.py +98 -0
  26. nmdc_runtime/api/endpoints/search.py +38 -0
  27. nmdc_runtime/api/endpoints/sites.py +205 -0
  28. nmdc_runtime/api/endpoints/triggers.py +25 -0
  29. nmdc_runtime/api/endpoints/users.py +214 -0
  30. nmdc_runtime/api/endpoints/util.py +817 -0
  31. nmdc_runtime/api/endpoints/wf_file_staging.py +307 -0
  32. nmdc_runtime/api/endpoints/workflows.py +353 -0
  33. nmdc_runtime/api/entrypoint.sh +7 -0
  34. nmdc_runtime/api/main.py +495 -0
  35. nmdc_runtime/api/middleware.py +43 -0
  36. nmdc_runtime/api/models/capability.py +14 -0
  37. nmdc_runtime/api/models/id.py +92 -0
  38. nmdc_runtime/api/models/job.py +57 -0
  39. nmdc_runtime/api/models/lib/helpers.py +78 -0
  40. nmdc_runtime/api/models/metadata.py +11 -0
  41. nmdc_runtime/api/models/nmdc_schema.py +146 -0
  42. nmdc_runtime/api/models/object.py +180 -0
  43. nmdc_runtime/api/models/object_type.py +20 -0
  44. nmdc_runtime/api/models/operation.py +66 -0
  45. nmdc_runtime/api/models/query.py +246 -0
  46. nmdc_runtime/api/models/query_continuation.py +111 -0
  47. nmdc_runtime/api/models/run.py +161 -0
  48. nmdc_runtime/api/models/site.py +87 -0
  49. nmdc_runtime/api/models/trigger.py +13 -0
  50. nmdc_runtime/api/models/user.py +207 -0
  51. nmdc_runtime/api/models/util.py +260 -0
  52. nmdc_runtime/api/models/wfe_file_stages.py +122 -0
  53. nmdc_runtime/api/models/workflow.py +15 -0
  54. nmdc_runtime/api/openapi.py +178 -0
  55. nmdc_runtime/api/swagger_ui/assets/EllipsesButton.js +146 -0
  56. nmdc_runtime/api/swagger_ui/assets/EndpointSearchWidget.js +369 -0
  57. nmdc_runtime/api/swagger_ui/assets/script.js +252 -0
  58. nmdc_runtime/api/swagger_ui/assets/style.css +155 -0
  59. nmdc_runtime/api/swagger_ui/swagger_ui.py +34 -0
  60. nmdc_runtime/config.py +56 -0
  61. nmdc_runtime/minter/adapters/repository.py +22 -2
  62. nmdc_runtime/minter/config.py +30 -4
  63. nmdc_runtime/minter/domain/model.py +55 -1
  64. nmdc_runtime/minter/entrypoints/fastapi_app.py +1 -1
  65. nmdc_runtime/mongo_util.py +89 -0
  66. nmdc_runtime/site/backup/nmdcdb_mongodump.py +1 -1
  67. nmdc_runtime/site/backup/nmdcdb_mongoexport.py +1 -3
  68. nmdc_runtime/site/changesheets/data/OmicsProcessing-to-catted-Biosamples.tsv +1561 -0
  69. nmdc_runtime/site/changesheets/scripts/missing_neon_soils_ecosystem_data.py +311 -0
  70. nmdc_runtime/site/changesheets/scripts/neon_soils_add_ncbi_ids.py +210 -0
  71. nmdc_runtime/site/dagster.yaml +53 -0
  72. nmdc_runtime/site/entrypoint-daemon.sh +29 -0
  73. nmdc_runtime/site/entrypoint-dagit-readonly.sh +26 -0
  74. nmdc_runtime/site/entrypoint-dagit.sh +29 -0
  75. nmdc_runtime/site/export/ncbi_xml.py +1331 -0
  76. nmdc_runtime/site/export/ncbi_xml_utils.py +405 -0
  77. nmdc_runtime/site/export/study_metadata.py +27 -4
  78. nmdc_runtime/site/graphs.py +294 -45
  79. nmdc_runtime/site/ops.py +1008 -230
  80. nmdc_runtime/site/repair/database_updater.py +451 -0
  81. nmdc_runtime/site/repository.py +368 -133
  82. nmdc_runtime/site/resources.py +154 -80
  83. nmdc_runtime/site/translation/gold_translator.py +235 -83
  84. nmdc_runtime/site/translation/neon_benthic_translator.py +212 -188
  85. nmdc_runtime/site/translation/neon_soil_translator.py +82 -58
  86. nmdc_runtime/site/translation/neon_surface_water_translator.py +698 -0
  87. nmdc_runtime/site/translation/neon_utils.py +24 -7
  88. nmdc_runtime/site/translation/submission_portal_translator.py +616 -162
  89. nmdc_runtime/site/translation/translator.py +73 -3
  90. nmdc_runtime/site/util.py +26 -7
  91. nmdc_runtime/site/validation/emsl.py +1 -0
  92. nmdc_runtime/site/validation/gold.py +1 -0
  93. nmdc_runtime/site/validation/util.py +16 -12
  94. nmdc_runtime/site/workspace.yaml +13 -0
  95. nmdc_runtime/static/NMDC_logo.svg +1073 -0
  96. nmdc_runtime/static/ORCID-iD_icon_vector.svg +4 -0
  97. nmdc_runtime/static/README.md +5 -0
  98. nmdc_runtime/static/favicon.ico +0 -0
  99. nmdc_runtime/util.py +236 -192
  100. nmdc_runtime-2.12.0.dist-info/METADATA +45 -0
  101. nmdc_runtime-2.12.0.dist-info/RECORD +131 -0
  102. {nmdc_runtime-1.3.1.dist-info → nmdc_runtime-2.12.0.dist-info}/WHEEL +1 -2
  103. {nmdc_runtime-1.3.1.dist-info → nmdc_runtime-2.12.0.dist-info}/entry_points.txt +0 -1
  104. nmdc_runtime/containers.py +0 -14
  105. nmdc_runtime/core/db/Database.py +0 -15
  106. nmdc_runtime/core/exceptions/__init__.py +0 -23
  107. nmdc_runtime/core/exceptions/base.py +0 -47
  108. nmdc_runtime/core/exceptions/token.py +0 -13
  109. nmdc_runtime/domain/users/queriesInterface.py +0 -18
  110. nmdc_runtime/domain/users/userSchema.py +0 -37
  111. nmdc_runtime/domain/users/userService.py +0 -14
  112. nmdc_runtime/infrastructure/database/db.py +0 -3
  113. nmdc_runtime/infrastructure/database/models/user.py +0 -10
  114. nmdc_runtime/lib/__init__.py +0 -1
  115. nmdc_runtime/lib/extract_nmdc_data.py +0 -41
  116. nmdc_runtime/lib/load_nmdc_data.py +0 -121
  117. nmdc_runtime/lib/nmdc_dataframes.py +0 -829
  118. nmdc_runtime/lib/nmdc_etl_class.py +0 -402
  119. nmdc_runtime/lib/transform_nmdc_data.py +0 -1117
  120. nmdc_runtime/site/drsobjects/ingest.py +0 -93
  121. nmdc_runtime/site/drsobjects/registration.py +0 -131
  122. nmdc_runtime/site/terminusdb/generate.py +0 -198
  123. nmdc_runtime/site/terminusdb/ingest.py +0 -44
  124. nmdc_runtime/site/terminusdb/schema.py +0 -1671
  125. nmdc_runtime/site/translation/emsl.py +0 -42
  126. nmdc_runtime/site/translation/gold.py +0 -53
  127. nmdc_runtime/site/translation/jgi.py +0 -31
  128. nmdc_runtime/site/translation/util.py +0 -132
  129. nmdc_runtime/site/validation/jgi.py +0 -42
  130. nmdc_runtime-1.3.1.dist-info/METADATA +0 -181
  131. nmdc_runtime-1.3.1.dist-info/RECORD +0 -81
  132. nmdc_runtime-1.3.1.dist-info/top_level.txt +0 -1
  133. /nmdc_runtime/{client → api}/__init__.py +0 -0
  134. /nmdc_runtime/{core → api/boot}/__init__.py +0 -0
  135. /nmdc_runtime/{core/db → api/core}/__init__.py +0 -0
  136. /nmdc_runtime/{domain → api/db}/__init__.py +0 -0
  137. /nmdc_runtime/{domain/users → api/endpoints}/__init__.py +0 -0
  138. /nmdc_runtime/{infrastructure → api/endpoints/lib}/__init__.py +0 -0
  139. /nmdc_runtime/{infrastructure/database → api/models}/__init__.py +0 -0
  140. /nmdc_runtime/{infrastructure/database/models → api/models/lib}/__init__.py +0 -0
  141. /nmdc_runtime/{site/drsobjects/__init__.py → api/models/minter.py} +0 -0
  142. /nmdc_runtime/site/{terminusdb → repair}/__init__.py +0 -0
  143. {nmdc_runtime-1.3.1.dist-info → nmdc_runtime-2.12.0.dist-info/licenses}/LICENSE +0 -0
@@ -0,0 +1,369 @@
1
+ /**
2
+ * This JavaScript file contains the implementation of a Web Component designed for use with Swagger UI.
3
+ * Reference: https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements
4
+ *
5
+ * Developer notes:
6
+ *
7
+ * 1. Because we define our CSS and HTML within `String.raw` tagged templates [1],
8
+ * the popular "lit-html" extension [2] (if installed) for VS Code will apply
9
+ * CSS and HTML syntax highlighting to those strings.
10
+ *
11
+ * References:
12
+ * - [1] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals
13
+ * - [2] https://marketplace.visualstudio.com/items?itemName=bierner.lit-html
14
+ *
15
+ *****************************************************************************/
16
+
17
+ // If FastAPI's Swagger UI JavaScript hasn't been executed yet, throw an error explaining the situation.
18
+ // Note: FastAPI runs the Swagger UI JavaScript in a way that creates a global variable named `ui`.
19
+ // Note: We do a `typeof` check because, if we did `ui === undefined` and `ui` weren't defined,
20
+ // JavaScript would raise its own `ReferenceError` exception with its own message.
21
+ if (typeof ui === "undefined") {
22
+ throw new Error("FastAPI's Swagger UI JavaScript has not been executed yet.");
23
+ }
24
+
25
+ /**
26
+ * Endpoint search widget, implemented as a Web Component.
27
+ *
28
+ * The widget provides a search input field, and a results panel that lists
29
+ * the API endpoints whose URL paths match the search term. When the user
30
+ * clicks on a search result, the browser navigates to the corresponding
31
+ * endpoint in the Swagger UI page (via a full page refresh/"deep link").
32
+ *
33
+ * References:
34
+ * - https://developer.mozilla.org/en-US/docs/Web/API/Web_components
35
+ * - https://open-wc.org/codelabs/basics/web-components#2
36
+ */
37
+ class EndpointSearchWidget extends HTMLElement {
38
+ constructor() {
39
+ super();
40
+
41
+ // Initialize all instance properties.
42
+ this.inputEl = null;
43
+ this.resultsPanelEl = null;
44
+ this.noEndpointsMessageEl = null;
45
+ this.resultsListEl = null;
46
+ this.endpoints = [];
47
+
48
+ // Redefine each callback method so that, within that method, the "this" variable
49
+ // refers to this instance of the class.
50
+ //
51
+ // Note: If we didn't do this, the value of the "this" variable would depend upon how
52
+ // the method was invoked (e.g. if invoked via an event listener, the "this"
53
+ // variable would refer to the element that triggered the event; e.g. a text field).
54
+ //
55
+ // Reference:
56
+ // - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind#creating_a_bound_function
57
+ //
58
+ this._handleInput = this._handleInput.bind(this);
59
+ }
60
+
61
+ /**
62
+ * Initializes the custom HTML element after it's been added to the DOM.
63
+ *
64
+ * Note: The `connectedCallback` function gets called automatically when
65
+ * this HTML element gets added to the DOM.
66
+ *
67
+ * References:
68
+ * - https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#custom_element_lifecycle_callbacks
69
+ */
70
+ connectedCallback() {
71
+ // Create a Shadow DOM tree specific to this custom HTML element, so that its styles don't
72
+ // impact other elements on the page and so styles on the page don't impact this element. [1]
73
+ //
74
+ // Note: This will populate the instance's inherited `this.shadowRoot` property [2]
75
+ // with a reference to the root node of this Shadow DOM tree. [3]
76
+ //
77
+ // References:
78
+ // - [1] https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM#creating_a_shadow_dom
79
+ // - [2] https://developer.mozilla.org/en-US/docs/Web/API/Element/shadowRoot
80
+ // - [3] https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot
81
+ //
82
+ this.attachShadow({ mode: "open" });
83
+
84
+ // Implement the structure and style of the widget.
85
+ //
86
+ // Note: We chose the specific style rules shown below in an attempt to
87
+ // match the "look and feel" of the Swagger UI page. At least some
88
+ // of the seemingly-arbitrarily-chosen values were copied verbatim
89
+ // from native Swagger UI elements (via a web browser's DevTools).
90
+ //
91
+ this.shadowRoot.innerHTML = String.raw`
92
+ <style>
93
+ :host {
94
+ display: block;
95
+ margin: 0 auto;
96
+ max-width: 1460px;
97
+ font-family: sans-serif;
98
+ font-size: 14px;
99
+ color: #3b4151;
100
+ }
101
+ .container {
102
+ margin: 20px 20px 0px 20px;
103
+ }
104
+ input {
105
+ padding: 8px 10px;
106
+ box-sizing: border-box;
107
+ border: 1px solid #d9d9d9;
108
+ border-radius: 4px;
109
+ width: 100%;
110
+ }
111
+ .results-panel {
112
+ background-color: rgba(0, 0, 0, .05);
113
+ border-radius: 4px;
114
+ }
115
+ .results-panel p.no-endpoints {
116
+ font-size: 14px;
117
+ padding: 26px;
118
+ }
119
+ ul {
120
+ list-style-type: none;
121
+ padding-left: 26px;
122
+ padding-right: 26px;
123
+ font-family: monospace;
124
+ }
125
+ ul > li:first-child {
126
+ padding-top: 26px;
127
+ }
128
+ ul > li:last-child {
129
+ padding-bottom: 26px;
130
+ }
131
+ li > a {
132
+ color: #4990e2;
133
+ text-decoration: none;
134
+ }
135
+ li > a:hover {
136
+ color: #1f69c0;
137
+ }
138
+ li > a .http-method {
139
+ display: inline-block;
140
+ min-width: 52px;
141
+ margin-right: 10px;
142
+ }
143
+ .matching-substring {
144
+ background-color: #f9f871;
145
+ }
146
+ .hidden {
147
+ /*
148
+ Note: When we hide elements via 'display: none', we do not have to also set 'aria-hidden="true"'.
149
+ MDN says: 'aria-hidden="true" should not be added when [...] The element [...] is hidden with 'display: none'.
150
+ Reference: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-hidden
151
+ */
152
+ display: none;
153
+ }
154
+ </style>
155
+
156
+ <div class="container">
157
+ <input name="search-term" placeholder="Find an endpoint..." aria-label="Search term for finding an endpoint" />
158
+ <div class="results-panel">
159
+ <p class="no-endpoints hidden">No matching endpoints.</p>
160
+ <ul class="results-list"></ul>
161
+ </div>
162
+ </div>
163
+ `;
164
+ this.inputEl = this.shadowRoot.querySelector("input");
165
+ this.resultsPanelEl = this.shadowRoot.querySelector(".results-panel");
166
+ this.noEndpointsMessageEl = this.shadowRoot.querySelector(".no-endpoints");
167
+ this.resultsListEl = this.shadowRoot.querySelector(".results-list");
168
+
169
+ // Make an array of all the endpoints that Swagger UI knows about. This will be our search index.
170
+ //
171
+ // Note: In an earlier implementation of this step, we queried the DOM for this information.
172
+ // However, that didn't work when any of the endpoint groups were collapsed. So, instead,
173
+ // we access the data structure returned by the `SwaggerUI()` constructor, which is
174
+ // invoked by the JavaScript code built into FastAPI (that JavaScript code assigns
175
+ // the return value to a global variable named `ui`, which we access here).
176
+ //
177
+ // Note: Should we eventually choose to display endpoint's description also, we can be obtain it
178
+ // via the endpoint's operation Map; i.e. `opMap.get("operation").get("description")`.
179
+ //
180
+ // Reference: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/get
181
+ //
182
+ const operationMaps = Array.from(ui.specSelectors.operations());
183
+ this.endpoints = operationMaps.map(opMap => ({
184
+ urlPath: opMap.get("path"), // e.g. "/studies/{study_id}"
185
+ httpMethod: opMap.get("method").toUpperCase(), // e.g. "GET"
186
+ operationId: opMap.get("operation").get("operationId"), // e.g. "find_studies_studies_get"
187
+ tag: opMap.get("operation").get("tags").get(0), // e.g. "Metadata access: Find"
188
+ }));
189
+ console.debug(`Found ${this.endpoints.length} endpoints in OpenAPI schema`);
190
+
191
+ // Make it so the search results list gets updated whenever the user changes the search input.
192
+ // Reference: https://developer.mozilla.org/en-US/docs/Web/API/Element/input_event
193
+ this.inputEl.addEventListener("input", this._handleInput);
194
+ }
195
+
196
+ /**
197
+ * Callback function that handles the "input" event on the endpoint search input field.
198
+ * The main role of this method is to extract the search term from the event object
199
+ * and pass it to the method that updates the search results to reflect that search term.
200
+ *
201
+ * @param {Event} event The "input" event.
202
+ */
203
+ _handleInput(event) {
204
+ this.updateSearchResults(event.target.value);
205
+ }
206
+
207
+ /**
208
+ * Callback function designed to be passed in as the `compareFn` argument to `Array.prototype.sort()`.
209
+ * This callback function that sorts endpoint objects by comparing the URL paths and, if those are
210
+ * equal, comparing the HTTP methods.
211
+ *
212
+ * @param {{ urlPath: string, httpMethod: string }} a
213
+ * @param {{ urlPath: string, httpMethod: string }} b
214
+ * @returns {number} A negative number if `a` comes before `b`,
215
+ * a positive number if `a` comes after `b`,
216
+ * and 0 if `a` and `b` are equivalent.
217
+ *
218
+ * References:
219
+ * - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#comparefn
220
+ * - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/localeCompare
221
+ */
222
+ _sortEndpoints(a, b) {
223
+ if (a.urlPath.localeCompare(b.urlPath) === 0) {
224
+ return a.httpMethod.localeCompare(b.httpMethod);
225
+ } else {
226
+ return a.urlPath.localeCompare(b.urlPath);
227
+ }
228
+ }
229
+
230
+ /**
231
+ * Wraps the part of the full string that matches the test string (case insensitively),
232
+ * in a `<span>` element having the class "matching-substring".
233
+ *
234
+ * Note: That will allow us to style that part of the string differently from the other parts.
235
+ *
236
+ * @param {string} fullStr The full string.
237
+ * @param {string} searchTermStr The string to search for within the full string.
238
+ * @returns {HTMLSpanElement} A `<span>` element containing the full string, with the matching
239
+ * substring—if any—wrapped in a `<span class="matching-substring">`.
240
+ */
241
+ _wrapMatchingSubstring(fullStr, searchTermStr) {
242
+ // Create the outer `<span>` element.
243
+ const fullStrSpanEl = document.createElement("span");
244
+
245
+ // Return early if we know the search term string _cannot_ exist within the full string.
246
+ if (fullStr.length === 0 || searchTermStr.length === 0 || searchTermStr.length > fullStr.length) {
247
+ fullStrSpanEl.textContent = fullStr;
248
+ return fullStrSpanEl;
249
+ }
250
+
251
+ // Lowercase both strings to enable case-insensitive comparison.
252
+ const fullStrLowercase = fullStr.toLowerCase();
253
+ const searchTermStrLowercase = searchTermStr.toLowerCase();
254
+
255
+ // Locate the (lowercased) substring within the (lowercased) full string.
256
+ const substrCharIdx = fullStrLowercase.indexOf(searchTermStrLowercase);
257
+
258
+ // Return early if we know the search term string _doesn't_ exist within the full string.
259
+ if (substrCharIdx === -1) {
260
+ fullStrSpanEl.textContent = fullStr;
261
+ return fullStrSpanEl;
262
+ }
263
+
264
+ // Split the full string into three parts.
265
+ const preSubstrChars = fullStr.substring(0, substrCharIdx);
266
+ const substringChars = fullStr.substring(substrCharIdx, substrCharIdx + searchTermStr.length)
267
+ const postSubstrChars = fullStr.substring(substrCharIdx + searchTermStr.length);
268
+
269
+ // Create and populate the inner `<span>` element containing the matching substring.
270
+ const matchingSubstrSpanEl = document.createElement("span");
271
+ matchingSubstrSpanEl.classList.add("matching-substring");
272
+ matchingSubstrSpanEl.textContent = substringChars;
273
+
274
+ // Populate the outer `<span>` element.
275
+ fullStrSpanEl.replaceChildren(preSubstrChars, matchingSubstrSpanEl, postSubstrChars);
276
+ return fullStrSpanEl;
277
+ }
278
+
279
+ /**
280
+ * Returns a Swagger UI-compliant "Deep Link" URL pointing to the specific API endpoint
281
+ * on a Swagger UI page.
282
+ *
283
+ * @param {string} tagNameStr Name of the tag of the endpoint being linked to.
284
+ * @param {string} operationIdStr Operation ID of the endpoint being linked to.
285
+ * @returns {string} A Swagger UI-compliant "Deep Link" URL that points to the
286
+ * specified API endpoint on a Swagger UI page.
287
+ *
288
+ * Reference: https://swagger.io/docs/open-source-tools/swagger-ui/usage/deep-linking/#usage
289
+ */
290
+ _buildSwaggerUIDeepLinkUrl(tagNameStr, operationIdStr) {
291
+ const urlWithoutQueryStr = window.location.origin + window.location.pathname;
292
+ const encodedTagNameStr = encodeURIComponent(tagNameStr);
293
+ const encodedOperationIdPart = encodeURIComponent(operationIdStr);
294
+ return `${urlWithoutQueryStr}/#${encodedTagNameStr}/${encodedOperationIdPart}`;
295
+ }
296
+
297
+ /**
298
+ * Updates the search results list so it shows the endpoints whose URL paths contain the search term.
299
+ *
300
+ * @param {string} searchTerm The search term.
301
+ */
302
+ updateSearchResults(searchTerm) {
303
+ // Special case: If the search term is empty, clear the search results.
304
+ if (searchTerm.trim().length === 0) {
305
+ this.resultsListEl.replaceChildren();
306
+ this.noEndpointsMessageEl.classList.add("hidden");
307
+ return;
308
+ }
309
+
310
+ // Lowercase the search term to enable case-insensitive comparison.
311
+ const lowercaseSearchTerm = searchTerm.toLowerCase();
312
+
313
+ // Make a list of all endpoints whose URL paths contains the search term.
314
+ const matchingEndpoints = this.endpoints.filter(item => {
315
+ const lowercaseUrlPath = item.urlPath.toLowerCase();
316
+ return lowercaseUrlPath.includes(lowercaseSearchTerm);
317
+ });
318
+
319
+ // If there are no matching endpoints, clear the search results.
320
+ if (matchingEndpoints.length === 0) {
321
+ this.resultsListEl.replaceChildren();
322
+ this.noEndpointsMessageEl.classList.remove("hidden");
323
+ return;
324
+ } else {
325
+ this.noEndpointsMessageEl.classList.add("hidden");
326
+ }
327
+
328
+ // Make an array of hyperlinks to the matching endpoints.
329
+ const resultEls = matchingEndpoints.sort(this._sortEndpoints).map(matchingEndpoint => {
330
+ // Wrap the part of the URL path that matches the search term, so that we can
331
+ // style it differently from the rest of the URL path.
332
+ const urlPathSpanEl = this._wrapMatchingSubstring(matchingEndpoint.urlPath, lowercaseSearchTerm);
333
+
334
+ // Build a hyperlink that points to the matching endpoint.
335
+ //
336
+ // Note: The reason we don't use something like `scrollTo` or `scrollIntoView`
337
+ // is that the target element may not be mounted to the DOM right now,
338
+ // due to it being within a collapsed section. Instead, we send the
339
+ // browser to the "deep link" URL (Swagger UI will automatically expand
340
+ // the relevant section when the page loads that "deep link" URL).
341
+ //
342
+ const aEl = document.createElement("a");
343
+ const httpMethodSpanEl = document.createElement("span");
344
+ httpMethodSpanEl.classList.add("http-method"); // e.g. "<span class="http-method">GET</span>"
345
+ httpMethodSpanEl.textContent = matchingEndpoint.httpMethod;
346
+ aEl.href = this._buildSwaggerUIDeepLinkUrl(matchingEndpoint.tag, matchingEndpoint.operationId);
347
+ aEl.replaceChildren(httpMethodSpanEl, urlPathSpanEl);
348
+
349
+ // Return a list item consisting of the hyperlink.
350
+ const liEl = document.createElement("li");
351
+ liEl.replaceChildren(aEl);
352
+ return liEl;
353
+ });
354
+ this.resultsListEl.replaceChildren(...resultEls);
355
+ }
356
+
357
+ /**
358
+ * Callback function that will be automatically invoked when the element is removed from the DOM.
359
+ *
360
+ * References:
361
+ * - https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#custom_element_lifecycle_callbacks
362
+ */
363
+ disconnectedCallback() {
364
+ console.debug("Cleaning up event listener(s)");
365
+ if (this.inputEl !== null) {
366
+ this.inputEl.removeEventListener("input", this._handleInput);
367
+ }
368
+ }
369
+ }
@@ -0,0 +1,252 @@
1
+ console.debug("Listening for event: nmdcInit");
2
+ window.addEventListener("nmdcInit", (event) => {
3
+ console.debug("Detected event: nmdcInit");
4
+
5
+ // Get the DOM elements we'll be referencing below.
6
+ const bodyEl = document.querySelector("body");
7
+
8
+ // Add the NMDC logo to the top of the page, next to the title.
9
+ // Note: The logo image will be added as a background image via CSS.
10
+ const addLogo = () => {
11
+ console.debug("Adding logo");
12
+ const headingGroupEl = document.querySelector(".information-container hgroup.main");
13
+ const titleEl = headingGroupEl.querySelector(".title");
14
+ const openapiSchemaLinkEl = headingGroupEl.querySelector("a.link");
15
+ const titleWrapperEl = document.createElement("div");
16
+ const logoEl = document.createElement("div");
17
+ logoEl.classList.add("nmdc-logo");
18
+ headingGroupEl.classList.add("nmdc-heading-group");
19
+ titleWrapperEl.replaceChildren(titleEl, openapiSchemaLinkEl);
20
+ headingGroupEl.replaceChildren(logoEl, titleWrapperEl);
21
+ };
22
+ addLogo();
23
+
24
+ // If there is a non-empty access token present in the DOM (see `main.py`), create and add a banner
25
+ // displaying the token along with buttons to show/hide it and copy it to the clipboard.
26
+ const accessToken = document.getElementById("nmdc-access-token")?.getAttribute("data-token");
27
+ if (typeof accessToken === "string" && accessToken.trim().length > 0) {
28
+ console.debug("Adding token banner");
29
+
30
+ // Create the banner.
31
+ const sectionEl = document.createElement("section");
32
+ sectionEl.classList.add("nmdc-info", "nmdc-info-token", "block", "col-12");
33
+ sectionEl.innerHTML = `
34
+ <p>You are now authorized. Prefer a command-line interface (CLI)? Use this header for HTTP requests:</p>
35
+ <p>
36
+ <code>
37
+ <span>Authorization: Bearer </span>
38
+ <span id="token" data-state="masked">***</span>
39
+ </code>
40
+ </p>
41
+ <p>
42
+ <button id="token-mask-toggler">Show token</button>
43
+ <button id="token-copier">Copy token</button>
44
+ <span id="token-copier-message"></span>
45
+ </p>
46
+ `;
47
+
48
+ // Mount the banner to the DOM.
49
+ document.querySelector(".information-container").append(sectionEl);
50
+
51
+ // Get references to DOM elements within the banner that was mounted to the DOM.
52
+ const tokenMaskTogglerEl = document.getElementById("token-mask-toggler");
53
+ const tokenEl = document.getElementById("token");
54
+ const tokenCopierEl = document.getElementById("token-copier");
55
+ const tokenCopierMessageEl = document.getElementById("token-copier-message");
56
+
57
+ // Set up the token visibility toggler.
58
+ console.debug("Setting up token visibility toggler");
59
+ tokenMaskTogglerEl.addEventListener("click", (event) => {
60
+ if (tokenEl.dataset.state == "masked") {
61
+ console.debug("Unmasking token");
62
+ tokenEl.dataset.state = "unmasked";
63
+ tokenEl.textContent = accessToken;
64
+ event.target.textContent = "Hide token";
65
+ } else {
66
+ console.debug("Masking token");
67
+ tokenEl.dataset.state = "masked";
68
+ tokenEl.textContent = "***";
69
+ event.target.textContent = "Show token";
70
+ }
71
+ });
72
+
73
+ // Set up the token copier.
74
+ // Reference: https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/writeText
75
+ console.debug("Setting up token copier");
76
+ tokenCopierEl.addEventListener("click", async (event) => {
77
+ tokenCopierMessageEl.textContent = "";
78
+ try {
79
+ await navigator.clipboard.writeText(accessToken);
80
+ tokenCopierMessageEl.innerHTML = "<span class='nmdc-success'>Copied to clipboard</span>";
81
+ } catch (error) {
82
+ console.error(error.message);
83
+ tokenCopierMessageEl.innerHTML = "<span class='nmdc-error'>Copying failed</span>";
84
+ }
85
+ });
86
+ }
87
+
88
+ /**
89
+ * Customizes the login form in the following ways:
90
+ * - Changes the header text of the username/password login form to "User login".
91
+ * - Changes the header text of the client credentials login form to "Site client login".
92
+ * - Augments the "Logout" button on the `bearerAuth` login form so that, when it is clicked,
93
+ * it clears and expires the `user_id_token` cookie, and reloads the web page.
94
+ * - Focuses on the username input field whenever the login form appears.
95
+ * - Adds a "Login with ORCID" widget to the login form.
96
+ *
97
+ * Reference: https://developer.mozilla.org/en-US/docs/Web/API/Document/cookie
98
+ *
99
+ * Prerequisite: The login form must be present in the DOM.
100
+ */
101
+ const customizeLoginForm = () => {
102
+ const modalContentEl = document.querySelector('.auth-wrapper .modal-ux-content');
103
+ const formHeaderEls = modalContentEl.querySelectorAll('.auth-container h4');
104
+ formHeaderEls.forEach(el => {
105
+ switch (el.textContent.trim()) {
106
+ case "OAuth2PasswordOrClientCredentialsBearer (OAuth2, password)":
107
+ console.debug(`Customizing "password" login form header`);
108
+ el.textContent = "User login";
109
+ break;
110
+ case "OAuth2PasswordOrClientCredentialsBearer (OAuth2, clientCredentials)":
111
+ console.debug(`Customizing "clientCredentials" login form header`);
112
+ el.textContent = "Site client login";
113
+ break;
114
+ // Note: This string has a `U+00a0` character before the regular space.
115
+ case "bearerAuth  (http, Bearer)":
116
+ const buttonEls = el.closest(".auth-container").querySelectorAll("button");
117
+ buttonEls.forEach(buttonEl => {
118
+ if (buttonEl.textContent.trim() === "Logout") {
119
+ console.debug(`Augmenting "bearerAuth" form logout button`);
120
+ buttonEl.addEventListener("click", () => {
121
+ console.debug("Clearing and expiring `user_id_token` cookie");
122
+ document.cookie = "user_id_token=; max-age=0; path=/;";
123
+ // Reload the web page so that any in-memory authentication state is reset.
124
+ // Note: If we had full control over the Swagger UI code, we would just
125
+ // manipulate that state directly instead of reloading the page.
126
+ console.debug("Reloading web page");
127
+ window.location.reload();
128
+ });
129
+ }
130
+ });
131
+ break;
132
+ default:
133
+ console.debug(`Unrecognized header: ${el.textContent}`);
134
+ }
135
+ });
136
+ // Add a "Login with ORCID" widget to the login form.
137
+ //
138
+ // TODO: Consider disabling this when the user is already logged in.
139
+ //
140
+ // TODO: Consider moving this up next to (or into) the regular "User login" form,
141
+ // once our system administrators have implemented a practical process for
142
+ // managing "allowances" of users whose usernames are ORCID IDs. Putting it
143
+ // at the bottom of the modal (I think) makes it less likely people will use it.
144
+ //
145
+ console.debug("Adding ORCID Login widget to login form");
146
+ const orcidLoginUrl = document.getElementById("nmdc-orcid-login-url")?.getAttribute("data-url");
147
+ const orcidLoginWidgetEl = document.createElement("div");
148
+ orcidLoginWidgetEl.classList.add("auth-container", "nmdc-orcid-login");
149
+ orcidLoginWidgetEl.innerHTML = `
150
+ <h4>User login with ORCID</h4>
151
+ <div class="nmdc-orcid-login-icon-link">
152
+ <img src="/static/ORCID-iD_icon_vector.svg" height="16" width="16"/>
153
+ <a href="${orcidLoginUrl}">Login with ORCID</a>
154
+ </div>
155
+ `;
156
+ modalContentEl.appendChild(orcidLoginWidgetEl);
157
+
158
+ console.debug("Focusing on username field if present");
159
+ const usernameInputEl = modalContentEl.querySelector("input#oauth_username");
160
+ if (usernameInputEl !== null) {
161
+ usernameInputEl.focus();
162
+ }
163
+ };
164
+ console.debug("Setting up event listener for customizing login form");
165
+ //
166
+ // Listen for a "click" event on the `body` element, check whether the element that was clicked
167
+ // was the "Authorize" button (or one of its descendants), and if so, customize the modal login
168
+ // form that will have been mounted to the DOM by the time the "click" event propagated to the
169
+ // `body` element and our event handler was called.
170
+ //
171
+ // Note: We attach this event listener to the `body` element because that's the lowest-level
172
+ // element where we found that mounting it doesn't cause our event handler to run too early
173
+ // (i.e. doesn't cause it to run _before_ the event handlers that mount the modal login form
174
+ // to the DOM have run). Our event handler needs that form to be mounted so it can access
175
+ // its elements.
176
+ //
177
+ // If we were to attach it to a lower-level element (e.g. directly to the "Authorize" button),
178
+ // we would have to, for example, make its body a `setTimeout(fn, 0)` callback in order to
179
+ // defer its execution until all the event handlers for the "click" even have run.
180
+ //
181
+ bodyEl.addEventListener("click", (event) => {
182
+ // Check whether the clicked element was the "Authorize" button or any of its descendants.
183
+ if (event.target.closest(".auth-wrapper > .btn.authorize:not(.modal-btn)") !== null) {
184
+ customizeLoginForm();
185
+ }
186
+ });
187
+
188
+ // If the `EllipsesButton` class is defined, define a corresponding custom HTML element
189
+ // and set up the tag description details togglers.
190
+ // Docs: https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#using_a_custom_element
191
+ //
192
+ // If the `<ellipses-button>` custom HTML element is available, set up the tag
193
+ // description details togglers.
194
+ //
195
+ // Note: At the time of this writing, all of our tag descriptions begin with a
196
+ // single-paragraph summary of the tag. Some of the tag descriptions have
197
+ // additional paragraphs that provide more _details_ about the tag. In an
198
+ // attempt to keep the Swagger UI page "initially concise" (only showing
199
+ // more information when the user requests it), for the tag descriptions
200
+ // that have additional paragraphs, we add a toggler button that the user
201
+ // can press to toggle the visibility of the additional paragraphs.
202
+ //
203
+ if (typeof EllipsesButton === "function") {
204
+ console.debug("Setting up tag description details togglers");
205
+ customElements.define("ellipses-button", EllipsesButton);
206
+ const tagSectionEls = bodyEl.querySelectorAll(".opblock-tag-section");
207
+ Array.from(tagSectionEls).forEach(el => {
208
+
209
+ // Check whether the description contains more than one element (i.e. paragraph).
210
+ const descriptionEl = el.querySelector("h3 > small > .renderedMarkdown");
211
+ if (descriptionEl.children.length > 1) {
212
+
213
+ // Wrap the additional elements (i.e. paragraphs) in a hidable `<div>`.
214
+ const detailsEl = document.createElement("div");
215
+ detailsEl.classList.add("tag-description-details", "hidden");
216
+ Array.from(descriptionEl.children).slice(1).forEach(el => {
217
+ detailsEl.appendChild(el);
218
+ });
219
+ descriptionEl.replaceChildren(descriptionEl.firstChild, detailsEl);
220
+
221
+ // Add a button that, when clicked, toggles the visibility of the tag
222
+ // description details (but does not propagate the click event upward,
223
+ // so the visibility of the containing tag section isn't toggled).
224
+ const toggleButtonEl = document.createElement("ellipses-button");
225
+ toggleButtonEl.textContent = "Show details"; // populates the "slot"
226
+ descriptionEl.firstChild.appendChild(toggleButtonEl);
227
+ toggleButtonEl.addEventListener("click", (event) => {
228
+ detailsEl.classList.toggle("hidden");
229
+ event.stopPropagation();
230
+
231
+ // Update the button's "is-open" attribute so the button's icon changes.
232
+ if (toggleButtonEl.getAttribute("is-open") === "true") {
233
+ toggleButtonEl.setAttribute("is-open", "false");
234
+ toggleButtonEl.textContent = "Show details";
235
+ } else {
236
+ toggleButtonEl.setAttribute("is-open", "true");
237
+ toggleButtonEl.textContent = "Hide details";
238
+ }
239
+ });
240
+ }
241
+ });
242
+ };
243
+
244
+ // If the `EndpointSearchWidget` class is defined, define a corresponding custom HTML element and add it to the DOM.
245
+ // Docs: https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#using_a_custom_element
246
+ if (typeof EndpointSearchWidget === "function") {
247
+ console.debug("Setting up endpoint search widget");
248
+ customElements.define("endpoint-search-widget", EndpointSearchWidget);
249
+ const endpointSearchWidgetEl = document.createElement("endpoint-search-widget");
250
+ bodyEl.querySelector(".scheme-container").after(endpointSearchWidgetEl); // put it below the "Authorize" section
251
+ }
252
+ });