@stoplight/elements-core 7.7.21 → 7.9.0

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.
@@ -5,4 +5,8 @@ interface ServerInfoProps {
5
5
  mockUrl?: string;
6
6
  }
7
7
  export declare const ServerInfo: React.FC<ServerInfoProps>;
8
+ export declare function useSplitUrl(url: string): {
9
+ kind: 'variable' | 'static';
10
+ value: string;
11
+ }[];
8
12
  export {};
@@ -0,0 +1,9 @@
1
+ import * as React from 'react';
2
+ import { ServerVariable } from '../../../utils/http-spec/IServer';
3
+ interface ServerVariablesProps<P extends keyof any = string> {
4
+ variables: readonly ServerVariable[];
5
+ values: Record<P, string>;
6
+ onChangeValue: (variableName: P, newValue: string) => void;
7
+ }
8
+ export declare const ServerVariables: React.FC<ServerVariablesProps>;
9
+ export {};
@@ -0,0 +1,11 @@
1
+ import { SelectProps } from '@stoplight/mosaic';
2
+ import * as React from 'react';
3
+ import { ServerVariable } from '../../../utils/http-spec/IServer';
4
+ interface VariableProps {
5
+ variable: ServerVariable;
6
+ value?: string;
7
+ onChange: SelectProps['onChange'];
8
+ validate?: boolean;
9
+ }
10
+ export declare const VariableEditor: React.FC<VariableProps>;
11
+ export {};
@@ -0,0 +1,4 @@
1
+ export declare const useServerVariables: () => {
2
+ serverVariables: {};
3
+ updateServerVariableValue: (name: string, value: string) => void;
4
+ };
@@ -4,6 +4,7 @@ declare const _default: Meta<TryItProps>;
4
4
  export default _default;
5
5
  export declare const SimpleGET: Story<TryItProps>;
6
6
  export declare const WithParameters: Story<TryItProps>;
7
+ export declare const WithVariables: Story<TryItProps>;
7
8
  export declare const UrlEncoded: Story<TryItProps>;
8
9
  export declare const Multipart: Story<TryItProps>;
9
10
  export declare const RequestBodySchema: Story<TryItProps>;
@@ -3,3 +3,4 @@ import { TryItWithRequestSamplesProps } from './TryItWithRequestSamples';
3
3
  declare const _default: Meta<TryItWithRequestSamplesProps>;
4
4
  export default _default;
5
5
  export declare const WithParameters: Story<TryItWithRequestSamplesProps>;
6
+ export declare const WithVariables: Story<TryItWithRequestSamplesProps>;
@@ -7,6 +7,7 @@ interface BuildRequestInput {
7
7
  httpOperation: IHttpOperation;
8
8
  mediaTypeContent: IMediaTypeContent | undefined;
9
9
  parameterValues: Dictionary<string, string>;
10
+ serverVariableValues: Dictionary<string, string>;
10
11
  bodyInput?: BodyParameterValues | string;
11
12
  mockData?: MockData;
12
13
  auth?: HttpSecuritySchemeWithValues;
@@ -18,7 +19,7 @@ export declare const getQueryParams: ({ httpOperation, parameterValues, }: Pick<
18
19
  name: string;
19
20
  value: string;
20
21
  }[];
21
- export declare function buildFetchRequest({ httpOperation, mediaTypeContent, bodyInput, parameterValues, mockData, auth, chosenServer, credentials, corsProxy, }: BuildRequestInput): Promise<Parameters<typeof fetch>>;
22
- export declare function buildHarRequest({ httpOperation, bodyInput, parameterValues, mediaTypeContent, auth, mockData, chosenServer, corsProxy, }: BuildRequestInput): Promise<HarRequest>;
22
+ export declare function buildFetchRequest({ httpOperation, mediaTypeContent, bodyInput, parameterValues, serverVariableValues, mockData, auth, chosenServer, credentials, corsProxy, }: BuildRequestInput): Promise<Parameters<typeof fetch>>;
23
+ export declare function buildHarRequest({ httpOperation, bodyInput, parameterValues, serverVariableValues, mediaTypeContent, auth, mockData, chosenServer, corsProxy, }: BuildRequestInput): Promise<HarRequest>;
23
24
  export declare function getAcceptedMimeTypes(httpOperation: IHttpOperation): string[];
24
25
  export {};
package/index.esm.js CHANGED
@@ -41,6 +41,9 @@ import entries from 'lodash/entries.js';
41
41
  import keys from 'lodash/keys.js';
42
42
  import { JsonSchemaViewer } from '@stoplight/json-schema-viewer';
43
43
  import sortBy from 'lodash/sortBy.js';
44
+ import isEmpty from 'lodash/isEmpty.js';
45
+ import isNil from 'lodash/isNil.js';
46
+ import omitBy from 'lodash/omitBy.js';
44
47
  import { HashLink } from 'react-router-hash-link';
45
48
  import { QueryClient, useQueryClient, QueryClientProvider } from 'react-query';
46
49
  import $RefParser from '@stoplight/json-schema-ref-parser';
@@ -486,15 +489,17 @@ function useChosenServerUrl(chosenServerUrl) {
486
489
  const chosenServerAtom = atom(undefined);
487
490
 
488
491
  function isValidServer(server) {
489
- return server.url !== null && isProperUrl(server.url);
492
+ return server.url !== null;
490
493
  }
491
- const getServersToDisplay = (originalServers, mockUrl) => {
492
- const servers = originalServers
493
- .map((server, i) => {
494
+ const getServersToDisplay = (originalServers, mockUrl, inlineDefaults) => {
495
+ const servers = originalServers.map((server, i) => {
494
496
  const fallbackDescription = originalServers.length === 1 ? 'Live Server' : `Server ${i + 1}`;
495
- return Object.assign(Object.assign({}, server), { url: getServerUrlWithDefaultValues(server), description: server.description || fallbackDescription });
496
- })
497
- .filter(isValidServer);
497
+ let url = server.url;
498
+ if (inlineDefaults) {
499
+ url = getServerUrlWithVariableValues(server, {});
500
+ }
501
+ return Object.assign(Object.assign({}, server), { url, description: server.description || fallbackDescription });
502
+ });
498
503
  if (mockUrl) {
499
504
  servers.push({
500
505
  id: 'mock',
@@ -502,27 +507,48 @@ const getServersToDisplay = (originalServers, mockUrl) => {
502
507
  url: mockUrl,
503
508
  });
504
509
  }
505
- return servers;
510
+ return servers.filter(isValidServer);
506
511
  };
507
- const getServerUrlWithDefaultValues = (server) => {
512
+ const getServerVariables = (server) => {
508
513
  var _a;
509
- let urlString = server.url;
510
- const variables = Object.entries((_a = server.variables) !== null && _a !== void 0 ? _a : {});
511
- variables.forEach(([variableName, variableInfo]) => {
512
- urlString = urlString.replace(`{${variableName}}`, variableInfo.default);
513
- });
514
+ return Object.entries((_a = server === null || server === void 0 ? void 0 : server.variables) !== null && _a !== void 0 ? _a : {}).map(([key, value]) => ({
515
+ name: key,
516
+ default: value.default,
517
+ description: value.description,
518
+ enum: value.enum,
519
+ }));
520
+ };
521
+ function resolveUrl(urlString) {
522
+ if (urlString === null)
523
+ return null;
514
524
  let url;
515
525
  try {
516
526
  url = URI(urlString);
517
527
  }
518
- catch (_b) {
528
+ catch (_a) {
519
529
  return null;
520
530
  }
531
+ let stringifiedUrl;
521
532
  if (url.is('relative') && typeof window !== 'undefined') {
522
- url = url.absoluteTo(window.location.origin);
533
+ stringifiedUrl = url.absoluteTo(window.location.origin).toString();
534
+ }
535
+ else {
536
+ stringifiedUrl = url.toString();
523
537
  }
524
- const stringifiedUrl = url.toString();
525
- return stringifiedUrl.endsWith('/') ? stringifiedUrl.slice(0, -1) : stringifiedUrl;
538
+ if (isProperUrl(stringifiedUrl)) {
539
+ return stringifiedUrl.endsWith('/') ? stringifiedUrl.slice(0, -1) : stringifiedUrl;
540
+ }
541
+ return null;
542
+ }
543
+ const getServerUrlWithVariableValues = (server, values) => {
544
+ var _a;
545
+ let urlString = server.url;
546
+ const variables = Object.entries((_a = server.variables) !== null && _a !== void 0 ? _a : {});
547
+ variables.forEach(([variableName, variableInfo]) => {
548
+ var _a;
549
+ urlString = urlString.replaceAll(`{${variableName}}`, (_a = values[variableName]) !== null && _a !== void 0 ? _a : variableInfo.default);
550
+ });
551
+ return urlString;
526
552
  };
527
553
 
528
554
  const persistAtom = (key, atomInstance) => {
@@ -1401,11 +1427,11 @@ const useTextRequestBodyState = (mediaTypeContent) => {
1401
1427
  };
1402
1428
 
1403
1429
  const nameAndValueObjectToPair = ({ name, value }) => [name, value];
1404
- const getServerUrl = ({ chosenServer, httpOperation, mockData, corsProxy, }) => {
1430
+ const getServerUrl = ({ chosenServer, httpOperation, mockData, corsProxy, serverVariableValues, }) => {
1405
1431
  var _a;
1406
1432
  const server = chosenServer || ((_a = httpOperation.servers) === null || _a === void 0 ? void 0 : _a[0]);
1407
- const chosenServerUrl = server && getServerUrlWithDefaultValues(server);
1408
- const serverUrl = (mockData === null || mockData === void 0 ? void 0 : mockData.url) || chosenServerUrl || window.location.origin;
1433
+ const chosenServerUrl = server && getServerUrlWithVariableValues(server, serverVariableValues);
1434
+ const serverUrl = resolveUrl((mockData === null || mockData === void 0 ? void 0 : mockData.url) || chosenServerUrl || window.location.origin);
1409
1435
  if (corsProxy && !mockData) {
1410
1436
  return `${corsProxy}${serverUrl}`;
1411
1437
  }
@@ -1482,10 +1508,10 @@ const getQueryParams = ({ httpOperation, parameterValues, }) => {
1482
1508
  return acc;
1483
1509
  }, []);
1484
1510
  };
1485
- function buildFetchRequest({ httpOperation, mediaTypeContent, bodyInput, parameterValues, mockData, auth, chosenServer, credentials = 'omit', corsProxy, }) {
1511
+ function buildFetchRequest({ httpOperation, mediaTypeContent, bodyInput, parameterValues, serverVariableValues, mockData, auth, chosenServer, credentials = 'omit', corsProxy, }) {
1486
1512
  var _a, _b, _c;
1487
1513
  return __awaiter(this, void 0, void 0, function* () {
1488
- const serverUrl = getServerUrl({ httpOperation, mockData, chosenServer, corsProxy });
1514
+ const serverUrl = getServerUrl({ httpOperation, mockData, chosenServer, corsProxy, serverVariableValues });
1489
1515
  const shouldIncludeBody = ['PUT', 'POST', 'PATCH'].includes(httpOperation.method.toUpperCase()) && bodyInput !== undefined;
1490
1516
  const queryParams = getQueryParams({ httpOperation, parameterValues });
1491
1517
  const rawHeaders = filterOutAuthorizationParams((_b = (_a = httpOperation.request) === null || _a === void 0 ? void 0 : _a.headers) !== null && _b !== void 0 ? _b : [], httpOperation.security)
@@ -1558,10 +1584,10 @@ const runAuthRequestEhancements = (auth, queryParams, headers) => {
1558
1584
  }
1559
1585
  return [newQueryParams, newHeaders];
1560
1586
  };
1561
- function buildHarRequest({ httpOperation, bodyInput, parameterValues, mediaTypeContent, auth, mockData, chosenServer, corsProxy, }) {
1587
+ function buildHarRequest({ httpOperation, bodyInput, parameterValues, serverVariableValues, mediaTypeContent, auth, mockData, chosenServer, corsProxy, }) {
1562
1588
  var _a, _b, _c, _d;
1563
1589
  return __awaiter(this, void 0, void 0, function* () {
1564
- const serverUrl = getServerUrl({ httpOperation, mockData, chosenServer, corsProxy });
1590
+ const serverUrl = getServerUrl({ httpOperation, mockData, chosenServer, corsProxy, serverVariableValues });
1565
1591
  const mimeType = (_a = mediaTypeContent === null || mediaTypeContent === void 0 ? void 0 : mediaTypeContent.mediaType) !== null && _a !== void 0 ? _a : 'application/json';
1566
1592
  const shouldIncludeBody = ['PUT', 'POST', 'PATCH'].includes(httpOperation.method.toUpperCase()) && bodyInput !== undefined;
1567
1593
  const queryParams = getQueryParams({ httpOperation, parameterValues });
@@ -1883,8 +1909,18 @@ class NetworkError extends Error {
1883
1909
  }
1884
1910
  const isNetworkError = (error) => error instanceof NetworkError;
1885
1911
 
1912
+ const persistedServerVariableValuesAtom = atom({});
1913
+ const useServerVariables = () => {
1914
+ const [serverVariables, setPersistedServerVariableValues] = useAtom(persistedServerVariableValuesAtom);
1915
+ const updateServerVariableValue = (name, value) => {
1916
+ setPersistedServerVariableValues(Object.assign({}, serverVariables, { [name]: value }));
1917
+ };
1918
+ return { serverVariables, updateServerVariableValue };
1919
+ };
1920
+
1886
1921
  const ServersDropdown = ({ servers }) => {
1887
1922
  const [chosenServer, setChosenServer] = useAtom(chosenServerAtom);
1923
+ const { serverVariables } = useServerVariables();
1888
1924
  const serverItems = [
1889
1925
  {
1890
1926
  type: 'option_group',
@@ -1898,7 +1934,7 @@ const ServersDropdown = ({ servers }) => {
1898
1934
  ...servers.map((server, i) => ({
1899
1935
  id: server.url,
1900
1936
  title: server.description,
1901
- description: server.url,
1937
+ description: getServerUrlWithVariableValues(server, serverVariables),
1902
1938
  value: server.url,
1903
1939
  })),
1904
1940
  ],
@@ -1908,6 +1944,21 @@ const ServersDropdown = ({ servers }) => {
1908
1944
  };
1909
1945
  ServersDropdown.displayName = 'ServersDropdown';
1910
1946
 
1947
+ const VariableEditor = ({ variable, value, onChange }) => {
1948
+ const inputId = useUniqueId(`id_${variable.name}_`);
1949
+ return (React.createElement(React.Fragment, null,
1950
+ React.createElement(Text, { as: "label", "aria-hidden": "true", "data-testid": "param-label", htmlFor: inputId, fontSize: "base" }, variable.name),
1951
+ React.createElement(Text, { mx: 3 }, ":"),
1952
+ React.createElement("div", null, variable.enum ? (React.createElement(Select, { flex: 1, "aria-label": variable.name, options: variable.enum.map(s => ({ value: s })), value: value || variable.default, onChange: onChange })) : (React.createElement(Flex, { flex: 1 },
1953
+ React.createElement(Input, { id: inputId, "aria-label": variable.name, appearance: 'minimal', flex: 1, placeholder: variable.default, type: "text", required: true, intent: 'default', value: value || '', onChange: e => onChange && onChange(e.currentTarget.value) }))))));
1954
+ };
1955
+
1956
+ const ServerVariables = ({ variables, values, onChangeValue }) => {
1957
+ return (React.createElement(Panel, { defaultIsOpen: true },
1958
+ React.createElement(Panel.Titlebar, null, "Server Variables"),
1959
+ React.createElement(Panel.Content, { className: "sl-overflow-y-auto ParameterGrid ServerVariablesContent" }, variables.map(variable => (React.createElement(VariableEditor, { key: variable.name, variable: variable, value: values[variable.name], onChange: (value) => onChangeValue(variable.name, String(value)) }))))));
1960
+ };
1961
+
1911
1962
  const defaultServers = [];
1912
1963
  const TryIt = ({ httpOperation, mockUrl, onRequestChange, requestBodyIndex, embeddedInMd = false, tryItCredentialsPolicy, corsProxy, }) => {
1913
1964
  var _a, _b, _c, _d, _e, _f, _g;
@@ -1924,11 +1975,12 @@ const TryIt = ({ httpOperation, mockUrl, onRequestChange, requestBodyIndex, embe
1924
1975
  const [textRequestBody, setTextRequestBody] = useTextRequestBodyState(mediaTypeContent);
1925
1976
  const [operationAuthValue, setOperationAuthValue] = usePersistedSecuritySchemeWithValues();
1926
1977
  const servers = React.useMemo(() => {
1927
- const toDisplay = getServersToDisplay(httpOperation.servers || defaultServers, mockUrl);
1928
- return toDisplay;
1978
+ return getServersToDisplay(httpOperation.servers || defaultServers, mockUrl, false);
1929
1979
  }, [httpOperation.servers, mockUrl]);
1930
1980
  const firstServer = servers[0] || null;
1931
1981
  const [chosenServer, setChosenServer] = useAtom(chosenServerAtom);
1982
+ const serverVariables = getServerVariables(chosenServer);
1983
+ const { serverVariables: serverVariableValues, updateServerVariableValue } = useServerVariables();
1932
1984
  const isMockingEnabled = mockUrl && (chosenServer === null || chosenServer === void 0 ? void 0 : chosenServer.url) === mockUrl;
1933
1985
  const hasRequiredButEmptyParameters = allParameters.some(parameter => parameter.required && !parameterValuesWithDefaults[parameter.name]);
1934
1986
  const getValues = () => Object.keys(bodyParameterValues)
@@ -1950,7 +2002,8 @@ const TryIt = ({ httpOperation, mockUrl, onRequestChange, requestBodyIndex, embe
1950
2002
  React.useEffect(() => {
1951
2003
  let isMounted = true;
1952
2004
  if (onRequestChange || embeddedInMd) {
1953
- buildHarRequest(Object.assign(Object.assign({ mediaTypeContent, parameterValues: parameterValuesWithDefaults, httpOperation, bodyInput: formDataState.isFormDataBody ? getValues() : textRequestBody, auth: operationAuthValue }, (isMockingEnabled && { mockData: getMockData(mockUrl, httpOperation, mockingOptions) })), { chosenServer,
2005
+ buildHarRequest(Object.assign(Object.assign({ mediaTypeContent, parameterValues: parameterValuesWithDefaults, serverVariableValues,
2006
+ httpOperation, bodyInput: formDataState.isFormDataBody ? getValues() : textRequestBody, auth: operationAuthValue }, (isMockingEnabled && { mockData: getMockData(mockUrl, httpOperation, mockingOptions) })), { chosenServer,
1954
2007
  corsProxy })).then(request => {
1955
2008
  if (isMounted) {
1956
2009
  if (onRequestChange) {
@@ -1970,6 +2023,7 @@ const TryIt = ({ httpOperation, mockUrl, onRequestChange, requestBodyIndex, embe
1970
2023
  parameterValuesWithDefaults,
1971
2024
  formDataState.isFormDataBody,
1972
2025
  bodyParameterValues,
2026
+ serverVariableValues,
1973
2027
  isAllowedEmptyValues,
1974
2028
  textRequestBody,
1975
2029
  operationAuthValue,
@@ -1987,6 +2041,7 @@ const TryIt = ({ httpOperation, mockUrl, onRequestChange, requestBodyIndex, embe
1987
2041
  const mockData = isMockingEnabled ? getMockData(mockUrl, httpOperation, mockingOptions) : undefined;
1988
2042
  const request = yield buildFetchRequest({
1989
2043
  parameterValues: parameterValuesWithDefaults,
2044
+ serverVariableValues,
1990
2045
  httpOperation,
1991
2046
  mediaTypeContent,
1992
2047
  bodyInput: formDataState.isFormDataBody ? getValues() : textRequestBody,
@@ -2027,6 +2082,7 @@ const TryIt = ({ httpOperation, mockUrl, onRequestChange, requestBodyIndex, embe
2027
2082
  const isOnlySendButton = !((_d = httpOperation.security) === null || _d === void 0 ? void 0 : _d.length) && !allParameters.length && !formDataState.isFormDataBody && !mediaTypeContent;
2028
2083
  const tryItPanelContents = (React.createElement(React.Fragment, null,
2029
2084
  ((_e = httpOperation.security) === null || _e === void 0 ? void 0 : _e.length) ? (React.createElement(TryItAuth, { onChange: setOperationAuthValue, operationSecurityScheme: (_f = httpOperation.security) !== null && _f !== void 0 ? _f : [], value: operationAuthValue })) : null,
2085
+ serverVariables.length > 0 && (React.createElement(ServerVariables, { variables: serverVariables, values: serverVariableValues, onChangeValue: updateServerVariableValue })),
2030
2086
  allParameters.length > 0 && (React.createElement(OperationParameters, { parameters: allParameters, values: parameterValuesWithDefaults, onChangeValue: updateParameterValue, validate: validateParameters })),
2031
2087
  formDataState.isFormDataBody ? (React.createElement(FormDataBody, { specification: formDataState.bodySpecification, values: bodyParameterValues, onChangeValues: setBodyParameterValues, onChangeParameterAllow: setAllowedEmptyValues, isAllowedEmptyValues: isAllowedEmptyValues })) : mediaTypeContent ? (React.createElement(RequestBody, { examples: (_g = mediaTypeContent.examples) !== null && _g !== void 0 ? _g : [], requestBody: textRequestBody, onChange: setTextRequestBody })) : null,
2032
2088
  React.createElement(Panel.Content, { className: "SendButtonHolder", mt: 4, pt: !isOnlySendButton && !embeddedInMd ? 0 : undefined },
@@ -2538,7 +2594,8 @@ const ServerInfo = ({ servers, mockUrl }) => {
2538
2594
  const mocking = React.useContext(MockingContext);
2539
2595
  const showMocking = !mocking.hideMocking && mockUrl && isProperUrl(mockUrl);
2540
2596
  const $mockUrl = showMocking ? mockUrl || mocking.mockUrl : undefined;
2541
- const serversToDisplay = getServersToDisplay(servers, $mockUrl);
2597
+ const serversToDisplay = React.useMemo(() => getServersToDisplay(servers, $mockUrl, false), [servers, $mockUrl]);
2598
+ const firstServerVariableIndex = React.useMemo(() => serversToDisplay.findIndex(server => !isEmpty(server.variables)), [serversToDisplay]);
2542
2599
  if (!showMocking && serversToDisplay.length === 0) {
2543
2600
  return null;
2544
2601
  }
@@ -2546,25 +2603,88 @@ const ServerInfo = ({ servers, mockUrl }) => {
2546
2603
  React.createElement(Panel, { rounded: true, isCollapsible: false, className: "BaseURLContent", w: "full" },
2547
2604
  React.createElement(Panel.Titlebar, { whitespace: "nowrap" }, "API Base URL"),
2548
2605
  React.createElement(Panel.Content, { w: "full", className: "sl-flex sl-flex-col" },
2549
- React.createElement(VStack, { spacing: 1, divider: true }, serversToDisplay.map((server, index) => (React.createElement(ServerUrl, Object.assign({}, server, { key: index })))))))));
2606
+ React.createElement(VStack, { spacing: 1, divider: true }, serversToDisplay.map((server, index) => (React.createElement(ServerUrl, Object.assign({}, server, { defaultIsOpen: index === firstServerVariableIndex, hasAnyServerVariables: firstServerVariableIndex !== -1, key: server.id })))))))));
2550
2607
  };
2551
- const ServerUrl = ({ id, description, url }) => {
2608
+ const ServerUrl = ({ id, description, url, variables, hasAnyServerVariables, defaultIsOpen, }) => {
2552
2609
  const { nodeHasChanged } = useOptionsCtx();
2553
2610
  const { onCopy, hasCopied } = useClipboard(url);
2611
+ const urlFragments = useSplitUrl(url);
2554
2612
  const hasChanged = nodeHasChanged === null || nodeHasChanged === void 0 ? void 0 : nodeHasChanged({ nodeId: id });
2555
- return (React.createElement(Box, { whitespace: "nowrap", pos: "relative" },
2556
- React.createElement(Text, { pr: 2, fontWeight: "bold" },
2557
- description,
2558
- ":"),
2559
- React.createElement(Tooltip, { placement: "right", renderTrigger: () => React.createElement(Text, { "aria-label": description }, url) },
2560
- !hasCopied && (React.createElement(Box, { p: 1, onClick: onCopy, cursor: "pointer" },
2561
- "Copy Server URL ",
2562
- React.createElement(Icon, { className: "sl-ml-1", icon: ['fas', 'copy'] }))),
2563
- hasCopied && (React.createElement(Box, { p: 1 },
2564
- "Copied Server URL ",
2565
- React.createElement(Icon, { className: "sl-ml-1", icon: ['fas', 'check'] })))),
2566
- React.createElement(NodeAnnotation, { change: hasChanged, additionalLeftOffset: 16 })));
2567
- };
2613
+ const variablesSchema = useVariablesJSONSchema(variables);
2614
+ const titlePaddingLeft = hasAnyServerVariables && !variablesSchema ? 4 : 0;
2615
+ const handleCopyClick = React.useCallback(e => {
2616
+ e.stopPropagation();
2617
+ onCopy();
2618
+ }, [onCopy]);
2619
+ return (React.createElement(Panel, { isCollapsible: !!variablesSchema, defaultIsOpen: defaultIsOpen, w: "full" },
2620
+ React.createElement(Panel.Titlebar, { whitespace: "nowrap" },
2621
+ React.createElement(Text, { pl: titlePaddingLeft, pr: 2, fontWeight: "bold" },
2622
+ description,
2623
+ ":"),
2624
+ React.createElement(Tooltip, { placement: "right", renderTrigger: () => (React.createElement(Text, { "aria-label": description }, urlFragments.map(({ kind, value }, i) => (React.createElement(Text, { key: i, fontWeight: kind === 'variable' ? 'semibold' : 'normal' }, value))))) },
2625
+ !hasCopied && (React.createElement(Box, { p: 1, onClick: handleCopyClick, cursor: "pointer" },
2626
+ "Copy Server URL ",
2627
+ React.createElement(Icon, { className: "sl-ml-1", icon: ['fas', 'copy'] }))),
2628
+ hasCopied && (React.createElement(Box, { p: 1 },
2629
+ "Copied Server URL ",
2630
+ React.createElement(Icon, { className: "sl-ml-1", icon: ['fas', 'check'] })))),
2631
+ React.createElement(NodeAnnotation, { change: hasChanged, additionalLeftOffset: 16 })),
2632
+ variablesSchema && (React.createElement(Panel.Content, { w: "full" },
2633
+ React.createElement(Box, { pl: 4 },
2634
+ React.createElement(JsonSchemaViewer, { schema: variablesSchema }))))));
2635
+ };
2636
+ function useVariablesJSONSchema(variables) {
2637
+ return React.useMemo(() => {
2638
+ if (isEmpty(variables))
2639
+ return;
2640
+ const propertiesPairs = Object.entries(variables).map(([name, variable]) => [
2641
+ name,
2642
+ Object.assign({ type: 'string' }, omitBy({
2643
+ description: variable.description,
2644
+ enum: variable.enum,
2645
+ default: variable.default,
2646
+ }, isNil)),
2647
+ ]);
2648
+ return {
2649
+ type: 'object',
2650
+ properties: Object.fromEntries(propertiesPairs),
2651
+ };
2652
+ }, [variables]);
2653
+ }
2654
+ function useSplitUrl(url) {
2655
+ return React.useMemo(() => {
2656
+ const curly = /[{}]/g;
2657
+ const fragments = [];
2658
+ let startOffset = 0;
2659
+ let curPos = 0;
2660
+ let match;
2661
+ while ((match = curly.exec(url))) {
2662
+ if (match[0] === '{' || startOffset + 1 === match.index) {
2663
+ startOffset = match.index;
2664
+ continue;
2665
+ }
2666
+ if (startOffset !== curPos) {
2667
+ fragments.push({
2668
+ kind: 'static',
2669
+ value: url.slice(curPos, startOffset),
2670
+ });
2671
+ }
2672
+ const variable = url.slice(startOffset, match.index + 1);
2673
+ fragments.push({
2674
+ kind: 'variable',
2675
+ value: variable,
2676
+ });
2677
+ curPos = startOffset + variable.length;
2678
+ }
2679
+ if (curPos < url.length) {
2680
+ fragments.push({
2681
+ kind: 'static',
2682
+ value: url.slice(curPos),
2683
+ });
2684
+ }
2685
+ return fragments;
2686
+ }, [url]);
2687
+ }
2568
2688
 
2569
2689
  const HttpServiceComponent = React.memo(({ data: unresolvedData, location = {}, layoutOptions, exportProps }) => {
2570
2690
  var _a, _b, _c, _d;
package/index.js CHANGED
@@ -43,6 +43,9 @@ var entries = require('lodash/entries.js');
43
43
  var keys = require('lodash/keys.js');
44
44
  var jsonSchemaViewer = require('@stoplight/json-schema-viewer');
45
45
  var sortBy = require('lodash/sortBy.js');
46
+ var isEmpty = require('lodash/isEmpty.js');
47
+ var isNil = require('lodash/isNil.js');
48
+ var omitBy = require('lodash/omitBy.js');
46
49
  var reactRouterHashLink = require('react-router-hash-link');
47
50
  var reactQuery = require('react-query');
48
51
  var $RefParser = require('@stoplight/json-schema-ref-parser');
@@ -96,6 +99,9 @@ var formatXml__default = /*#__PURE__*/_interopDefaultLegacy(formatXml);
96
99
  var entries__default = /*#__PURE__*/_interopDefaultLegacy(entries);
97
100
  var keys__default = /*#__PURE__*/_interopDefaultLegacy(keys);
98
101
  var sortBy__default = /*#__PURE__*/_interopDefaultLegacy(sortBy);
102
+ var isEmpty__default = /*#__PURE__*/_interopDefaultLegacy(isEmpty);
103
+ var isNil__default = /*#__PURE__*/_interopDefaultLegacy(isNil);
104
+ var omitBy__default = /*#__PURE__*/_interopDefaultLegacy(omitBy);
99
105
  var $RefParser__default = /*#__PURE__*/_interopDefaultLegacy($RefParser);
100
106
  var PropTypes__namespace = /*#__PURE__*/_interopNamespace(PropTypes);
101
107
  var isEqual__default = /*#__PURE__*/_interopDefaultLegacy(isEqual);
@@ -539,15 +545,17 @@ function useChosenServerUrl(chosenServerUrl) {
539
545
  const chosenServerAtom = jotai.atom(undefined);
540
546
 
541
547
  function isValidServer(server) {
542
- return server.url !== null && isProperUrl(server.url);
548
+ return server.url !== null;
543
549
  }
544
- const getServersToDisplay = (originalServers, mockUrl) => {
545
- const servers = originalServers
546
- .map((server, i) => {
550
+ const getServersToDisplay = (originalServers, mockUrl, inlineDefaults) => {
551
+ const servers = originalServers.map((server, i) => {
547
552
  const fallbackDescription = originalServers.length === 1 ? 'Live Server' : `Server ${i + 1}`;
548
- return Object.assign(Object.assign({}, server), { url: getServerUrlWithDefaultValues(server), description: server.description || fallbackDescription });
549
- })
550
- .filter(isValidServer);
553
+ let url = server.url;
554
+ if (inlineDefaults) {
555
+ url = getServerUrlWithVariableValues(server, {});
556
+ }
557
+ return Object.assign(Object.assign({}, server), { url, description: server.description || fallbackDescription });
558
+ });
551
559
  if (mockUrl) {
552
560
  servers.push({
553
561
  id: 'mock',
@@ -555,27 +563,48 @@ const getServersToDisplay = (originalServers, mockUrl) => {
555
563
  url: mockUrl,
556
564
  });
557
565
  }
558
- return servers;
566
+ return servers.filter(isValidServer);
559
567
  };
560
- const getServerUrlWithDefaultValues = (server) => {
568
+ const getServerVariables = (server) => {
561
569
  var _a;
562
- let urlString = server.url;
563
- const variables = Object.entries((_a = server.variables) !== null && _a !== void 0 ? _a : {});
564
- variables.forEach(([variableName, variableInfo]) => {
565
- urlString = urlString.replace(`{${variableName}}`, variableInfo.default);
566
- });
570
+ return Object.entries((_a = server === null || server === void 0 ? void 0 : server.variables) !== null && _a !== void 0 ? _a : {}).map(([key, value]) => ({
571
+ name: key,
572
+ default: value.default,
573
+ description: value.description,
574
+ enum: value.enum,
575
+ }));
576
+ };
577
+ function resolveUrl(urlString) {
578
+ if (urlString === null)
579
+ return null;
567
580
  let url;
568
581
  try {
569
582
  url = URI__default["default"](urlString);
570
583
  }
571
- catch (_b) {
584
+ catch (_a) {
572
585
  return null;
573
586
  }
587
+ let stringifiedUrl;
574
588
  if (url.is('relative') && typeof window !== 'undefined') {
575
- url = url.absoluteTo(window.location.origin);
589
+ stringifiedUrl = url.absoluteTo(window.location.origin).toString();
590
+ }
591
+ else {
592
+ stringifiedUrl = url.toString();
576
593
  }
577
- const stringifiedUrl = url.toString();
578
- return stringifiedUrl.endsWith('/') ? stringifiedUrl.slice(0, -1) : stringifiedUrl;
594
+ if (isProperUrl(stringifiedUrl)) {
595
+ return stringifiedUrl.endsWith('/') ? stringifiedUrl.slice(0, -1) : stringifiedUrl;
596
+ }
597
+ return null;
598
+ }
599
+ const getServerUrlWithVariableValues = (server, values) => {
600
+ var _a;
601
+ let urlString = server.url;
602
+ const variables = Object.entries((_a = server.variables) !== null && _a !== void 0 ? _a : {});
603
+ variables.forEach(([variableName, variableInfo]) => {
604
+ var _a;
605
+ urlString = urlString.replaceAll(`{${variableName}}`, (_a = values[variableName]) !== null && _a !== void 0 ? _a : variableInfo.default);
606
+ });
607
+ return urlString;
579
608
  };
580
609
 
581
610
  const persistAtom = (key, atomInstance) => {
@@ -1454,11 +1483,11 @@ const useTextRequestBodyState = (mediaTypeContent) => {
1454
1483
  };
1455
1484
 
1456
1485
  const nameAndValueObjectToPair = ({ name, value }) => [name, value];
1457
- const getServerUrl = ({ chosenServer, httpOperation, mockData, corsProxy, }) => {
1486
+ const getServerUrl = ({ chosenServer, httpOperation, mockData, corsProxy, serverVariableValues, }) => {
1458
1487
  var _a;
1459
1488
  const server = chosenServer || ((_a = httpOperation.servers) === null || _a === void 0 ? void 0 : _a[0]);
1460
- const chosenServerUrl = server && getServerUrlWithDefaultValues(server);
1461
- const serverUrl = (mockData === null || mockData === void 0 ? void 0 : mockData.url) || chosenServerUrl || window.location.origin;
1489
+ const chosenServerUrl = server && getServerUrlWithVariableValues(server, serverVariableValues);
1490
+ const serverUrl = resolveUrl((mockData === null || mockData === void 0 ? void 0 : mockData.url) || chosenServerUrl || window.location.origin);
1462
1491
  if (corsProxy && !mockData) {
1463
1492
  return `${corsProxy}${serverUrl}`;
1464
1493
  }
@@ -1535,10 +1564,10 @@ const getQueryParams = ({ httpOperation, parameterValues, }) => {
1535
1564
  return acc;
1536
1565
  }, []);
1537
1566
  };
1538
- function buildFetchRequest({ httpOperation, mediaTypeContent, bodyInput, parameterValues, mockData, auth, chosenServer, credentials = 'omit', corsProxy, }) {
1567
+ function buildFetchRequest({ httpOperation, mediaTypeContent, bodyInput, parameterValues, serverVariableValues, mockData, auth, chosenServer, credentials = 'omit', corsProxy, }) {
1539
1568
  var _a, _b, _c;
1540
1569
  return tslib.__awaiter(this, void 0, void 0, function* () {
1541
- const serverUrl = getServerUrl({ httpOperation, mockData, chosenServer, corsProxy });
1570
+ const serverUrl = getServerUrl({ httpOperation, mockData, chosenServer, corsProxy, serverVariableValues });
1542
1571
  const shouldIncludeBody = ['PUT', 'POST', 'PATCH'].includes(httpOperation.method.toUpperCase()) && bodyInput !== undefined;
1543
1572
  const queryParams = getQueryParams({ httpOperation, parameterValues });
1544
1573
  const rawHeaders = filterOutAuthorizationParams((_b = (_a = httpOperation.request) === null || _a === void 0 ? void 0 : _a.headers) !== null && _b !== void 0 ? _b : [], httpOperation.security)
@@ -1611,10 +1640,10 @@ const runAuthRequestEhancements = (auth, queryParams, headers) => {
1611
1640
  }
1612
1641
  return [newQueryParams, newHeaders];
1613
1642
  };
1614
- function buildHarRequest({ httpOperation, bodyInput, parameterValues, mediaTypeContent, auth, mockData, chosenServer, corsProxy, }) {
1643
+ function buildHarRequest({ httpOperation, bodyInput, parameterValues, serverVariableValues, mediaTypeContent, auth, mockData, chosenServer, corsProxy, }) {
1615
1644
  var _a, _b, _c, _d;
1616
1645
  return tslib.__awaiter(this, void 0, void 0, function* () {
1617
- const serverUrl = getServerUrl({ httpOperation, mockData, chosenServer, corsProxy });
1646
+ const serverUrl = getServerUrl({ httpOperation, mockData, chosenServer, corsProxy, serverVariableValues });
1618
1647
  const mimeType = (_a = mediaTypeContent === null || mediaTypeContent === void 0 ? void 0 : mediaTypeContent.mediaType) !== null && _a !== void 0 ? _a : 'application/json';
1619
1648
  const shouldIncludeBody = ['PUT', 'POST', 'PATCH'].includes(httpOperation.method.toUpperCase()) && bodyInput !== undefined;
1620
1649
  const queryParams = getQueryParams({ httpOperation, parameterValues });
@@ -1936,8 +1965,18 @@ class NetworkError extends Error {
1936
1965
  }
1937
1966
  const isNetworkError = (error) => error instanceof NetworkError;
1938
1967
 
1968
+ const persistedServerVariableValuesAtom = jotai.atom({});
1969
+ const useServerVariables = () => {
1970
+ const [serverVariables, setPersistedServerVariableValues] = jotai.useAtom(persistedServerVariableValuesAtom);
1971
+ const updateServerVariableValue = (name, value) => {
1972
+ setPersistedServerVariableValues(Object.assign({}, serverVariables, { [name]: value }));
1973
+ };
1974
+ return { serverVariables, updateServerVariableValue };
1975
+ };
1976
+
1939
1977
  const ServersDropdown = ({ servers }) => {
1940
1978
  const [chosenServer, setChosenServer] = jotai.useAtom(chosenServerAtom);
1979
+ const { serverVariables } = useServerVariables();
1941
1980
  const serverItems = [
1942
1981
  {
1943
1982
  type: 'option_group',
@@ -1951,7 +1990,7 @@ const ServersDropdown = ({ servers }) => {
1951
1990
  ...servers.map((server, i) => ({
1952
1991
  id: server.url,
1953
1992
  title: server.description,
1954
- description: server.url,
1993
+ description: getServerUrlWithVariableValues(server, serverVariables),
1955
1994
  value: server.url,
1956
1995
  })),
1957
1996
  ],
@@ -1961,6 +2000,21 @@ const ServersDropdown = ({ servers }) => {
1961
2000
  };
1962
2001
  ServersDropdown.displayName = 'ServersDropdown';
1963
2002
 
2003
+ const VariableEditor = ({ variable, value, onChange }) => {
2004
+ const inputId = useUniqueId(`id_${variable.name}_`);
2005
+ return (React__namespace.createElement(React__namespace.Fragment, null,
2006
+ React__namespace.createElement(mosaic.Text, { as: "label", "aria-hidden": "true", "data-testid": "param-label", htmlFor: inputId, fontSize: "base" }, variable.name),
2007
+ React__namespace.createElement(mosaic.Text, { mx: 3 }, ":"),
2008
+ React__namespace.createElement("div", null, variable.enum ? (React__namespace.createElement(mosaic.Select, { flex: 1, "aria-label": variable.name, options: variable.enum.map(s => ({ value: s })), value: value || variable.default, onChange: onChange })) : (React__namespace.createElement(mosaic.Flex, { flex: 1 },
2009
+ React__namespace.createElement(mosaic.Input, { id: inputId, "aria-label": variable.name, appearance: 'minimal', flex: 1, placeholder: variable.default, type: "text", required: true, intent: 'default', value: value || '', onChange: e => onChange && onChange(e.currentTarget.value) }))))));
2010
+ };
2011
+
2012
+ const ServerVariables = ({ variables, values, onChangeValue }) => {
2013
+ return (React__namespace.createElement(mosaic.Panel, { defaultIsOpen: true },
2014
+ React__namespace.createElement(mosaic.Panel.Titlebar, null, "Server Variables"),
2015
+ React__namespace.createElement(mosaic.Panel.Content, { className: "sl-overflow-y-auto ParameterGrid ServerVariablesContent" }, variables.map(variable => (React__namespace.createElement(VariableEditor, { key: variable.name, variable: variable, value: values[variable.name], onChange: (value) => onChangeValue(variable.name, String(value)) }))))));
2016
+ };
2017
+
1964
2018
  const defaultServers = [];
1965
2019
  const TryIt = ({ httpOperation, mockUrl, onRequestChange, requestBodyIndex, embeddedInMd = false, tryItCredentialsPolicy, corsProxy, }) => {
1966
2020
  var _a, _b, _c, _d, _e, _f, _g;
@@ -1977,11 +2031,12 @@ const TryIt = ({ httpOperation, mockUrl, onRequestChange, requestBodyIndex, embe
1977
2031
  const [textRequestBody, setTextRequestBody] = useTextRequestBodyState(mediaTypeContent);
1978
2032
  const [operationAuthValue, setOperationAuthValue] = usePersistedSecuritySchemeWithValues();
1979
2033
  const servers = React__namespace.useMemo(() => {
1980
- const toDisplay = getServersToDisplay(httpOperation.servers || defaultServers, mockUrl);
1981
- return toDisplay;
2034
+ return getServersToDisplay(httpOperation.servers || defaultServers, mockUrl, false);
1982
2035
  }, [httpOperation.servers, mockUrl]);
1983
2036
  const firstServer = servers[0] || null;
1984
2037
  const [chosenServer, setChosenServer] = jotai.useAtom(chosenServerAtom);
2038
+ const serverVariables = getServerVariables(chosenServer);
2039
+ const { serverVariables: serverVariableValues, updateServerVariableValue } = useServerVariables();
1985
2040
  const isMockingEnabled = mockUrl && (chosenServer === null || chosenServer === void 0 ? void 0 : chosenServer.url) === mockUrl;
1986
2041
  const hasRequiredButEmptyParameters = allParameters.some(parameter => parameter.required && !parameterValuesWithDefaults[parameter.name]);
1987
2042
  const getValues = () => Object.keys(bodyParameterValues)
@@ -2003,7 +2058,8 @@ const TryIt = ({ httpOperation, mockUrl, onRequestChange, requestBodyIndex, embe
2003
2058
  React__namespace.useEffect(() => {
2004
2059
  let isMounted = true;
2005
2060
  if (onRequestChange || embeddedInMd) {
2006
- buildHarRequest(Object.assign(Object.assign({ mediaTypeContent, parameterValues: parameterValuesWithDefaults, httpOperation, bodyInput: formDataState.isFormDataBody ? getValues() : textRequestBody, auth: operationAuthValue }, (isMockingEnabled && { mockData: getMockData(mockUrl, httpOperation, mockingOptions) })), { chosenServer,
2061
+ buildHarRequest(Object.assign(Object.assign({ mediaTypeContent, parameterValues: parameterValuesWithDefaults, serverVariableValues,
2062
+ httpOperation, bodyInput: formDataState.isFormDataBody ? getValues() : textRequestBody, auth: operationAuthValue }, (isMockingEnabled && { mockData: getMockData(mockUrl, httpOperation, mockingOptions) })), { chosenServer,
2007
2063
  corsProxy })).then(request => {
2008
2064
  if (isMounted) {
2009
2065
  if (onRequestChange) {
@@ -2023,6 +2079,7 @@ const TryIt = ({ httpOperation, mockUrl, onRequestChange, requestBodyIndex, embe
2023
2079
  parameterValuesWithDefaults,
2024
2080
  formDataState.isFormDataBody,
2025
2081
  bodyParameterValues,
2082
+ serverVariableValues,
2026
2083
  isAllowedEmptyValues,
2027
2084
  textRequestBody,
2028
2085
  operationAuthValue,
@@ -2040,6 +2097,7 @@ const TryIt = ({ httpOperation, mockUrl, onRequestChange, requestBodyIndex, embe
2040
2097
  const mockData = isMockingEnabled ? getMockData(mockUrl, httpOperation, mockingOptions) : undefined;
2041
2098
  const request = yield buildFetchRequest({
2042
2099
  parameterValues: parameterValuesWithDefaults,
2100
+ serverVariableValues,
2043
2101
  httpOperation,
2044
2102
  mediaTypeContent,
2045
2103
  bodyInput: formDataState.isFormDataBody ? getValues() : textRequestBody,
@@ -2080,6 +2138,7 @@ const TryIt = ({ httpOperation, mockUrl, onRequestChange, requestBodyIndex, embe
2080
2138
  const isOnlySendButton = !((_d = httpOperation.security) === null || _d === void 0 ? void 0 : _d.length) && !allParameters.length && !formDataState.isFormDataBody && !mediaTypeContent;
2081
2139
  const tryItPanelContents = (React__namespace.createElement(React__namespace.Fragment, null,
2082
2140
  ((_e = httpOperation.security) === null || _e === void 0 ? void 0 : _e.length) ? (React__namespace.createElement(TryItAuth, { onChange: setOperationAuthValue, operationSecurityScheme: (_f = httpOperation.security) !== null && _f !== void 0 ? _f : [], value: operationAuthValue })) : null,
2141
+ serverVariables.length > 0 && (React__namespace.createElement(ServerVariables, { variables: serverVariables, values: serverVariableValues, onChangeValue: updateServerVariableValue })),
2083
2142
  allParameters.length > 0 && (React__namespace.createElement(OperationParameters, { parameters: allParameters, values: parameterValuesWithDefaults, onChangeValue: updateParameterValue, validate: validateParameters })),
2084
2143
  formDataState.isFormDataBody ? (React__namespace.createElement(FormDataBody, { specification: formDataState.bodySpecification, values: bodyParameterValues, onChangeValues: setBodyParameterValues, onChangeParameterAllow: setAllowedEmptyValues, isAllowedEmptyValues: isAllowedEmptyValues })) : mediaTypeContent ? (React__namespace.createElement(RequestBody, { examples: (_g = mediaTypeContent.examples) !== null && _g !== void 0 ? _g : [], requestBody: textRequestBody, onChange: setTextRequestBody })) : null,
2085
2144
  React__namespace.createElement(mosaic.Panel.Content, { className: "SendButtonHolder", mt: 4, pt: !isOnlySendButton && !embeddedInMd ? 0 : undefined },
@@ -2591,7 +2650,8 @@ const ServerInfo = ({ servers, mockUrl }) => {
2591
2650
  const mocking = React__namespace.useContext(MockingContext);
2592
2651
  const showMocking = !mocking.hideMocking && mockUrl && isProperUrl(mockUrl);
2593
2652
  const $mockUrl = showMocking ? mockUrl || mocking.mockUrl : undefined;
2594
- const serversToDisplay = getServersToDisplay(servers, $mockUrl);
2653
+ const serversToDisplay = React__namespace.useMemo(() => getServersToDisplay(servers, $mockUrl, false), [servers, $mockUrl]);
2654
+ const firstServerVariableIndex = React__namespace.useMemo(() => serversToDisplay.findIndex(server => !isEmpty__default["default"](server.variables)), [serversToDisplay]);
2595
2655
  if (!showMocking && serversToDisplay.length === 0) {
2596
2656
  return null;
2597
2657
  }
@@ -2599,25 +2659,88 @@ const ServerInfo = ({ servers, mockUrl }) => {
2599
2659
  React__namespace.createElement(mosaic.Panel, { rounded: true, isCollapsible: false, className: "BaseURLContent", w: "full" },
2600
2660
  React__namespace.createElement(mosaic.Panel.Titlebar, { whitespace: "nowrap" }, "API Base URL"),
2601
2661
  React__namespace.createElement(mosaic.Panel.Content, { w: "full", className: "sl-flex sl-flex-col" },
2602
- React__namespace.createElement(mosaic.VStack, { spacing: 1, divider: true }, serversToDisplay.map((server, index) => (React__namespace.createElement(ServerUrl, Object.assign({}, server, { key: index })))))))));
2662
+ React__namespace.createElement(mosaic.VStack, { spacing: 1, divider: true }, serversToDisplay.map((server, index) => (React__namespace.createElement(ServerUrl, Object.assign({}, server, { defaultIsOpen: index === firstServerVariableIndex, hasAnyServerVariables: firstServerVariableIndex !== -1, key: server.id })))))))));
2603
2663
  };
2604
- const ServerUrl = ({ id, description, url }) => {
2664
+ const ServerUrl = ({ id, description, url, variables, hasAnyServerVariables, defaultIsOpen, }) => {
2605
2665
  const { nodeHasChanged } = useOptionsCtx();
2606
2666
  const { onCopy, hasCopied } = mosaic.useClipboard(url);
2667
+ const urlFragments = useSplitUrl(url);
2607
2668
  const hasChanged = nodeHasChanged === null || nodeHasChanged === void 0 ? void 0 : nodeHasChanged({ nodeId: id });
2608
- return (React__namespace.createElement(mosaic.Box, { whitespace: "nowrap", pos: "relative" },
2609
- React__namespace.createElement(mosaic.Text, { pr: 2, fontWeight: "bold" },
2610
- description,
2611
- ":"),
2612
- React__namespace.createElement(mosaic.Tooltip, { placement: "right", renderTrigger: () => React__namespace.createElement(mosaic.Text, { "aria-label": description }, url) },
2613
- !hasCopied && (React__namespace.createElement(mosaic.Box, { p: 1, onClick: onCopy, cursor: "pointer" },
2614
- "Copy Server URL ",
2615
- React__namespace.createElement(mosaic.Icon, { className: "sl-ml-1", icon: ['fas', 'copy'] }))),
2616
- hasCopied && (React__namespace.createElement(mosaic.Box, { p: 1 },
2617
- "Copied Server URL ",
2618
- React__namespace.createElement(mosaic.Icon, { className: "sl-ml-1", icon: ['fas', 'check'] })))),
2619
- React__namespace.createElement(mosaic.NodeAnnotation, { change: hasChanged, additionalLeftOffset: 16 })));
2620
- };
2669
+ const variablesSchema = useVariablesJSONSchema(variables);
2670
+ const titlePaddingLeft = hasAnyServerVariables && !variablesSchema ? 4 : 0;
2671
+ const handleCopyClick = React__namespace.useCallback(e => {
2672
+ e.stopPropagation();
2673
+ onCopy();
2674
+ }, [onCopy]);
2675
+ return (React__namespace.createElement(mosaic.Panel, { isCollapsible: !!variablesSchema, defaultIsOpen: defaultIsOpen, w: "full" },
2676
+ React__namespace.createElement(mosaic.Panel.Titlebar, { whitespace: "nowrap" },
2677
+ React__namespace.createElement(mosaic.Text, { pl: titlePaddingLeft, pr: 2, fontWeight: "bold" },
2678
+ description,
2679
+ ":"),
2680
+ React__namespace.createElement(mosaic.Tooltip, { placement: "right", renderTrigger: () => (React__namespace.createElement(mosaic.Text, { "aria-label": description }, urlFragments.map(({ kind, value }, i) => (React__namespace.createElement(mosaic.Text, { key: i, fontWeight: kind === 'variable' ? 'semibold' : 'normal' }, value))))) },
2681
+ !hasCopied && (React__namespace.createElement(mosaic.Box, { p: 1, onClick: handleCopyClick, cursor: "pointer" },
2682
+ "Copy Server URL ",
2683
+ React__namespace.createElement(mosaic.Icon, { className: "sl-ml-1", icon: ['fas', 'copy'] }))),
2684
+ hasCopied && (React__namespace.createElement(mosaic.Box, { p: 1 },
2685
+ "Copied Server URL ",
2686
+ React__namespace.createElement(mosaic.Icon, { className: "sl-ml-1", icon: ['fas', 'check'] })))),
2687
+ React__namespace.createElement(mosaic.NodeAnnotation, { change: hasChanged, additionalLeftOffset: 16 })),
2688
+ variablesSchema && (React__namespace.createElement(mosaic.Panel.Content, { w: "full" },
2689
+ React__namespace.createElement(mosaic.Box, { pl: 4 },
2690
+ React__namespace.createElement(jsonSchemaViewer.JsonSchemaViewer, { schema: variablesSchema }))))));
2691
+ };
2692
+ function useVariablesJSONSchema(variables) {
2693
+ return React__namespace.useMemo(() => {
2694
+ if (isEmpty__default["default"](variables))
2695
+ return;
2696
+ const propertiesPairs = Object.entries(variables).map(([name, variable]) => [
2697
+ name,
2698
+ Object.assign({ type: 'string' }, omitBy__default["default"]({
2699
+ description: variable.description,
2700
+ enum: variable.enum,
2701
+ default: variable.default,
2702
+ }, isNil__default["default"])),
2703
+ ]);
2704
+ return {
2705
+ type: 'object',
2706
+ properties: Object.fromEntries(propertiesPairs),
2707
+ };
2708
+ }, [variables]);
2709
+ }
2710
+ function useSplitUrl(url) {
2711
+ return React__namespace.useMemo(() => {
2712
+ const curly = /[{}]/g;
2713
+ const fragments = [];
2714
+ let startOffset = 0;
2715
+ let curPos = 0;
2716
+ let match;
2717
+ while ((match = curly.exec(url))) {
2718
+ if (match[0] === '{' || startOffset + 1 === match.index) {
2719
+ startOffset = match.index;
2720
+ continue;
2721
+ }
2722
+ if (startOffset !== curPos) {
2723
+ fragments.push({
2724
+ kind: 'static',
2725
+ value: url.slice(curPos, startOffset),
2726
+ });
2727
+ }
2728
+ const variable = url.slice(startOffset, match.index + 1);
2729
+ fragments.push({
2730
+ kind: 'variable',
2731
+ value: variable,
2732
+ });
2733
+ curPos = startOffset + variable.length;
2734
+ }
2735
+ if (curPos < url.length) {
2736
+ fragments.push({
2737
+ kind: 'static',
2738
+ value: url.slice(curPos),
2739
+ });
2740
+ }
2741
+ return fragments;
2742
+ }, [url]);
2743
+ }
2621
2744
 
2622
2745
  const HttpServiceComponent = React__namespace.memo(({ data: unresolvedData, location = {}, layoutOptions, exportProps }) => {
2623
2746
  var _a, _b, _c, _d;
package/index.mjs CHANGED
@@ -41,6 +41,9 @@ import entries from 'lodash/entries.js';
41
41
  import keys from 'lodash/keys.js';
42
42
  import { JsonSchemaViewer } from '@stoplight/json-schema-viewer';
43
43
  import sortBy from 'lodash/sortBy.js';
44
+ import isEmpty from 'lodash/isEmpty.js';
45
+ import isNil from 'lodash/isNil.js';
46
+ import omitBy from 'lodash/omitBy.js';
44
47
  import { HashLink } from 'react-router-hash-link';
45
48
  import { QueryClient, useQueryClient, QueryClientProvider } from 'react-query';
46
49
  import $RefParser from '@stoplight/json-schema-ref-parser';
@@ -486,15 +489,17 @@ function useChosenServerUrl(chosenServerUrl) {
486
489
  const chosenServerAtom = atom(undefined);
487
490
 
488
491
  function isValidServer(server) {
489
- return server.url !== null && isProperUrl(server.url);
492
+ return server.url !== null;
490
493
  }
491
- const getServersToDisplay = (originalServers, mockUrl) => {
492
- const servers = originalServers
493
- .map((server, i) => {
494
+ const getServersToDisplay = (originalServers, mockUrl, inlineDefaults) => {
495
+ const servers = originalServers.map((server, i) => {
494
496
  const fallbackDescription = originalServers.length === 1 ? 'Live Server' : `Server ${i + 1}`;
495
- return Object.assign(Object.assign({}, server), { url: getServerUrlWithDefaultValues(server), description: server.description || fallbackDescription });
496
- })
497
- .filter(isValidServer);
497
+ let url = server.url;
498
+ if (inlineDefaults) {
499
+ url = getServerUrlWithVariableValues(server, {});
500
+ }
501
+ return Object.assign(Object.assign({}, server), { url, description: server.description || fallbackDescription });
502
+ });
498
503
  if (mockUrl) {
499
504
  servers.push({
500
505
  id: 'mock',
@@ -502,27 +507,48 @@ const getServersToDisplay = (originalServers, mockUrl) => {
502
507
  url: mockUrl,
503
508
  });
504
509
  }
505
- return servers;
510
+ return servers.filter(isValidServer);
506
511
  };
507
- const getServerUrlWithDefaultValues = (server) => {
512
+ const getServerVariables = (server) => {
508
513
  var _a;
509
- let urlString = server.url;
510
- const variables = Object.entries((_a = server.variables) !== null && _a !== void 0 ? _a : {});
511
- variables.forEach(([variableName, variableInfo]) => {
512
- urlString = urlString.replace(`{${variableName}}`, variableInfo.default);
513
- });
514
+ return Object.entries((_a = server === null || server === void 0 ? void 0 : server.variables) !== null && _a !== void 0 ? _a : {}).map(([key, value]) => ({
515
+ name: key,
516
+ default: value.default,
517
+ description: value.description,
518
+ enum: value.enum,
519
+ }));
520
+ };
521
+ function resolveUrl(urlString) {
522
+ if (urlString === null)
523
+ return null;
514
524
  let url;
515
525
  try {
516
526
  url = URI(urlString);
517
527
  }
518
- catch (_b) {
528
+ catch (_a) {
519
529
  return null;
520
530
  }
531
+ let stringifiedUrl;
521
532
  if (url.is('relative') && typeof window !== 'undefined') {
522
- url = url.absoluteTo(window.location.origin);
533
+ stringifiedUrl = url.absoluteTo(window.location.origin).toString();
534
+ }
535
+ else {
536
+ stringifiedUrl = url.toString();
523
537
  }
524
- const stringifiedUrl = url.toString();
525
- return stringifiedUrl.endsWith('/') ? stringifiedUrl.slice(0, -1) : stringifiedUrl;
538
+ if (isProperUrl(stringifiedUrl)) {
539
+ return stringifiedUrl.endsWith('/') ? stringifiedUrl.slice(0, -1) : stringifiedUrl;
540
+ }
541
+ return null;
542
+ }
543
+ const getServerUrlWithVariableValues = (server, values) => {
544
+ var _a;
545
+ let urlString = server.url;
546
+ const variables = Object.entries((_a = server.variables) !== null && _a !== void 0 ? _a : {});
547
+ variables.forEach(([variableName, variableInfo]) => {
548
+ var _a;
549
+ urlString = urlString.replaceAll(`{${variableName}}`, (_a = values[variableName]) !== null && _a !== void 0 ? _a : variableInfo.default);
550
+ });
551
+ return urlString;
526
552
  };
527
553
 
528
554
  const persistAtom = (key, atomInstance) => {
@@ -1401,11 +1427,11 @@ const useTextRequestBodyState = (mediaTypeContent) => {
1401
1427
  };
1402
1428
 
1403
1429
  const nameAndValueObjectToPair = ({ name, value }) => [name, value];
1404
- const getServerUrl = ({ chosenServer, httpOperation, mockData, corsProxy, }) => {
1430
+ const getServerUrl = ({ chosenServer, httpOperation, mockData, corsProxy, serverVariableValues, }) => {
1405
1431
  var _a;
1406
1432
  const server = chosenServer || ((_a = httpOperation.servers) === null || _a === void 0 ? void 0 : _a[0]);
1407
- const chosenServerUrl = server && getServerUrlWithDefaultValues(server);
1408
- const serverUrl = (mockData === null || mockData === void 0 ? void 0 : mockData.url) || chosenServerUrl || window.location.origin;
1433
+ const chosenServerUrl = server && getServerUrlWithVariableValues(server, serverVariableValues);
1434
+ const serverUrl = resolveUrl((mockData === null || mockData === void 0 ? void 0 : mockData.url) || chosenServerUrl || window.location.origin);
1409
1435
  if (corsProxy && !mockData) {
1410
1436
  return `${corsProxy}${serverUrl}`;
1411
1437
  }
@@ -1482,10 +1508,10 @@ const getQueryParams = ({ httpOperation, parameterValues, }) => {
1482
1508
  return acc;
1483
1509
  }, []);
1484
1510
  };
1485
- function buildFetchRequest({ httpOperation, mediaTypeContent, bodyInput, parameterValues, mockData, auth, chosenServer, credentials = 'omit', corsProxy, }) {
1511
+ function buildFetchRequest({ httpOperation, mediaTypeContent, bodyInput, parameterValues, serverVariableValues, mockData, auth, chosenServer, credentials = 'omit', corsProxy, }) {
1486
1512
  var _a, _b, _c;
1487
1513
  return __awaiter(this, void 0, void 0, function* () {
1488
- const serverUrl = getServerUrl({ httpOperation, mockData, chosenServer, corsProxy });
1514
+ const serverUrl = getServerUrl({ httpOperation, mockData, chosenServer, corsProxy, serverVariableValues });
1489
1515
  const shouldIncludeBody = ['PUT', 'POST', 'PATCH'].includes(httpOperation.method.toUpperCase()) && bodyInput !== undefined;
1490
1516
  const queryParams = getQueryParams({ httpOperation, parameterValues });
1491
1517
  const rawHeaders = filterOutAuthorizationParams((_b = (_a = httpOperation.request) === null || _a === void 0 ? void 0 : _a.headers) !== null && _b !== void 0 ? _b : [], httpOperation.security)
@@ -1558,10 +1584,10 @@ const runAuthRequestEhancements = (auth, queryParams, headers) => {
1558
1584
  }
1559
1585
  return [newQueryParams, newHeaders];
1560
1586
  };
1561
- function buildHarRequest({ httpOperation, bodyInput, parameterValues, mediaTypeContent, auth, mockData, chosenServer, corsProxy, }) {
1587
+ function buildHarRequest({ httpOperation, bodyInput, parameterValues, serverVariableValues, mediaTypeContent, auth, mockData, chosenServer, corsProxy, }) {
1562
1588
  var _a, _b, _c, _d;
1563
1589
  return __awaiter(this, void 0, void 0, function* () {
1564
- const serverUrl = getServerUrl({ httpOperation, mockData, chosenServer, corsProxy });
1590
+ const serverUrl = getServerUrl({ httpOperation, mockData, chosenServer, corsProxy, serverVariableValues });
1565
1591
  const mimeType = (_a = mediaTypeContent === null || mediaTypeContent === void 0 ? void 0 : mediaTypeContent.mediaType) !== null && _a !== void 0 ? _a : 'application/json';
1566
1592
  const shouldIncludeBody = ['PUT', 'POST', 'PATCH'].includes(httpOperation.method.toUpperCase()) && bodyInput !== undefined;
1567
1593
  const queryParams = getQueryParams({ httpOperation, parameterValues });
@@ -1883,8 +1909,18 @@ class NetworkError extends Error {
1883
1909
  }
1884
1910
  const isNetworkError = (error) => error instanceof NetworkError;
1885
1911
 
1912
+ const persistedServerVariableValuesAtom = atom({});
1913
+ const useServerVariables = () => {
1914
+ const [serverVariables, setPersistedServerVariableValues] = useAtom(persistedServerVariableValuesAtom);
1915
+ const updateServerVariableValue = (name, value) => {
1916
+ setPersistedServerVariableValues(Object.assign({}, serverVariables, { [name]: value }));
1917
+ };
1918
+ return { serverVariables, updateServerVariableValue };
1919
+ };
1920
+
1886
1921
  const ServersDropdown = ({ servers }) => {
1887
1922
  const [chosenServer, setChosenServer] = useAtom(chosenServerAtom);
1923
+ const { serverVariables } = useServerVariables();
1888
1924
  const serverItems = [
1889
1925
  {
1890
1926
  type: 'option_group',
@@ -1898,7 +1934,7 @@ const ServersDropdown = ({ servers }) => {
1898
1934
  ...servers.map((server, i) => ({
1899
1935
  id: server.url,
1900
1936
  title: server.description,
1901
- description: server.url,
1937
+ description: getServerUrlWithVariableValues(server, serverVariables),
1902
1938
  value: server.url,
1903
1939
  })),
1904
1940
  ],
@@ -1908,6 +1944,21 @@ const ServersDropdown = ({ servers }) => {
1908
1944
  };
1909
1945
  ServersDropdown.displayName = 'ServersDropdown';
1910
1946
 
1947
+ const VariableEditor = ({ variable, value, onChange }) => {
1948
+ const inputId = useUniqueId(`id_${variable.name}_`);
1949
+ return (React.createElement(React.Fragment, null,
1950
+ React.createElement(Text, { as: "label", "aria-hidden": "true", "data-testid": "param-label", htmlFor: inputId, fontSize: "base" }, variable.name),
1951
+ React.createElement(Text, { mx: 3 }, ":"),
1952
+ React.createElement("div", null, variable.enum ? (React.createElement(Select, { flex: 1, "aria-label": variable.name, options: variable.enum.map(s => ({ value: s })), value: value || variable.default, onChange: onChange })) : (React.createElement(Flex, { flex: 1 },
1953
+ React.createElement(Input, { id: inputId, "aria-label": variable.name, appearance: 'minimal', flex: 1, placeholder: variable.default, type: "text", required: true, intent: 'default', value: value || '', onChange: e => onChange && onChange(e.currentTarget.value) }))))));
1954
+ };
1955
+
1956
+ const ServerVariables = ({ variables, values, onChangeValue }) => {
1957
+ return (React.createElement(Panel, { defaultIsOpen: true },
1958
+ React.createElement(Panel.Titlebar, null, "Server Variables"),
1959
+ React.createElement(Panel.Content, { className: "sl-overflow-y-auto ParameterGrid ServerVariablesContent" }, variables.map(variable => (React.createElement(VariableEditor, { key: variable.name, variable: variable, value: values[variable.name], onChange: (value) => onChangeValue(variable.name, String(value)) }))))));
1960
+ };
1961
+
1911
1962
  const defaultServers = [];
1912
1963
  const TryIt = ({ httpOperation, mockUrl, onRequestChange, requestBodyIndex, embeddedInMd = false, tryItCredentialsPolicy, corsProxy, }) => {
1913
1964
  var _a, _b, _c, _d, _e, _f, _g;
@@ -1924,11 +1975,12 @@ const TryIt = ({ httpOperation, mockUrl, onRequestChange, requestBodyIndex, embe
1924
1975
  const [textRequestBody, setTextRequestBody] = useTextRequestBodyState(mediaTypeContent);
1925
1976
  const [operationAuthValue, setOperationAuthValue] = usePersistedSecuritySchemeWithValues();
1926
1977
  const servers = React.useMemo(() => {
1927
- const toDisplay = getServersToDisplay(httpOperation.servers || defaultServers, mockUrl);
1928
- return toDisplay;
1978
+ return getServersToDisplay(httpOperation.servers || defaultServers, mockUrl, false);
1929
1979
  }, [httpOperation.servers, mockUrl]);
1930
1980
  const firstServer = servers[0] || null;
1931
1981
  const [chosenServer, setChosenServer] = useAtom(chosenServerAtom);
1982
+ const serverVariables = getServerVariables(chosenServer);
1983
+ const { serverVariables: serverVariableValues, updateServerVariableValue } = useServerVariables();
1932
1984
  const isMockingEnabled = mockUrl && (chosenServer === null || chosenServer === void 0 ? void 0 : chosenServer.url) === mockUrl;
1933
1985
  const hasRequiredButEmptyParameters = allParameters.some(parameter => parameter.required && !parameterValuesWithDefaults[parameter.name]);
1934
1986
  const getValues = () => Object.keys(bodyParameterValues)
@@ -1950,7 +2002,8 @@ const TryIt = ({ httpOperation, mockUrl, onRequestChange, requestBodyIndex, embe
1950
2002
  React.useEffect(() => {
1951
2003
  let isMounted = true;
1952
2004
  if (onRequestChange || embeddedInMd) {
1953
- buildHarRequest(Object.assign(Object.assign({ mediaTypeContent, parameterValues: parameterValuesWithDefaults, httpOperation, bodyInput: formDataState.isFormDataBody ? getValues() : textRequestBody, auth: operationAuthValue }, (isMockingEnabled && { mockData: getMockData(mockUrl, httpOperation, mockingOptions) })), { chosenServer,
2005
+ buildHarRequest(Object.assign(Object.assign({ mediaTypeContent, parameterValues: parameterValuesWithDefaults, serverVariableValues,
2006
+ httpOperation, bodyInput: formDataState.isFormDataBody ? getValues() : textRequestBody, auth: operationAuthValue }, (isMockingEnabled && { mockData: getMockData(mockUrl, httpOperation, mockingOptions) })), { chosenServer,
1954
2007
  corsProxy })).then(request => {
1955
2008
  if (isMounted) {
1956
2009
  if (onRequestChange) {
@@ -1970,6 +2023,7 @@ const TryIt = ({ httpOperation, mockUrl, onRequestChange, requestBodyIndex, embe
1970
2023
  parameterValuesWithDefaults,
1971
2024
  formDataState.isFormDataBody,
1972
2025
  bodyParameterValues,
2026
+ serverVariableValues,
1973
2027
  isAllowedEmptyValues,
1974
2028
  textRequestBody,
1975
2029
  operationAuthValue,
@@ -1987,6 +2041,7 @@ const TryIt = ({ httpOperation, mockUrl, onRequestChange, requestBodyIndex, embe
1987
2041
  const mockData = isMockingEnabled ? getMockData(mockUrl, httpOperation, mockingOptions) : undefined;
1988
2042
  const request = yield buildFetchRequest({
1989
2043
  parameterValues: parameterValuesWithDefaults,
2044
+ serverVariableValues,
1990
2045
  httpOperation,
1991
2046
  mediaTypeContent,
1992
2047
  bodyInput: formDataState.isFormDataBody ? getValues() : textRequestBody,
@@ -2027,6 +2082,7 @@ const TryIt = ({ httpOperation, mockUrl, onRequestChange, requestBodyIndex, embe
2027
2082
  const isOnlySendButton = !((_d = httpOperation.security) === null || _d === void 0 ? void 0 : _d.length) && !allParameters.length && !formDataState.isFormDataBody && !mediaTypeContent;
2028
2083
  const tryItPanelContents = (React.createElement(React.Fragment, null,
2029
2084
  ((_e = httpOperation.security) === null || _e === void 0 ? void 0 : _e.length) ? (React.createElement(TryItAuth, { onChange: setOperationAuthValue, operationSecurityScheme: (_f = httpOperation.security) !== null && _f !== void 0 ? _f : [], value: operationAuthValue })) : null,
2085
+ serverVariables.length > 0 && (React.createElement(ServerVariables, { variables: serverVariables, values: serverVariableValues, onChangeValue: updateServerVariableValue })),
2030
2086
  allParameters.length > 0 && (React.createElement(OperationParameters, { parameters: allParameters, values: parameterValuesWithDefaults, onChangeValue: updateParameterValue, validate: validateParameters })),
2031
2087
  formDataState.isFormDataBody ? (React.createElement(FormDataBody, { specification: formDataState.bodySpecification, values: bodyParameterValues, onChangeValues: setBodyParameterValues, onChangeParameterAllow: setAllowedEmptyValues, isAllowedEmptyValues: isAllowedEmptyValues })) : mediaTypeContent ? (React.createElement(RequestBody, { examples: (_g = mediaTypeContent.examples) !== null && _g !== void 0 ? _g : [], requestBody: textRequestBody, onChange: setTextRequestBody })) : null,
2032
2088
  React.createElement(Panel.Content, { className: "SendButtonHolder", mt: 4, pt: !isOnlySendButton && !embeddedInMd ? 0 : undefined },
@@ -2538,7 +2594,8 @@ const ServerInfo = ({ servers, mockUrl }) => {
2538
2594
  const mocking = React.useContext(MockingContext);
2539
2595
  const showMocking = !mocking.hideMocking && mockUrl && isProperUrl(mockUrl);
2540
2596
  const $mockUrl = showMocking ? mockUrl || mocking.mockUrl : undefined;
2541
- const serversToDisplay = getServersToDisplay(servers, $mockUrl);
2597
+ const serversToDisplay = React.useMemo(() => getServersToDisplay(servers, $mockUrl, false), [servers, $mockUrl]);
2598
+ const firstServerVariableIndex = React.useMemo(() => serversToDisplay.findIndex(server => !isEmpty(server.variables)), [serversToDisplay]);
2542
2599
  if (!showMocking && serversToDisplay.length === 0) {
2543
2600
  return null;
2544
2601
  }
@@ -2546,25 +2603,88 @@ const ServerInfo = ({ servers, mockUrl }) => {
2546
2603
  React.createElement(Panel, { rounded: true, isCollapsible: false, className: "BaseURLContent", w: "full" },
2547
2604
  React.createElement(Panel.Titlebar, { whitespace: "nowrap" }, "API Base URL"),
2548
2605
  React.createElement(Panel.Content, { w: "full", className: "sl-flex sl-flex-col" },
2549
- React.createElement(VStack, { spacing: 1, divider: true }, serversToDisplay.map((server, index) => (React.createElement(ServerUrl, Object.assign({}, server, { key: index })))))))));
2606
+ React.createElement(VStack, { spacing: 1, divider: true }, serversToDisplay.map((server, index) => (React.createElement(ServerUrl, Object.assign({}, server, { defaultIsOpen: index === firstServerVariableIndex, hasAnyServerVariables: firstServerVariableIndex !== -1, key: server.id })))))))));
2550
2607
  };
2551
- const ServerUrl = ({ id, description, url }) => {
2608
+ const ServerUrl = ({ id, description, url, variables, hasAnyServerVariables, defaultIsOpen, }) => {
2552
2609
  const { nodeHasChanged } = useOptionsCtx();
2553
2610
  const { onCopy, hasCopied } = useClipboard(url);
2611
+ const urlFragments = useSplitUrl(url);
2554
2612
  const hasChanged = nodeHasChanged === null || nodeHasChanged === void 0 ? void 0 : nodeHasChanged({ nodeId: id });
2555
- return (React.createElement(Box, { whitespace: "nowrap", pos: "relative" },
2556
- React.createElement(Text, { pr: 2, fontWeight: "bold" },
2557
- description,
2558
- ":"),
2559
- React.createElement(Tooltip, { placement: "right", renderTrigger: () => React.createElement(Text, { "aria-label": description }, url) },
2560
- !hasCopied && (React.createElement(Box, { p: 1, onClick: onCopy, cursor: "pointer" },
2561
- "Copy Server URL ",
2562
- React.createElement(Icon, { className: "sl-ml-1", icon: ['fas', 'copy'] }))),
2563
- hasCopied && (React.createElement(Box, { p: 1 },
2564
- "Copied Server URL ",
2565
- React.createElement(Icon, { className: "sl-ml-1", icon: ['fas', 'check'] })))),
2566
- React.createElement(NodeAnnotation, { change: hasChanged, additionalLeftOffset: 16 })));
2567
- };
2613
+ const variablesSchema = useVariablesJSONSchema(variables);
2614
+ const titlePaddingLeft = hasAnyServerVariables && !variablesSchema ? 4 : 0;
2615
+ const handleCopyClick = React.useCallback(e => {
2616
+ e.stopPropagation();
2617
+ onCopy();
2618
+ }, [onCopy]);
2619
+ return (React.createElement(Panel, { isCollapsible: !!variablesSchema, defaultIsOpen: defaultIsOpen, w: "full" },
2620
+ React.createElement(Panel.Titlebar, { whitespace: "nowrap" },
2621
+ React.createElement(Text, { pl: titlePaddingLeft, pr: 2, fontWeight: "bold" },
2622
+ description,
2623
+ ":"),
2624
+ React.createElement(Tooltip, { placement: "right", renderTrigger: () => (React.createElement(Text, { "aria-label": description }, urlFragments.map(({ kind, value }, i) => (React.createElement(Text, { key: i, fontWeight: kind === 'variable' ? 'semibold' : 'normal' }, value))))) },
2625
+ !hasCopied && (React.createElement(Box, { p: 1, onClick: handleCopyClick, cursor: "pointer" },
2626
+ "Copy Server URL ",
2627
+ React.createElement(Icon, { className: "sl-ml-1", icon: ['fas', 'copy'] }))),
2628
+ hasCopied && (React.createElement(Box, { p: 1 },
2629
+ "Copied Server URL ",
2630
+ React.createElement(Icon, { className: "sl-ml-1", icon: ['fas', 'check'] })))),
2631
+ React.createElement(NodeAnnotation, { change: hasChanged, additionalLeftOffset: 16 })),
2632
+ variablesSchema && (React.createElement(Panel.Content, { w: "full" },
2633
+ React.createElement(Box, { pl: 4 },
2634
+ React.createElement(JsonSchemaViewer, { schema: variablesSchema }))))));
2635
+ };
2636
+ function useVariablesJSONSchema(variables) {
2637
+ return React.useMemo(() => {
2638
+ if (isEmpty(variables))
2639
+ return;
2640
+ const propertiesPairs = Object.entries(variables).map(([name, variable]) => [
2641
+ name,
2642
+ Object.assign({ type: 'string' }, omitBy({
2643
+ description: variable.description,
2644
+ enum: variable.enum,
2645
+ default: variable.default,
2646
+ }, isNil)),
2647
+ ]);
2648
+ return {
2649
+ type: 'object',
2650
+ properties: Object.fromEntries(propertiesPairs),
2651
+ };
2652
+ }, [variables]);
2653
+ }
2654
+ function useSplitUrl(url) {
2655
+ return React.useMemo(() => {
2656
+ const curly = /[{}]/g;
2657
+ const fragments = [];
2658
+ let startOffset = 0;
2659
+ let curPos = 0;
2660
+ let match;
2661
+ while ((match = curly.exec(url))) {
2662
+ if (match[0] === '{' || startOffset + 1 === match.index) {
2663
+ startOffset = match.index;
2664
+ continue;
2665
+ }
2666
+ if (startOffset !== curPos) {
2667
+ fragments.push({
2668
+ kind: 'static',
2669
+ value: url.slice(curPos, startOffset),
2670
+ });
2671
+ }
2672
+ const variable = url.slice(startOffset, match.index + 1);
2673
+ fragments.push({
2674
+ kind: 'variable',
2675
+ value: variable,
2676
+ });
2677
+ curPos = startOffset + variable.length;
2678
+ }
2679
+ if (curPos < url.length) {
2680
+ fragments.push({
2681
+ kind: 'static',
2682
+ value: url.slice(curPos),
2683
+ });
2684
+ }
2685
+ return fragments;
2686
+ }, [url]);
2687
+ }
2568
2688
 
2569
2689
  const HttpServiceComponent = React.memo(({ data: unresolvedData, location = {}, layoutOptions, exportProps }) => {
2570
2690
  var _a, _b, _c, _d;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stoplight/elements-core",
3
- "version": "7.7.21",
3
+ "version": "7.9.0",
4
4
  "main": "./index.js",
5
5
  "sideEffects": [
6
6
  "web-components.min.js",
@@ -1,3 +1,9 @@
1
- import type { IServer } from '@stoplight/types';
2
- export declare const getServersToDisplay: (originalServers: IServer[], mockUrl?: string) => IServer[];
3
- export declare const getServerUrlWithDefaultValues: (server: IServer) => string | null;
1
+ import type { Dictionary, INodeVariable, IServer } from '@stoplight/types';
2
+ export declare type ServerVariable = INodeVariable & {
3
+ name: string;
4
+ };
5
+ export declare const getServersToDisplay: (originalServers: IServer[], mockUrl: string | undefined, inlineDefaults: boolean) => IServer[];
6
+ export declare const getServerVariables: (server?: IServer | null) => ServerVariable[];
7
+ export declare const getServerVariableDefaults: (server: IServer) => Record<string, string>;
8
+ export declare function resolveUrl(urlString: string | null): string | null;
9
+ export declare const getServerUrlWithVariableValues: (server: IServer, values: Dictionary<string, string>) => string;