Line data Source code
1 : import * as React from 'react';
2 :
3 : import * as connection from '@ankoh/dashql-jsonschema/connection.js';
4 : import * as auth from '@ankoh/dashql-jsonschema/auth.js';
5 :
6 : import * as style from './connection_settings.module.css';
7 :
8 : import { BookIcon, KeyIcon, PlugIcon, XIcon } from '../ui/foundations/symbol_icon.js';
9 :
10 : import { Button, ButtonVariant } from '../ui/foundations/button.js';
11 : import { ConnectionHealth } from './connection_state.js';
12 : import { Dispatch } from '../utils/variant.js';
13 : import { KeyValueListBuilder, KeyValueListElement, UpdateKeyValueList } from '../ui/foundations/keyvalue_list.js';
14 : import { TextField, VALIDATION_WARNING } from '../ui/foundations/text_field.js';
15 : import { classNames } from '../utils/classnames.js';
16 : import { useConnectionById, useConnectionDraftForm } from './connection_registry.js';
17 : import { useConnectionSave } from './connection_persistence.js';
18 : import { useConnectionSetup } from './connection_setup.js';
19 : import { useLogger } from '../platform/logger/logger_provider.js';
20 : import { useQueryExecutor } from './query_executor.js';
21 : import { performHealthCheck } from './health_check.js';
22 : import { useTrinoSetup } from './trino/trino_connector.js';
23 : import { CONNECTOR_INFOS, ConnectorType, requiresSwitchingToNative, TRINO_CONNECTOR } from './connector_info.js';
24 : import { UpdateValueList, ValueListBuilder } from '../ui/foundations/value_list.js';
25 : import { ConnectionInlineHeader } from './connection_inline_header.js';
26 : import { SegmentedControl } from '../ui/foundations/segmented_control.js';
27 : import { LoggableException, stringifyError } from '../platform/logger/logger.js';
28 : import { withTrinoCatalogFields } from './trino/trino_catalogs.js';
29 :
30 1 : const LOG_CTX = "trino_connector";
31 :
32 : interface PageState {
33 : activeParams: connection.TrinoConnectionParams | null;
34 : newParams: connection.TrinoConnectionParams;
35 : newParamsMetadata: KeyValueListElement[];
36 : };
37 :
38 1 : const DEFAULT_TRINO_PARAMS: connection.TrinoConnectionParams = {
39 1 : endpoint: "http://localhost:8080",
40 1 : auth: {
41 1 : authType: "AUTH_BASIC",
42 1 : basic: {
43 1 : username: "",
44 1 : secret: "",
45 1 : }
46 1 : },
47 1 : metadata: {
48 1 : message: ""
49 1 : },
50 1 : defaultCatalog: "",
51 1 : defaultSchema: "",
52 1 : catalogs: [],
53 1 : };
54 :
55 1 : export function buildPageStateFromParams(params: connection.TrinoConnectionParams | null | undefined): PageState {
56 1 : const active = params ?? null;
57 1 : const newParams = params ? withTrinoCatalogFields(params) : DEFAULT_TRINO_PARAMS;
58 1 : const metadataData = newParams.metadata ?? {};
59 1 : return {
60 1 : activeParams: active,
61 1 : newParams,
62 1 : newParamsMetadata: params?.draftRows?.metadata ?? Object.entries(metadataData).map(([k, v]) => ({ key: k, value: v ?? "" })),
63 1 : };
64 1 : }
65 :
66 : interface Props {
67 : connectionId: string | null;
68 : onClose?: () => void;
69 : }
70 :
71 1 : export const TrinoConnectorSettings: React.FC<Props> = (props: Props) => {
72 1 : const logger = useLogger();
73 1 : const trinoSetup = useTrinoSetup();
74 1 : const queryExecutor = useQueryExecutor();
75 :
76 : // Can we use the connector here?
77 1 : const connectorInfo = CONNECTOR_INFOS[ConnectorType.TRINO];
78 1 : const wrongPlatform = requiresSwitchingToNative(connectorInfo);
79 :
80 : // Resolve connection state
81 1 : const [connectionState, dispatchConnectionState] = useConnectionById(props.connectionId);
82 1 : const connectionSetup = useConnectionSetup(props.connectionId);
83 1 : const { saving } = useConnectionSave(props.connectionId);
84 :
85 1 : const [pageState, setPageState] = useConnectionDraftForm(props.connectionId,
86 1 : params => buildPageStateFromParams(params && 'trino' in params ? params.trino as connection.TrinoConnectionParams : undefined),
87 1 : form => ({ trino: { ...form.newParams, draftRows: { metadata: form.newParamsMetadata } } }));
88 :
89 1 : const endpoint = pageState.newParams?.endpoint;
90 1 : const setEndpoint = (v: string) => setPageState(s => ({
91 1 : ...s,
92 1 : newParams: {
93 1 : ...s.newParams,
94 1 : endpoint: v
95 1 : },
96 1 : }));
97 1 : const authType = pageState.newParams?.auth?.authType ?? "AUTH_BASIC";
98 1 : const setAuthType = (t: auth.AuthType) => setPageState(s => ({
99 1 : ...s,
100 1 : newParams: {
101 1 : ...s.newParams,
102 1 : auth: {
103 1 : authType: t,
104 1 : },
105 1 : },
106 1 : }));
107 1 : const basicAuthUsername = pageState.newParams?.auth?.basic?.username;
108 1 : const setBasicAuthUsername: Dispatch<string> = (v: string) => setPageState(s => ({
109 1 : ...s,
110 1 : newParams: {
111 1 : ...s.newParams,
112 1 : auth: {
113 1 : authType: "AUTH_BASIC",
114 1 : basic: {
115 1 : username: v,
116 1 : secret: s.newParams.auth?.basic?.secret ?? "",
117 1 : },
118 1 : },
119 1 : }
120 1 : }));
121 1 : const basicAuthSecret = pageState.newParams?.auth?.basic?.secret;
122 1 : const setBasicAuthSecret: Dispatch<string> = (v: string) => setPageState(s => ({
123 1 : ...s,
124 1 : newParams: {
125 1 : ...s.newParams,
126 1 : auth: {
127 1 : authType: "AUTH_BASIC",
128 1 : basic: {
129 1 : username: s.newParams?.auth?.basic?.username ?? "",
130 1 : secret: v,
131 1 : },
132 1 : },
133 1 : }
134 1 : }));
135 1 : const oauthAuthEndpoint = pageState.newParams.auth?.oauth?.authorizationUrl;
136 1 : const setOAuthAuthEndpoint: Dispatch<string> = (v: string) => setPageState(s => ({
137 1 : ...s,
138 1 : newParams: {
139 1 : ...s.newParams,
140 1 : auth: {
141 1 : authType: "AUTH_OAUTH",
142 1 : oauth: {
143 1 : authorizationUrl: v,
144 1 : tokenUrl: s.newParams?.auth?.oauth?.tokenUrl ?? "",
145 1 : clientId: s.newParams?.auth?.oauth?.clientId ?? "",
146 1 : callbackUrl: s.newParams?.auth?.oauth?.callbackUrl ?? "",
147 1 : scopes: s.newParams?.auth?.oauth?.scopes ?? [],
148 1 : },
149 1 : },
150 1 : }
151 1 : }));
152 :
153 1 : const oauthTokenEndpoint = pageState.newParams.auth?.oauth?.tokenUrl;
154 1 : const setOAuthTokenEndpoint: Dispatch<string> = (v: string) => setPageState(s => ({
155 1 : ...s,
156 1 : newParams: {
157 1 : ...s.newParams,
158 1 : auth: {
159 1 : authType: "AUTH_OAUTH",
160 1 : oauth: {
161 1 : authorizationUrl: s.newParams?.auth?.oauth?.authorizationUrl ?? "",
162 1 : tokenUrl: v,
163 1 : clientId: s.newParams?.auth?.oauth?.clientId ?? "",
164 1 : callbackUrl: s.newParams?.auth?.oauth?.callbackUrl ?? "",
165 1 : scopes: s.newParams?.auth?.oauth?.scopes ?? [],
166 1 : },
167 1 : },
168 1 : }
169 1 : }));
170 :
171 1 : const oauthClientId = pageState.newParams.auth?.oauth?.clientId;
172 1 : const setOAuthClientId: Dispatch<string> = (v: string) => setPageState(s => ({
173 1 : ...s,
174 1 : newParams: {
175 1 : ...s.newParams,
176 1 : auth: {
177 1 : authType: "AUTH_OAUTH",
178 1 : oauth: {
179 1 : authorizationUrl: s.newParams?.auth?.oauth?.authorizationUrl ?? "",
180 1 : tokenUrl: s.newParams?.auth?.oauth?.tokenUrl ?? "",
181 1 : clientId: v,
182 1 : callbackUrl: s.newParams?.auth?.oauth?.callbackUrl ?? "",
183 1 : scopes: s.newParams?.auth?.oauth?.scopes ?? [],
184 1 : },
185 1 : },
186 1 : }
187 1 : }));
188 :
189 1 : const oauthRedirectUrl = pageState.newParams.auth?.oauth?.callbackUrl;
190 1 : const setOAuthRedirectUrl: Dispatch<string> = (v: string) => setPageState(s => ({
191 1 : ...s,
192 1 : newParams: {
193 1 : ...s.newParams,
194 1 : auth: {
195 1 : authType: "AUTH_OAUTH",
196 1 : oauth: {
197 1 : authorizationUrl: s.newParams?.auth?.oauth?.authorizationUrl ?? "",
198 1 : tokenUrl: s.newParams?.auth?.oauth?.tokenUrl ?? "",
199 1 : clientId: s.newParams?.auth?.oauth?.clientId ?? "",
200 1 : callbackUrl: v,
201 1 : scopes: s.newParams?.auth?.oauth?.scopes ?? [],
202 1 : },
203 1 : },
204 1 : }
205 1 : }));
206 :
207 1 : const oauthScopes = pageState.newParams.auth?.oauth?.scopes ?? [];
208 1 : const modifyOAuthScopes: Dispatch<UpdateValueList> = (action: UpdateValueList) => setPageState(s => ({
209 1 : ...s,
210 1 : newParams: {
211 1 : ...s.newParams,
212 1 : auth: {
213 1 : authType: "AUTH_OAUTH",
214 1 : oauth: {
215 1 : authorizationUrl: s.newParams?.auth?.oauth?.authorizationUrl ?? "",
216 1 : tokenUrl: s.newParams?.auth?.oauth?.tokenUrl ?? "",
217 1 : clientId: s.newParams?.auth?.oauth?.clientId ?? "",
218 1 : callbackUrl: s.newParams?.auth?.oauth?.callbackUrl ?? "",
219 1 : scopes: action(s.newParams?.auth?.oauth?.scopes ?? []),
220 1 : },
221 1 : },
222 1 : }
223 1 : }));
224 :
225 :
226 1 : const defaultCatalog = pageState.newParams.defaultCatalog ?? "";
227 1 : const setDefaultCatalog: Dispatch<string> = (v: string) => setPageState(s => ({
228 1 : ...s,
229 1 : newParams: {
230 1 : ...s.newParams,
231 1 : defaultCatalog: v
232 1 : }
233 1 : }));
234 1 : const defaultSchema = pageState.newParams.defaultSchema ?? "";
235 1 : const setDefaultSchema: Dispatch<string> = (v: string) => setPageState(s => ({
236 1 : ...s,
237 1 : newParams: {
238 1 : ...s.newParams,
239 1 : defaultSchema: v
240 1 : }
241 1 : }));
242 1 : const catalogs = pageState.newParams.catalogs ?? [];
243 1 : const modifyCatalogs: Dispatch<UpdateValueList> = (action: UpdateValueList) => setPageState(s => ({
244 1 : ...s,
245 1 : newParams: {
246 1 : ...s.newParams,
247 1 : catalogs: action(s.newParams.catalogs ?? [])
248 1 : }
249 1 : }));
250 1 : const modifyMetadata: Dispatch<UpdateKeyValueList> = (action: UpdateKeyValueList) => setPageState(s => {
251 1 : const metadata = action(s.newParamsMetadata);
252 :
253 : // Flatten the key-value list eagerly for the new params.
254 : // We could decide to do this lazily but it shouldn't really matter.
255 1 : const metadataObj: Record<string, string> = {};
256 1 : for (const entry of metadata) {
257 1 : metadataObj[entry.key] = entry.value;
258 : }
259 1 : return {
260 1 : ...s,
261 1 : newParamsMetadata: metadata,
262 1 : newParams: {
263 1 : ...s.newParams,
264 1 : metadata: metadataObj
265 1 : }
266 1 : };
267 1 : });
268 :
269 : // Helper to setup the connection
270 1 : const setupConnection = async () => {
271 : // Is there a Trino client?
272 1 : if (trinoSetup == null) {
273 1 : logger.error("Trino connector is unavailable", {}, LOG_CTX);
274 1 : return;
275 1 : }
276 : // Is there a connection id?
277 1 : if (connectionState == null) {
278 0 : logger.warn("Trino connection state is null", {}, LOG_CTX);
279 0 : return;
280 0 : }
281 :
282 1 : await connectionSetup.run(async signal => {
283 1 : try {
284 1 : const connectionParams: connection.TrinoConnectionParams = pageState.newParams;
285 1 : const trinoChannel = await trinoSetup.setup(dispatchConnectionState, connectionParams, signal);
286 1 : if (trinoChannel != null) {
287 1 : await performHealthCheck(queryExecutor, connectionState.connectionId, { type: 'trino', channel: trinoChannel }, dispatchConnectionState, signal);
288 : }
289 1 : } catch (error: any) {
290 1 : if (error instanceof LoggableException) {
291 0 : logger.warn(error.message, error.keyValues, error.target);
292 : } else {
293 1 : logger.warn("Error while setting up trino connection", {
294 1 : authType: (pageState.newParams.auth?.authType ?? 0).toString(),
295 1 : error: stringifyError(error)
296 1 : }, LOG_CTX);
297 : }
298 1 : }
299 1 : });
300 1 : };
301 1 : const cancelSetup = connectionSetup.cancel;
302 1 : const resetSetup = async () => {
303 1 : if (trinoSetup) {
304 1 : await trinoSetup.reset(dispatchConnectionState);
305 : }
306 1 : };
307 :
308 : // Get the action button
309 : let connectButton: React.ReactElement = <div />;
310 : // Saving writes the settings into every session that uses the connection
311 1 : let freezeInput = saving;
312 1 : switch (connectionState?.connectionHealth) {
313 1 : case ConnectionHealth.NOT_STARTED:
314 1 : case ConnectionHealth.FAILED:
315 1 : connectButton = <Button variant={ButtonVariant.Primary} leadingVisual={PlugIcon} onClick={setupConnection}>Connect</Button>;
316 1 : break;
317 1 : case ConnectionHealth.CONNECTING:
318 1 : connectButton = <Button variant={ButtonVariant.Danger} leadingVisual={XIcon} onClick={cancelSetup}>Cancel</Button>;
319 1 : freezeInput = true;
320 1 : break;
321 1 : case ConnectionHealth.ONLINE:
322 1 : connectButton = <Button variant={ButtonVariant.Danger} leadingVisual={XIcon} onClick={resetSetup}>Disconnect</Button>;
323 : break;
324 1 : }
325 :
326 1 : return (
327 1 : <div className={style.layout}>
328 1 : <ConnectionInlineHeader
329 1 : connector={connectorInfo}
330 1 : connection={connectionState}
331 1 : wrongPlatform={wrongPlatform}
332 1 : setupConnection={setupConnection}
333 1 : cancelSetup={cancelSetup}
334 1 : resetSetup={resetSetup}
335 1 : onClose={props.onClose}
336 1 : />
337 1 : <div className={style.body_container}>
338 1 : <div className={style.section}>
339 1 : <div className={classNames(style.section_layout, style.body_section_layout)}>
340 1 : <TextField
341 1 : name="Endpoint"
342 1 : caption="Endpoint of the Trino Api as 'https://host:port'"
343 1 : value={endpoint}
344 1 : placeholder=""
345 1 : validation={
346 1 : (endpoint.length ?? 0) == 0
347 1 : ? { type: VALIDATION_WARNING, value: "Endpoint is empty" }
348 1 : : undefined
349 : }
350 1 : leadingVisual={() => <div>URL</div>}
351 1 : onChange={(e) => setEndpoint(e.target.value)}
352 1 : disabled={freezeInput}
353 1 : readOnly={freezeInput}
354 1 : autoComplete={false}
355 1 : logContext={LOG_CTX}
356 1 : />
357 1 : </div>
358 1 : </div>
359 1 : <div className={style.section}>
360 1 : <div className={classNames(style.section_layout, style.body_section_layout)}>
361 1 : <div className={style.section_header}>
362 1 : <SegmentedControl
363 1 : aria-label="Authentication type"
364 1 : onChange={(index) => {
365 1 : const newAuthType = index === 0
366 1 : ? "AUTH_BASIC"
367 1 : : "AUTH_OAUTH";
368 : // The control also reports a click on the selected type, which must keep the credentials
369 1 : if (newAuthType !== authType) {
370 1 : setAuthType(newAuthType);
371 : }
372 1 : }}
373 : >
374 1 : <SegmentedControl.Button
375 1 : selected={authType === "AUTH_BASIC"}
376 1 : disabled={freezeInput}
377 1 : >
378 : Basic
379 1 : </SegmentedControl.Button>
380 1 : <SegmentedControl.Button
381 1 : selected={authType === "AUTH_OAUTH"}
382 1 : disabled={freezeInput}
383 1 : >
384 : OAuth 2.0
385 1 : </SegmentedControl.Button>
386 1 : </SegmentedControl>
387 1 : </div>
388 1 : <TextField
389 1 : style={{ display: authType == "AUTH_BASIC" ? 'block' : 'none' }}
390 1 : name="Username"
391 1 : caption="Username for the Trino Api"
392 1 : placeholder=""
393 1 : validation={
394 1 : (basicAuthUsername?.length ?? 0) == 0
395 1 : ? { type: VALIDATION_WARNING, value: "Username is empty" }
396 1 : : undefined
397 : }
398 1 : leadingVisual={() => <div>ID</div>}
399 1 : value={basicAuthUsername ?? ""}
400 1 : onChange={(e) => setBasicAuthUsername(e.target.value)}
401 1 : disabled={freezeInput}
402 1 : readOnly={freezeInput}
403 1 : autoComplete={false}
404 1 : logContext={LOG_CTX}
405 1 : />
406 1 : <TextField
407 1 : style={{ display: authType == "AUTH_BASIC" ? 'block' : 'none' }}
408 1 : name="Secret"
409 1 : caption="Password for the Trino Api"
410 1 : placeholder=""
411 1 : validation={
412 1 : (basicAuthSecret?.length ?? 0) == 0
413 1 : ? { type: VALIDATION_WARNING, value: "Secret is empty" }
414 1 : : undefined
415 : }
416 1 : leadingVisual={KeyIcon}
417 1 : value={basicAuthSecret ?? ""}
418 1 : onChange={(e) => setBasicAuthSecret(e.target.value)}
419 1 : disabled={freezeInput}
420 1 : readOnly={freezeInput}
421 1 : concealed={true}
422 1 : logContext={LOG_CTX}
423 1 : />
424 1 : <TextField
425 1 : style={{ display: authType == "AUTH_OAUTH" ? 'block' : 'none' }}
426 1 : name="Authorization Url"
427 1 : caption="Url to start the OAuth flow"
428 1 : placeholder=""
429 1 : validation={
430 1 : (pageState.newParams.auth?.oauth?.authorizationUrl.length ?? 0) == 0
431 1 : ? { type: VALIDATION_WARNING, value: "Authorization Url is empty" }
432 1 : : undefined
433 : }
434 1 : leadingVisual={() => <div>URL</div>}
435 1 : value={oauthAuthEndpoint ?? ""}
436 1 : onChange={(e) => setOAuthAuthEndpoint(e.target.value)}
437 1 : disabled={freezeInput}
438 1 : readOnly={freezeInput}
439 1 : autoComplete={false}
440 1 : logContext={LOG_CTX}
441 1 : />
442 1 : <TextField
443 1 : style={{ display: authType == "AUTH_OAUTH" ? 'block' : 'none' }}
444 1 : name="Token Url"
445 1 : caption="Url to retrieve an Access Token"
446 1 : placeholder=""
447 1 : validation={
448 1 : (pageState.newParams.auth?.oauth?.tokenUrl.length ?? 0) == 0
449 1 : ? { type: VALIDATION_WARNING, value: "Token Endpoint is empty" }
450 1 : : undefined
451 : }
452 1 : leadingVisual={() => <div>URL</div>}
453 1 : value={oauthTokenEndpoint ?? ""}
454 1 : onChange={(e) => setOAuthTokenEndpoint(e.target.value)}
455 1 : disabled={freezeInput}
456 1 : readOnly={freezeInput}
457 1 : autoComplete={false}
458 1 : logContext={LOG_CTX}
459 1 : />
460 1 : <TextField
461 1 : style={{ display: authType == "AUTH_OAUTH" ? 'block' : 'none' }}
462 1 : name="Client ID"
463 1 : caption="Client ID of the OAuth application"
464 1 : placeholder=""
465 1 : validation={
466 1 : (pageState.newParams.auth?.oauth?.clientId?.length ?? 0) == 0
467 1 : ? { type: VALIDATION_WARNING, value: "Client ID is empty" }
468 1 : : undefined
469 : }
470 1 : leadingVisual={() => <div>ID</div>}
471 1 : value={oauthClientId ?? ""}
472 1 : onChange={(e) => setOAuthClientId(e.target.value)}
473 1 : disabled={freezeInput}
474 1 : readOnly={freezeInput}
475 1 : autoComplete={false}
476 1 : logContext={LOG_CTX}
477 1 : />
478 1 : <TextField
479 1 : style={{ display: authType == "AUTH_OAUTH" ? 'block' : 'none' }}
480 1 : name="Redirect URL"
481 1 : caption="Redirect URL of the OAuth application"
482 1 : placeholder=""
483 1 : validation={
484 1 : (pageState.newParams.auth?.oauth?.callbackUrl?.length ?? 0) == 0
485 1 : ? { type: VALIDATION_WARNING, value: "Redirect URL is empty" }
486 1 : : undefined
487 : }
488 1 : leadingVisual={() => <div>URL</div>}
489 1 : value={oauthRedirectUrl ?? ""}
490 1 : onChange={(e) => setOAuthRedirectUrl(e.target.value)}
491 1 : disabled={freezeInput}
492 1 : readOnly={freezeInput}
493 1 : autoComplete={false}
494 1 : logContext={LOG_CTX}
495 1 : />
496 1 : <ValueListBuilder
497 1 : title="Scope"
498 1 : caption="OAuth scopes"
499 1 : valueIcon={() => <div>Scope</div>}
500 1 : addButtonLabel="Add Scope"
501 1 : elements={oauthScopes}
502 1 : modifyElements={modifyOAuthScopes}
503 1 : disabled={freezeInput}
504 1 : readOnly={freezeInput}
505 1 : />
506 1 : </div>
507 1 : </div>
508 1 : <div className={style.section}>
509 1 : <div className={classNames(style.section_layout, style.body_section_layout)}>
510 1 : <ValueListBuilder
511 1 : title="Catalogs"
512 1 : caption="Relations to load, 'catalog' for all schemas or 'catalog.schema' for one. Without entries the default catalog is loaded."
513 1 : valueIcon={BookIcon}
514 1 : addButtonLabel="Add Catalog"
515 1 : elements={catalogs}
516 1 : modifyElements={modifyCatalogs}
517 1 : disabled={freezeInput}
518 1 : readOnly={freezeInput}
519 1 : />
520 1 : </div>
521 1 : </div>
522 1 : <div className={style.section}>
523 1 : <div className={classNames(style.section_layout, style.body_section_layout)}>
524 1 : <TextField
525 1 : name="Default Catalog"
526 1 : caption="Catalog of the names that statements leave out"
527 1 : value={defaultCatalog}
528 1 : placeholder=""
529 1 : leadingVisual={BookIcon}
530 1 : onChange={(e) => setDefaultCatalog(e.target.value)}
531 1 : disabled={freezeInput}
532 1 : readOnly={freezeInput}
533 1 : autoComplete={false}
534 1 : logContext={LOG_CTX}
535 1 : />
536 1 : <TextField
537 1 : name="Default Schema"
538 1 : caption="Schema of the names that statements leave out"
539 1 : value={defaultSchema}
540 1 : placeholder=""
541 1 : validation={
542 1 : defaultSchema.trim().length > 0 && defaultCatalog.trim().length == 0
543 1 : ? { type: VALIDATION_WARNING, value: "A default schema needs a default catalog" }
544 1 : : undefined
545 : }
546 1 : leadingVisual={BookIcon}
547 1 : onChange={(e) => setDefaultSchema(e.target.value)}
548 1 : disabled={freezeInput}
549 1 : readOnly={freezeInput}
550 1 : autoComplete={false}
551 1 : logContext={LOG_CTX}
552 1 : />
553 1 : </div>
554 1 : </div>
555 1 : <div className={style.section}>
556 1 : <div className={classNames(style.section_layout, style.body_section_layout)}>
557 1 : <KeyValueListBuilder
558 1 : title="Additional Metadata"
559 1 : caption="Extra HTTP headers that are added to each request"
560 1 : keyIcon={() => <div>Header</div>}
561 1 : valueIcon={() => <div>Value</div>}
562 1 : addButtonLabel="Add Header"
563 1 : elements={pageState.newParamsMetadata}
564 1 : modifyElements={modifyMetadata}
565 1 : disabled={freezeInput}
566 1 : readOnly={freezeInput}
567 1 : />
568 1 : </div>
569 1 : </div>
570 1 : </div>
571 1 : </ div>
572 : );
573 1 : };
|