nmdc-runtime 2.6.0__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.
- nmdc_runtime/Dockerfile +177 -0
- nmdc_runtime/api/analytics.py +90 -0
- nmdc_runtime/api/boot/capabilities.py +9 -0
- nmdc_runtime/api/boot/object_types.py +126 -0
- nmdc_runtime/api/boot/triggers.py +84 -0
- nmdc_runtime/api/boot/workflows.py +116 -0
- nmdc_runtime/api/core/auth.py +212 -0
- nmdc_runtime/api/core/idgen.py +200 -0
- nmdc_runtime/api/core/metadata.py +777 -0
- nmdc_runtime/api/core/util.py +114 -0
- nmdc_runtime/api/db/mongo.py +436 -0
- nmdc_runtime/api/db/s3.py +37 -0
- nmdc_runtime/api/endpoints/capabilities.py +25 -0
- nmdc_runtime/api/endpoints/find.py +634 -0
- nmdc_runtime/api/endpoints/jobs.py +206 -0
- nmdc_runtime/api/endpoints/lib/helpers.py +274 -0
- nmdc_runtime/api/endpoints/lib/linked_instances.py +193 -0
- nmdc_runtime/api/endpoints/lib/path_segments.py +165 -0
- nmdc_runtime/api/endpoints/metadata.py +260 -0
- nmdc_runtime/api/endpoints/nmdcschema.py +515 -0
- nmdc_runtime/api/endpoints/object_types.py +38 -0
- nmdc_runtime/api/endpoints/objects.py +277 -0
- nmdc_runtime/api/endpoints/operations.py +78 -0
- nmdc_runtime/api/endpoints/queries.py +701 -0
- nmdc_runtime/api/endpoints/runs.py +98 -0
- nmdc_runtime/api/endpoints/search.py +38 -0
- nmdc_runtime/api/endpoints/sites.py +205 -0
- nmdc_runtime/api/endpoints/triggers.py +25 -0
- nmdc_runtime/api/endpoints/users.py +214 -0
- nmdc_runtime/api/endpoints/util.py +817 -0
- nmdc_runtime/api/endpoints/wf_file_staging.py +307 -0
- nmdc_runtime/api/endpoints/workflows.py +353 -0
- nmdc_runtime/api/entrypoint.sh +7 -0
- nmdc_runtime/api/main.py +495 -0
- nmdc_runtime/api/middleware.py +43 -0
- nmdc_runtime/api/models/capability.py +14 -0
- nmdc_runtime/api/models/id.py +92 -0
- nmdc_runtime/api/models/job.py +57 -0
- nmdc_runtime/api/models/lib/helpers.py +78 -0
- nmdc_runtime/api/models/metadata.py +11 -0
- nmdc_runtime/api/models/nmdc_schema.py +146 -0
- nmdc_runtime/api/models/object.py +180 -0
- nmdc_runtime/api/models/object_type.py +20 -0
- nmdc_runtime/api/models/operation.py +66 -0
- nmdc_runtime/api/models/query.py +246 -0
- nmdc_runtime/api/models/query_continuation.py +111 -0
- nmdc_runtime/api/models/run.py +161 -0
- nmdc_runtime/api/models/site.py +87 -0
- nmdc_runtime/api/models/trigger.py +13 -0
- nmdc_runtime/api/models/user.py +207 -0
- nmdc_runtime/api/models/util.py +260 -0
- nmdc_runtime/api/models/wfe_file_stages.py +122 -0
- nmdc_runtime/api/models/workflow.py +15 -0
- nmdc_runtime/api/openapi.py +178 -0
- nmdc_runtime/api/swagger_ui/assets/EllipsesButton.js +146 -0
- nmdc_runtime/api/swagger_ui/assets/EndpointSearchWidget.js +369 -0
- nmdc_runtime/api/swagger_ui/assets/script.js +252 -0
- nmdc_runtime/api/swagger_ui/assets/style.css +155 -0
- nmdc_runtime/api/swagger_ui/swagger_ui.py +34 -0
- nmdc_runtime/config.py +56 -1
- nmdc_runtime/minter/adapters/repository.py +22 -2
- nmdc_runtime/minter/config.py +2 -0
- nmdc_runtime/minter/domain/model.py +55 -1
- nmdc_runtime/minter/entrypoints/fastapi_app.py +1 -1
- nmdc_runtime/mongo_util.py +89 -0
- nmdc_runtime/site/backup/nmdcdb_mongodump.py +1 -1
- nmdc_runtime/site/backup/nmdcdb_mongoexport.py +1 -3
- nmdc_runtime/site/changesheets/data/OmicsProcessing-to-catted-Biosamples.tsv +1561 -0
- nmdc_runtime/site/changesheets/scripts/missing_neon_soils_ecosystem_data.py +311 -0
- nmdc_runtime/site/changesheets/scripts/neon_soils_add_ncbi_ids.py +210 -0
- nmdc_runtime/site/dagster.yaml +53 -0
- nmdc_runtime/site/entrypoint-daemon.sh +29 -0
- nmdc_runtime/site/entrypoint-dagit-readonly.sh +26 -0
- nmdc_runtime/site/entrypoint-dagit.sh +29 -0
- nmdc_runtime/site/export/ncbi_xml.py +731 -40
- nmdc_runtime/site/export/ncbi_xml_utils.py +142 -26
- nmdc_runtime/site/graphs.py +80 -29
- nmdc_runtime/site/ops.py +522 -183
- nmdc_runtime/site/repair/database_updater.py +210 -1
- nmdc_runtime/site/repository.py +108 -117
- nmdc_runtime/site/resources.py +72 -36
- nmdc_runtime/site/translation/gold_translator.py +22 -21
- nmdc_runtime/site/translation/neon_benthic_translator.py +1 -1
- nmdc_runtime/site/translation/neon_soil_translator.py +5 -5
- nmdc_runtime/site/translation/neon_surface_water_translator.py +1 -2
- nmdc_runtime/site/translation/submission_portal_translator.py +216 -69
- nmdc_runtime/site/translation/translator.py +64 -1
- nmdc_runtime/site/util.py +8 -3
- nmdc_runtime/site/validation/util.py +16 -12
- nmdc_runtime/site/workspace.yaml +13 -0
- nmdc_runtime/static/NMDC_logo.svg +1073 -0
- nmdc_runtime/static/ORCID-iD_icon_vector.svg +4 -0
- nmdc_runtime/static/README.md +5 -0
- nmdc_runtime/static/favicon.ico +0 -0
- nmdc_runtime/util.py +175 -348
- nmdc_runtime-2.12.0.dist-info/METADATA +45 -0
- nmdc_runtime-2.12.0.dist-info/RECORD +131 -0
- {nmdc_runtime-2.6.0.dist-info → nmdc_runtime-2.12.0.dist-info}/WHEEL +1 -2
- nmdc_runtime/containers.py +0 -14
- nmdc_runtime/core/db/Database.py +0 -15
- nmdc_runtime/core/exceptions/__init__.py +0 -23
- nmdc_runtime/core/exceptions/base.py +0 -47
- nmdc_runtime/core/exceptions/token.py +0 -13
- nmdc_runtime/domain/users/queriesInterface.py +0 -18
- nmdc_runtime/domain/users/userSchema.py +0 -37
- nmdc_runtime/domain/users/userService.py +0 -14
- nmdc_runtime/infrastructure/database/db.py +0 -3
- nmdc_runtime/infrastructure/database/models/user.py +0 -10
- nmdc_runtime/lib/__init__.py +0 -1
- nmdc_runtime/lib/extract_nmdc_data.py +0 -41
- nmdc_runtime/lib/load_nmdc_data.py +0 -121
- nmdc_runtime/lib/nmdc_dataframes.py +0 -829
- nmdc_runtime/lib/nmdc_etl_class.py +0 -402
- nmdc_runtime/lib/transform_nmdc_data.py +0 -1117
- nmdc_runtime/site/drsobjects/ingest.py +0 -93
- nmdc_runtime/site/drsobjects/registration.py +0 -131
- nmdc_runtime/site/translation/emsl.py +0 -43
- nmdc_runtime/site/translation/gold.py +0 -53
- nmdc_runtime/site/translation/jgi.py +0 -32
- nmdc_runtime/site/translation/util.py +0 -132
- nmdc_runtime/site/validation/jgi.py +0 -43
- nmdc_runtime-2.6.0.dist-info/METADATA +0 -199
- nmdc_runtime-2.6.0.dist-info/RECORD +0 -83
- nmdc_runtime-2.6.0.dist-info/top_level.txt +0 -1
- /nmdc_runtime/{client → api}/__init__.py +0 -0
- /nmdc_runtime/{core → api/boot}/__init__.py +0 -0
- /nmdc_runtime/{core/db → api/core}/__init__.py +0 -0
- /nmdc_runtime/{domain → api/db}/__init__.py +0 -0
- /nmdc_runtime/{domain/users → api/endpoints}/__init__.py +0 -0
- /nmdc_runtime/{infrastructure → api/endpoints/lib}/__init__.py +0 -0
- /nmdc_runtime/{infrastructure/database → api/models}/__init__.py +0 -0
- /nmdc_runtime/{infrastructure/database/models → api/models/lib}/__init__.py +0 -0
- /nmdc_runtime/{site/drsobjects/__init__.py → api/models/minter.py} +0 -0
- {nmdc_runtime-2.6.0.dist-info → nmdc_runtime-2.12.0.dist-info}/entry_points.txt +0 -0
- {nmdc_runtime-2.6.0.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
|
+
});
|