Skip to content

Commit d57ca72

Browse files
committed
Ability to pin or exclude headers from display
1 parent 5ecb2ad commit d57ca72

File tree

5 files changed

+201
-19
lines changed

5 files changed

+201
-19
lines changed

src/components/send/sent-response-headers.tsx

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import * as React from 'react';
22

33
import { RawHeaders } from '../../types';
4+
import {IncludeExcludeList} from '../../model/IncludeExcludeList';
5+
46

57
import {
68
CollapsibleCardHeading,
@@ -16,6 +18,7 @@ export interface ResponseHeaderSectionProps extends ExpandableCardProps {
1618
requestUrl: URL;
1719
headers: RawHeaders;
1820
}
21+
const HeadersIncludeExcludeList = new IncludeExcludeList<string>();
1922

2023
export const SentResponseHeaderSection = ({
2124
requestUrl,
@@ -34,7 +37,7 @@ export const SentResponseHeaderSection = ({
3437
Response Headers
3538
</CollapsibleCardHeading>
3639
</header>
37-
<HeaderDetails
40+
<HeaderDetails HeadersIncludeExcludeList={HeadersIncludeExcludeList}
3841
requestUrl={requestUrl}
3942
headers={headers}
4043
/>

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

+55-5
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ import {
1818

1919
import { CookieHeaderDescription } from './set-cookie-header-description';
2020
import { UserAgentHeaderDescription } from './user-agent-header-description';
21+
import { IEList, IncludeExcludeList } from '../../../model/IncludeExcludeList';
22+
import { HeadersContextMenuBuilder } from './headers-context-menu-builder';
23+
import { filterProps } from '../../component-utils';
24+
import { ArrowIcon, Icon, WarningIcon } from '../../../icons';
25+
import { UiStore } from '../../../model/ui/ui-store';
2126

2227
const HeadersGrid = styled.section`
2328
display: grid;
@@ -77,12 +82,49 @@ const getHeaderDescription = (
7782
</p>
7883
};
7984

80-
export const HeaderDetails = inject('accountStore')(observer((props: {
85+
86+
const RowPin = styled(
87+
filterProps(Icon, 'pinned')
88+
).attrs((p: { pinned: boolean }) => ({
89+
icon: ['fas', 'thumbtack'],
90+
title: p.pinned ? "This header is pinned, it will appear at the top of the list by default" : ''
91+
}))`
92+
font-size: 90%;
93+
background-color: ${p => p.theme.containerBackground};
94+
/* Without this, 0 width pins create a large & invisible but still clickable icon */
95+
overflow: hidden;
96+
transition: width 0.1s, padding 0.1s, margin 0.1s;
97+
${(p: { pinned: boolean }) =>
98+
p.pinned
99+
? `
100+
width: auto;
101+
padding: 4px;
102+
height: 40%;
103+
&& { margin-right: -3px; }
104+
`
105+
: `
106+
padding: 0px 0;
107+
width: 0 !important;
108+
margin: 0 !important;
109+
`
110+
}
111+
`;
112+
113+
export const HeaderDetails = inject('accountStore', 'uiStore')(observer((props: {
81114
headers: RawHeaders,
82115
requestUrl: URL,
83-
accountStore?: AccountStore
116+
HeadersIncludeExcludeList: IncludeExcludeList<string>,
117+
accountStore?: AccountStore,
118+
uiStore?: UiStore
84119
}) => {
85-
const sortedHeaders = _.sortBy(props.headers, ([key]) => key.toLowerCase());
120+
const contextMenuBuilder = new HeadersContextMenuBuilder(
121+
props.accountStore!,
122+
props.uiStore!
123+
);
124+
const filtered = props.HeadersIncludeExcludeList.FilterArrayAgainstList(_.sortBy(props.headers, ([key]) => key.toLowerCase()), IEList.Favorite, true, ([key]) => key);
125+
const sortedHeaders = Array.from(props.HeadersIncludeExcludeList.SortArrayAgainstList(filtered, IEList.Favorite, ([key]) => key));
126+
let hiddenCount = props.headers.length - sortedHeaders.length;
127+
86128

87129
return sortedHeaders.length === 0 ?
88130
<BlankContentPlaceholder>(None)</BlankContentPlaceholder>
@@ -98,8 +140,8 @@ export const HeaderDetails = inject('accountStore')(observer((props: {
98140
)
99141

100142
return <CollapsibleSection withinGrid={true} key={`${key}-${i}`}>
101-
<HeaderKeyValue>
102-
<HeaderName>{ key }: </HeaderName>
143+
<HeaderKeyValue onContextMenu={contextMenuBuilder.getContextMenuCallback({ header_name: key, header_value: [value], HeadersIncludeExcludeList: props.HeadersIncludeExcludeList })}>
144+
<HeaderName><RowPin pinned={props.HeadersIncludeExcludeList.IsKeyOnList(key, IEList.Favorite)} /> {key}: </HeaderName>
103145
<span>{ value }</span>
104146
</HeaderKeyValue>
105147

@@ -111,5 +153,13 @@ export const HeaderDetails = inject('accountStore')(observer((props: {
111153
</HeaderDescriptionContainer> }
112154
</CollapsibleSection>
113155
}) }
156+
{
157+
hiddenCount > 0 ?
158+
<CollapsibleSection withinGrid={true}><HeaderKeyValue>
159+
<HeaderName>Plus {hiddenCount} hidden...</HeaderName>
160+
161+
</HeaderKeyValue></CollapsibleSection>
162+
: <BlankContentPlaceholder />
163+
}
114164
</HeadersGrid>;
115165
}));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import { action, runInAction } from 'mobx';
2+
3+
4+
import { AccountStore } from '../../../model/account/account-store';
5+
import { UiStore } from '../../../model/ui/ui-store';
6+
7+
import { IEList, IncludeExcludeList } from '../../../model/IncludeExcludeList';
8+
9+
import { copyToClipboard } from '../../../util/ui';
10+
import { ContextMenuItem } from '../../../model/ui/context-menu';
11+
12+
13+
14+
15+
export interface HeadersHeaderClickedData {
16+
HeadersIncludeExcludeList : IncludeExcludeList<string>
17+
}
18+
19+
export interface HeaderEvent {
20+
HeadersIncludeExcludeList: IncludeExcludeList<string>,
21+
header_name: string;
22+
header_value: string[];
23+
}
24+
25+
export class HeadersHeaderContextMenuBuilder {
26+
27+
constructor(
28+
private uiStore: UiStore
29+
) {}
30+
31+
getContextMenuCallback(event: HeadersHeaderClickedData) {
32+
return (mouseEvent: React.MouseEvent) => {
33+
let excluded = event.HeadersIncludeExcludeList.GetKeysOnList(IEList.Exclude);
34+
35+
this.uiStore.handleContextMenuEvent(mouseEvent, [
36+
37+
{
38+
type: 'submenu',
39+
enabled: excluded.length > 0,
40+
label: `Excluded`,
41+
items: [
42+
{
43+
type: 'option',
44+
label: `Clear All Excluded Headers`,
45+
callback: async (data) => data.HeadersIncludeExcludeList.ClearList(IEList.Exclude)
46+
47+
},
48+
...
49+
(excluded.map((headerName) => ({
50+
51+
type: 'option',
52+
label: `Clear '${headerName}'`,
53+
callback: async (data: HeadersHeaderClickedData) =>
54+
data.HeadersIncludeExcludeList.RemoveFromList(headerName,IEList.Exclude)
55+
56+
57+
}
58+
))
59+
) as ContextMenuItem<HeadersHeaderClickedData>[]
60+
]
61+
}
62+
63+
64+
], event
65+
66+
);
67+
};
68+
}
69+
}
70+
71+
export class HeadersContextMenuBuilder {
72+
73+
constructor(
74+
private accountStore: AccountStore,
75+
private uiStore: UiStore
76+
) {}
77+
78+
getContextMenuCallback(event: HeaderEvent) {
79+
return (mouseEvent: React.MouseEvent) => {
80+
const { isPaidUser } = this.accountStore;
81+
let isPinned = event.HeadersIncludeExcludeList.IsKeyOnList(event.header_name,IEList.Favorite);
82+
83+
this.uiStore.handleContextMenuEvent(mouseEvent, [
84+
{
85+
type: 'option',
86+
label: (isPinned ? `Unpin` : `Pin`) + ` This Header`,
87+
callback: async (data) => {
88+
isPinned ? data.HeadersIncludeExcludeList.RemoveFromList(data.header_name,IEList.Favorite) : data.HeadersIncludeExcludeList.AddOrUpdateToList(data.header_name,IEList.Favorite);
89+
}
90+
},
91+
{
92+
type: 'option',
93+
label: `Exclude This Header`,
94+
callback: async (data) => {
95+
data.HeadersIncludeExcludeList.AddOrUpdateToList(data.header_name,IEList.Exclude);
96+
}
97+
},
98+
{
99+
type: 'option',
100+
label: `Copy Header Value`,
101+
callback: async (data) => copyToClipboard( data.header_value.join("\n" ) )
102+
103+
104+
}
105+
106+
], event
107+
108+
);
109+
};
110+
}
111+
}

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

+16-7
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import { getSummaryColour } from '../../../model/events/categorization';
1212
import { getMethodDocs } from '../../../model/http/http-docs';
1313
import { nameHandlerClass } from '../../../model/rules/rule-descriptions';
1414
import { HandlerClassKey } from '../../../model/rules/rules';
15+
import {IEList, IncludeExcludeList} from '../../../model/IncludeExcludeList';
16+
import { HeadersHeaderContextMenuBuilder, HeaderEvent } from './headers-context-menu-builder';
1517

1618
import {
1719
CollapsibleCardHeading,
@@ -87,7 +89,10 @@ const MatchedRulePill = styled(inject('uiStore')((p: {
8789
}
8890
`;
8991

90-
const RawRequestDetails = (p: { request: HtkRequest }) => {
92+
const HeadersIncludeExcludeList = new IncludeExcludeList<string>();
93+
94+
const RawRequestDetails = (p: { request: HtkRequest, contextMenuBuilder: HeadersHeaderContextMenuBuilder, uiStore : UiStore }) => {
95+
9196
const methodDocs = getMethodDocs(p.request.method);
9297
const methodDetails = [
9398
methodDocs && <Markdown
@@ -131,24 +136,28 @@ const RawRequestDetails = (p: { request: HtkRequest }) => {
131136
</CollapsibleSectionBody>
132137
}
133138
</CollapsibleSection>
134-
135-
<ContentLabelBlock>Headers</ContentLabelBlock>
136-
<HeaderDetails headers={p.request.rawHeaders} requestUrl={p.request.parsedUrl} />
139+
<ContentLabelBlock onContextMenu={p.contextMenuBuilder.getContextMenuCallback({HeadersIncludeExcludeList})}>Headers</ContentLabelBlock>
140+
<HeaderDetails uiStore={p.uiStore} headers={p.request.rawHeaders} HeadersIncludeExcludeList={HeadersIncludeExcludeList} requestUrl={p.request.parsedUrl} />
137141
</div>;
138142
}
139143

140144
interface HttpRequestCardProps extends CollapsibleCardProps {
141145
exchange: HttpExchange;
146+
uiStore?: UiStore;
142147
matchedRuleData: {
143148
stepTypes: HandlerClassKey[],
144149
status: 'unchanged' | 'modified-types' | 'deleted'
145150
} | undefined;
146151
onRuleClicked: () => void;
147152
}
148153

149-
export const HttpRequestCard = observer((props: HttpRequestCardProps) => {
154+
export const HttpRequestCard = inject('uiStore') (observer((props: HttpRequestCardProps) => {
150155
const { exchange, matchedRuleData, onRuleClicked } = props;
151156
const { request } = exchange;
157+
const contextMenuBuilder = new HeadersHeaderContextMenuBuilder(
158+
props.uiStore!
159+
);
160+
152161

153162
// We consider passthrough as a no-op, and so don't show anything in that case.
154163
const noopRule = matchedRuleData?.stepTypes.every(
@@ -177,6 +186,6 @@ export const HttpRequestCard = observer((props: HttpRequestCardProps) => {
177186
</CollapsibleCardHeading>
178187
</header>
179188

180-
<RawRequestDetails request={request} />
189+
<RawRequestDetails uiStore={props.uiStore!} request={request} contextMenuBuilder={contextMenuBuilder} />
181190
</CollapsibleCard>;
182-
});
191+
}));

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

+15-6
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import * as _ from 'lodash';
22
import * as React from 'react';
3-
import { observer } from 'mobx-react';
3+
4+
import { inject, observer } from 'mobx-react';
45
import { get } from 'typesafe-get';
56

67
import { HtkResponse, Omit } from '../../../types';
78
import { Theme } from '../../../styles';
89

910
import { ApiExchange } from '../../../model/api/api-interfaces';
11+
import { UiStore } from '../../../model/ui/ui-store';
1012
import { getStatusColor } from '../../../model/events/categorization';
1113
import { getStatusDocs, getStatusMessage } from '../../../model/http/http-docs';
1214

@@ -17,6 +19,8 @@ import {
1719
} from '../../common/card';
1820
import { Pill } from '../../common/pill';
1921
import { HeaderDetails } from './header-details';
22+
import { HeadersHeaderContextMenuBuilder, HeaderEvent } from './headers-context-menu-builder';
23+
import {IEList, IncludeExcludeList} from '../../../model/IncludeExcludeList';
2024
import {
2125
} from '../../common/card';
2226
import {
@@ -35,13 +39,19 @@ import { DocsLink } from '../../common/docs-link';
3539
interface HttpResponseCardProps extends CollapsibleCardProps {
3640
theme: Theme;
3741
requestUrl: URL;
42+
uiStore?: UiStore;
3843
response: HtkResponse;
3944
apiExchange: ApiExchange | undefined;
4045
}
46+
const HeadersIncludeExcludeList = new IncludeExcludeList<string>();
4147

42-
export const HttpResponseCard = observer((props: HttpResponseCardProps) => {
48+
export const HttpResponseCard = inject('uiStore') (observer((props: HttpResponseCardProps) => {
4349
const { response, requestUrl, theme, apiExchange } = props;
4450

51+
const contextMenuBuilder = new HeadersHeaderContextMenuBuilder(
52+
props.uiStore!
53+
);
54+
4555
const apiResponseDescription = get(apiExchange, 'response', 'description');
4656
const statusDocs = getStatusDocs(response.statusCode);
4757

@@ -84,9 +94,8 @@ export const HttpResponseCard = observer((props: HttpResponseCardProps) => {
8494
: null
8595
}
8696
</CollapsibleSection>
87-
88-
<ContentLabelBlock>Headers</ContentLabelBlock>
89-
<HeaderDetails headers={response.rawHeaders} requestUrl={requestUrl} />
97+
<ContentLabelBlock onContextMenu={contextMenuBuilder.getContextMenuCallback({HeadersIncludeExcludeList})}>Headers</ContentLabelBlock>
98+
<HeaderDetails headers={response.rawHeaders} requestUrl={requestUrl} HeadersIncludeExcludeList={HeadersIncludeExcludeList} uiStore={props.uiStore} />
9099
</div>
91100
</CollapsibleCard>;
92-
});
101+
}));

0 commit comments

Comments
 (0)