Skip to content

Commit 5055c82

Browse files
committed
Extract export logic for reuse, as part of a new UI model
This also moves markdown & the UI store itself into that same model. General idea here is to put anything that doesn't actually use React or define a visual component directly, but does think closely about UX & user-visible interactions, in here as a component/model middleground.
1 parent 9472883 commit 5055c82

19 files changed

+163
-137
lines changed

src/components/account/pro-placeholders.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { inject, observer } from "mobx-react";
44
import { styled } from "../../styles";
55
import { Icon } from "../../icons";
66

7-
import { UiStore } from "../../model/ui-store";
7+
import { UiStore } from "../../model/ui/ui-store";
88

99
import { Pill } from "../common/pill";
1010
import { UnstyledButton } from "../common/inputs";

src/components/common/text-content.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { styled } from "../../styles";
55
import { Html } from '../../types';
66
import { suggestionIconHtml, warningIconHtml } from '../../icons';
77

8-
import { fromMarkdown } from '../../model/markdown';
8+
import { fromMarkdown } from '../../model/ui/markdown';
99

1010
export const ContentLabel = styled.h2`
1111
text-transform: uppercase;

src/components/intercept/config/electron-config.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { Icon } from '../../../icons';
77
import { logError } from '../../../errors';
88

99
import { Interceptor } from '../../../model/interception/interceptors';
10-
import { UiStore } from '../../../model/ui-store';
10+
import { UiStore } from '../../../model/ui/ui-store';
1111
import { DesktopApi } from '../../../services/desktop-api';
1212

1313
import { uploadFile } from '../../../util/ui';

src/components/intercept/config/existing-terminal-config.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
} from '../../../services/service-versions';
1212

1313
import { AccountStore } from '../../../model/account/account-store';
14-
import { UiStore } from '../../../model/ui-store';
14+
import { UiStore } from '../../../model/ui/ui-store';
1515
import { InterceptorStore } from '../../../model/interception/interceptor-store';
1616
import { Interceptor } from '../../../model/interception/interceptors';
1717

src/components/settings/settings-page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { styled, Theme, ThemeName } from '../../styles';
1212
import { Icon, WarningIcon } from '../../icons';
1313

1414
import { AccountStore } from '../../model/account/account-store';
15-
import { UiStore } from '../../model/ui-store';
15+
import { UiStore } from '../../model/ui/ui-store';
1616
import { serverVersion, versionSatisfies, PORT_RANGE_SERVER_RANGE } from '../../services/service-versions';
1717

1818
import { CollapsibleCard, CollapsibleCardHeading } from '../common/card';

src/components/store-powered-theme-provider.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { inject, observer } from 'mobx-react';
44
import { ThemeProvider } from '../styles';
55
import { WithInjected } from '../types';
66

7-
import { UiStore } from '../model/ui-store';
7+
import { UiStore } from '../model/ui/ui-store';
88

99
const StorePoweredThemeProvider = inject('uiStore')(observer((p: {
1010
uiStore: UiStore,

src/components/view/filters/search-filter.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {
1919
buildCustomFilter,
2020
CustomFilterClass
2121
} from '../../../model/filters/filter-matching';
22-
import { UiStore } from '../../../model/ui-store';
22+
import { UiStore } from '../../../model/ui/ui-store';
2323
import { AccountStore } from '../../../model/account/account-store';
2424

2525
import { IconButton, IconButtonLink } from '../../common/icon-button';

src/components/view/http/http-aborted-card.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { observer, inject } from 'mobx-react';
44
import { HttpExchange } from '../../../types';
55
import { styled } from '../../../styles';
66

7-
import { UiStore } from '../../../model/ui-store';
7+
import { UiStore } from '../../../model/ui/ui-store';
88
import { getStatusColor } from '../../../model/events/categorization';
99

1010
import { Pill } from '../../common/pill';

src/components/view/http/http-details-pane.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { CollectedEvent, HtkResponse, HttpExchange } from '../../../types';
88
import { styled } from '../../../styles';
99
import { logError } from '../../../errors';
1010

11-
import { UiStore } from '../../../model/ui-store';
11+
import { UiStore } from '../../../model/ui/ui-store';
1212
import { RulesStore } from '../../../model/rules/rules-store';
1313
import { AccountStore } from '../../../model/account/account-store';
1414
import { ApiExchange } from '../../../model/api/api-interfaces';

src/components/view/http/http-export-card.tsx

Lines changed: 19 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,25 @@
1-
import * as _ from 'lodash';
21
import React from "react";
32
import { action, computed } from "mobx";
43
import { inject, observer } from "mobx-react";
5-
import * as HarFormat from 'har-format';
6-
import * as HTTPSnippet from "@httptoolkit/httpsnippet";
74
import dedent from 'dedent';
85

9-
import { Omit, HttpExchange } from '../../../types';
6+
import { HttpExchange } from '../../../types';
107
import { styled } from '../../../styles';
118
import { Icon } from '../../../icons';
12-
import { saveFile } from '../../../util/ui';
139
import { logError } from '../../../errors';
1410

1511
import { AccountStore } from '../../../model/account/account-store';
16-
import { UiStore } from '../../../model/ui-store';
17-
import { generateHarRequest, generateHar, ExtendedHarRequest } from '../../../model/http/har';
12+
import { UiStore } from '../../../model/ui/ui-store';
13+
import {
14+
exportHar,
15+
generateCodeSnippet,
16+
getCodeSnippetFormatKey,
17+
getCodeSnippetFormatName,
18+
getCodeSnippetOptionFromKey,
19+
DEFAULT_SNIPPET_FORMAT_KEY,
20+
snippetExportOptions,
21+
SnippetOption
22+
} from '../../../model/ui/export';
1823

1924
import { ProHeaderPill, CardSalesPitch } from '../../account/pro-placeholders';
2025
import {
@@ -27,39 +32,6 @@ import { CopyButtonPill } from '../../common/copy-button';
2732
import { DocsLink } from '../../common/docs-link';
2833
import { ThemedSelfSizedEditor } from '../../editor/base-editor';
2934

30-
interface SnippetOption {
31-
target: HTTPSnippet.Target,
32-
client: HTTPSnippet.Client,
33-
name: string,
34-
description: string,
35-
link: string
36-
}
37-
38-
const snippetExportOptions: _.Dictionary<SnippetOption[]> = _(HTTPSnippet.availableTargets())
39-
.keyBy(target => target.title)
40-
.mapValues(target =>
41-
target.clients.map((client) => ({
42-
target: target.key,
43-
client: client.key,
44-
name: client.title,
45-
description: client.description,
46-
link: client.link
47-
}))
48-
).value();
49-
50-
const KEY_SEPARATOR = '~~';
51-
52-
const getExportOptionKey = (option: SnippetOption) =>
53-
option.target + KEY_SEPARATOR + option.client;
54-
55-
// Show the client name, or an overridden name in some ambiguous cases
56-
const getExportOptionName = (option: SnippetOption) => ({
57-
'php~~curl': 'PHP ext-cURL',
58-
'php~~http1': 'PHP HTTP v1',
59-
'php~~http2': 'PHP HTTP v2',
60-
'node~~native': 'Node.js HTTP'
61-
} as _.Dictionary<string>)[getExportOptionKey(option)] || option.name;
62-
6335
interface ExportCardProps extends CollapsibleCardProps {
6436
exchange: HttpExchange;
6537
accountStore?: AccountStore;
@@ -84,62 +56,15 @@ const snippetEditorOptions = {
8456
hover: { enabled: false }
8557
};
8658

87-
const simplifyHarForSnippetExport = (harRequest: ExtendedHarRequest) => {
88-
const postData = !!harRequest.postData
89-
? harRequest.postData
90-
: harRequest._requestBodyStatus === 'discarded:not-representable'
91-
? {
92-
mimeType: 'text/plain',
93-
text: "!!! UNREPRESENTABLE BINARY REQUEST BODY - BODY MUST BE EXPORTED SEPARATELY !!!"
94-
}
95-
: harRequest._requestBodyStatus === 'discarded:too-large'
96-
? {
97-
mimeType: 'text/plain',
98-
text: "!!! VERY LARGE REQUEST BODY - BODY MUST BE EXPORTED & INCLUDED SEPARATELY !!!"
99-
}
100-
: harRequest._requestBodyStatus === 'discarded:not-decodable'
101-
? {
102-
mimeType: 'text/plain',
103-
text: "!!! REQUEST BODY COULD NOT BE DECODED !!!"
104-
}
105-
: undefined;
106-
107-
// When exporting code snippets the primary goal is to generate convenient code to send the
108-
// request that's *sematantically* equivalent to the original request, not to force every
109-
// tool to produce byte-for-byte identical requests (that's effectively impossible). To do
110-
// this, we drop headers that tools can produce automatically for themselves:
111-
return {
112-
...harRequest,
113-
postData,
114-
headers: _.filter(harRequest.headers, (header) => {
115-
// All clients should be able to automatically generate the correct content-length
116-
// headers as required for a request where it's unspecified. If we override this,
117-
// it can cause problems if tools change the body length (due to encoding/compression).
118-
if (header.name.toLowerCase() === 'content-length') return false;
119-
120-
// HTTP/2 headers should never be included in snippets - they're implicitly part of
121-
// the other request data (the method etc).
122-
// We can drop this after fixing https://github.com/Kong/httpsnippet/issues/298
123-
if (header.name.startsWith(':')) return false;
124-
125-
return true;
126-
})
127-
};
128-
};
129-
13059
const ExportSnippetEditor = observer((p: {
13160
exchange: HttpExchange
13261
exportOption: SnippetOption
13362
}) => {
13463
const { target, client, link, description } = p.exportOption;
135-
const harRequest = generateHarRequest(p.exchange.request, false, {
136-
bodySizeLimit: Infinity
137-
});
138-
const harSnippetBase = simplifyHarForSnippetExport(harRequest);
13964

14065
let snippet: string;
14166
try {
142-
snippet = new HTTPSnippet(harSnippetBase).convert(target, client);
67+
snippet = generateCodeSnippet(p.exchange, p.exportOption);
14368
} catch (e) {
14469
console.log(`Failed to export request for ${target}--${client}`);
14570
logError(e);
@@ -154,7 +79,7 @@ const ExportSnippetEditor = observer((p: {
15479
<SnippetDescriptionContainer>
15580
<p>
15681
<strong>{
157-
getExportOptionName(p.exportOption)
82+
getCodeSnippetFormatName(p.exportOption)
15883
}</strong>: { description }
15984
</p>
16085
<p>
@@ -174,29 +99,14 @@ const ExportSnippetEditor = observer((p: {
17499
'javascript': 'javascript',
175100
'node': 'javascript',
176101
'shell': 'shell',
177-
} as _.Dictionary<string>)[target] || 'text'
102+
} as Record<string, string>)[target] || 'text'
178103
}
179104
options={snippetEditorOptions}
180105
/>
181106
</SnippetEditorContainer>
182107
</>;
183108
});
184109

185-
const exportHar = async (exchange: HttpExchange) => {
186-
const harContent = JSON.stringify(
187-
await generateHar([exchange], {
188-
bodySizeLimit: Infinity
189-
})
190-
);
191-
const filename = `${
192-
exchange.request.method
193-
} ${
194-
exchange.request.parsedUrl.hostname
195-
}.har`;
196-
197-
saveFile(filename, 'application/har+json;charset=utf-8', harContent);
198-
};
199-
200110
const ExportHarPill = styled(observer((p: {
201111
className?: string,
202112
exchange: HttpExchange
@@ -232,8 +142,8 @@ export class HttpExportCard extends React.Component<ExportCardProps> {
232142
onChange={this.setSnippetOption}
233143
value={this.snippetOption}
234144
optGroups={snippetExportOptions}
235-
keyFormatter={getExportOptionKey}
236-
nameFormatter={getExportOptionName}
145+
keyFormatter={getCodeSnippetFormatKey}
146+
nameFormatter={getCodeSnippetFormatName}
237147
/>
238148

239149
<CollapsibleCardHeading onCollapseToggled={this.props.onCollapseToggled}>
@@ -268,15 +178,8 @@ export class HttpExportCard extends React.Component<ExportCardProps> {
268178
@computed
269179
private get snippetOption(): SnippetOption {
270180
let exportSnippetFormat = this.props.uiStore!.exportSnippetFormat ||
271-
`shell${KEY_SEPARATOR}curl`;
272-
273-
const [target, client] = exportSnippetFormat.split(KEY_SEPARATOR) as
274-
[HTTPSnippet.Target, HTTPSnippet.Client];
275-
276-
return _(snippetExportOptions)
277-
.values()
278-
.flatten()
279-
.find({ target, client }) as SnippetOption;
181+
DEFAULT_SNIPPET_FORMAT_KEY;
182+
return getCodeSnippetOptionFromKey(exportSnippetFormat);
280183
}
281184

282185
@action.bound

0 commit comments

Comments
 (0)