Skip to content

Commit 2fcec0d

Browse files
authored
Add share feedback form (#878)
1 parent 5696f95 commit 2fcec0d

File tree

22 files changed

+338
-5
lines changed

22 files changed

+338
-5
lines changed

web/packages/design/src/Icon/Icon.jsx

+4
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,10 @@ export const CarrotRight = makeFontIcon('CarrotRight', 'icon-caret-right');
7272
export const CarrotSort = makeFontIcon('CarrotSort', 'icon-sort');
7373
export const CarrotUp = makeFontIcon('CarrotUp', 'icon-caret-up');
7474
export const Cash = makeFontIcon('Cash', 'icon-cash-dollar');
75+
export const ChatBubble = makeFontIcon(
76+
'ChatBubble',
77+
'icon-chat_bubble_outline'
78+
);
7579
export const ChevronCircleDown = makeFontIcon(
7680
'ChevronCircleDown',
7781
'icon-chevron-down-circle'

web/packages/design/src/Icon/Icon.story.js

+1
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ export const ListOfIcons = () => (
5454
<IconBox IconCmpt={Icon.CarrotSort} text="CarrotSort" />
5555
<IconBox IconCmpt={Icon.CarrotUp} text="CarrotUp" />
5656
<IconBox IconCmpt={Icon.Cash} text="Cash" />
57+
<IconBox IconCmpt={Icon.ChatBubble} text="ChatBubble" />
5758
<IconBox IconCmpt={Icon.ChevronCircleDown} text="ChevronCircleDown" />
5859
<IconBox IconCmpt={Icon.ChevronCircleLeft} text="ChevronCircleLeft" />
5960
<IconBox IconCmpt={Icon.ChevronCircleRight} text="ChevronCircleRight" />
Binary file not shown.

web/packages/design/src/assets/icomoon/fonts/icomoon.svg

+1
Loading
Binary file not shown.
Binary file not shown.
Binary file not shown.

web/packages/design/src/assets/icomoon/style.css

+4-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

web/packages/teleport/src/Apps/__snapshots__/Apps.story.test.tsx.snap

+2
Original file line numberDiff line numberDiff line change
@@ -747,6 +747,7 @@ exports[`failed state 1`] = `
747747
border-radius: 12px;
748748
background: #222C59;
749749
cursor: pointer;
750+
flex-shrink: 0;
750751
}
751752
752753
.c15:before {
@@ -1687,6 +1688,7 @@ exports[`loaded state 1`] = `
16871688
border-radius: 12px;
16881689
background: #222C59;
16891690
cursor: pointer;
1691+
flex-shrink: 0;
16901692
}
16911693
16921694
.c15:before {

web/packages/teleport/src/Console/DocumentNodes/__snapshots__/DocumentNodes.story.test.tsx.snap

+1
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,7 @@ exports[`render DocumentNodes 1`] = `
373373
border-radius: 12px;
374374
background: #03203C;
375375
cursor: pointer;
376+
flex-shrink: 0;
376377
}
377378
378379
.c22:before {

web/packages/teleport/src/Databases/__snapshots__/Databases.story.test.tsx.snap

+2
Original file line numberDiff line numberDiff line change
@@ -678,6 +678,7 @@ exports[`failed 1`] = `
678678
border-radius: 12px;
679679
background: #222C59;
680680
cursor: pointer;
681+
flex-shrink: 0;
681682
}
682683
683684
.c15:before {
@@ -1418,6 +1419,7 @@ exports[`open source loaded 1`] = `
14181419
border-radius: 12px;
14191420
background: #222C59;
14201421
cursor: pointer;
1422+
flex-shrink: 0;
14211423
}
14221424
14231425
.c15:before {

web/packages/teleport/src/Kubes/__snapshots__/Kubes.story.test.tsx.snap

+2
Original file line numberDiff line numberDiff line change
@@ -637,6 +637,7 @@ exports[`failed 1`] = `
637637
border-radius: 12px;
638638
background: #222C59;
639639
cursor: pointer;
640+
flex-shrink: 0;
640641
}
641642
642643
.c15:before {
@@ -1336,6 +1337,7 @@ exports[`loaded 1`] = `
13361337
border-radius: 12px;
13371338
background: #222C59;
13381339
cursor: pointer;
1340+
flex-shrink: 0;
13391341
}
13401342
13411343
.c15:before {

web/packages/teleport/src/Nodes/__snapshots__/Nodes.story.test.tsx.snap

+2
Original file line numberDiff line numberDiff line change
@@ -688,6 +688,7 @@ exports[`failed 1`] = `
688688
border-radius: 12px;
689689
background: #222C59;
690690
cursor: pointer;
691+
flex-shrink: 0;
691692
}
692693
693694
.c15:before {
@@ -1654,6 +1655,7 @@ exports[`loaded 1`] = `
16541655
border-radius: 12px;
16551656
background: #222C59;
16561657
cursor: pointer;
1658+
flex-shrink: 0;
16571659
}
16581660
16591661
.c18:before {

web/packages/teleport/src/components/Toggle/Toggle.tsx

+6-2
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,23 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17-
import React from 'react';
17+
import React, { ReactNode } from 'react';
1818
import styled from 'styled-components';
1919

20-
export default function Toggle({ isToggled, onToggle }: Props) {
20+
export default function Toggle({ isToggled, onToggle, children }: Props) {
2121
return (
2222
<StyledWrapper>
2323
<StyledInput checked={isToggled} onChange={() => onToggle()} />
2424
<StyledSlider />
25+
{children}
2526
</StyledWrapper>
2627
);
2728
}
2829

2930
type Props = {
3031
isToggled: boolean;
3132
onToggle: () => void;
33+
children?: ReactNode;
3234
};
3335

3436
const StyledWrapper = styled.label`
@@ -44,6 +46,7 @@ const StyledSlider = styled.div`
4446
border-radius: 12px;
4547
background: ${props => props.theme.colors.primary.light};
4648
cursor: pointer;
49+
flex-shrink: 0;
4750
4851
&:before {
4952
content: '';
@@ -64,6 +67,7 @@ const StyledInput = styled.input.attrs({ type: 'checkbox' })`
6467
6568
&:checked + ${StyledSlider} {
6669
background: ${props => props.theme.colors.secondary.main};
70+
6771
&:before {
6872
transform: translate(16px, -50%);
6973
}

web/packages/teleterm/src/main.ts

+14-2
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,21 @@ app.on('web-contents-created', (_, contents) => {
8181
contents.setWindowOpenHandler(details => {
8282
const url = new URL(details.url);
8383

84-
// Open links to documentation in the external browser.
84+
function isUrlSafe(): boolean {
85+
if (url.host === 'goteleport.com') {
86+
return true;
87+
}
88+
if (
89+
url.host === 'github.com' &&
90+
url.pathname.startsWith('/gravitational/')
91+
) {
92+
return true;
93+
}
94+
}
95+
96+
// Open links to documentation and GitHub issues in the external browser.
8597
// They need to have `target` set to `_blank`.
86-
if (url.host === 'goteleport.com') {
98+
if (isUrlSafe()) {
8799
shell.openExternal(url.toString());
88100
} else {
89101
logger.warn(

web/packages/teleterm/src/ui/LayoutManager.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import React from 'react';
1818
import { Flex } from 'design';
1919
import { TabHostContainer } from 'teleterm/ui/TabHost';
2020
import { TopBar } from 'teleterm/ui/TopBar';
21+
import { StatusBar } from 'teleterm/ui/StatusBar';
2122

2223
export function LayoutManager() {
2324
return (
@@ -26,6 +27,7 @@ export function LayoutManager() {
2627
<Flex flex="1" minHeight={0} style={{ position: 'relative' }}>
2728
<TabHostContainer />
2829
</Flex>
30+
<StatusBar />
2931
</Flex>
3032
);
3133
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import React from 'react';
2+
import { fireEvent, render } from 'design/utils/testing';
3+
import { ShareFeedback } from './ShareFeedback';
4+
import { MockAppContextProvider } from 'teleterm/ui/fixtures/MockAppContextProvider';
5+
import { Cluster } from 'teleterm/services/tshd/v1/cluster_pb';
6+
import { MockAppContext } from 'teleterm/ui/fixtures/mocks';
7+
8+
test('email field is not prefilled with the username if is not an email', () => {
9+
const appContext = new MockAppContext();
10+
const clusterUri = '/clusters/localhost';
11+
jest
12+
.spyOn(appContext.clustersService, 'findCluster')
13+
.mockImplementation(() => {
14+
return {
15+
loggedInUser: { name: 'alice' },
16+
} as Cluster.AsObject;
17+
});
18+
19+
jest
20+
.spyOn(appContext.workspacesService, 'getRootClusterUri')
21+
.mockReturnValue(clusterUri);
22+
23+
const { getByLabelText } = render(
24+
<MockAppContextProvider appContext={appContext}>
25+
<ShareFeedback onClose={undefined} />
26+
</MockAppContextProvider>
27+
);
28+
29+
expect(appContext.clustersService.findCluster).toHaveBeenCalledWith(
30+
clusterUri
31+
);
32+
expect(getByLabelText('Email Address')).toHaveValue('');
33+
});
34+
35+
test('email field is prefilled with the username if it looks like an email', () => {
36+
const appContext = new MockAppContext();
37+
const clusterUri = '/clusters/production';
38+
jest
39+
.spyOn(appContext.clustersService, 'findCluster')
40+
.mockImplementation(() => {
41+
return {
42+
loggedInUser: {
43+
name: 'bob@prod.com',
44+
},
45+
} as Cluster.AsObject;
46+
});
47+
48+
jest
49+
.spyOn(appContext.workspacesService, 'getRootClusterUri')
50+
.mockReturnValue(clusterUri);
51+
52+
const { getByLabelText } = render(
53+
<MockAppContextProvider appContext={appContext}>
54+
<ShareFeedback onClose={undefined} />
55+
</MockAppContextProvider>
56+
);
57+
58+
expect(appContext.clustersService.findCluster).toHaveBeenCalledWith(
59+
clusterUri
60+
);
61+
expect(getByLabelText('Email Address')).toHaveValue('bob@prod.com');
62+
});
63+
64+
test('onClose is called when close button is clicked', () => {
65+
const appContext = new MockAppContext();
66+
const handleClose = jest.fn();
67+
68+
const { getByTitle } = render(
69+
<MockAppContextProvider appContext={appContext}>
70+
<ShareFeedback onClose={handleClose} />
71+
</MockAppContextProvider>
72+
);
73+
74+
fireEvent.click(getByTitle('Close'));
75+
76+
expect(handleClose).toHaveBeenCalledTimes(1);
77+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import React, { useState } from 'react';
2+
import { ButtonIcon, ButtonPrimary, Flex, Link, Text } from 'design';
3+
import Validation from 'shared/components/Validation';
4+
import { Close } from 'design/Icon';
5+
import { ShareFeedbackForm } from './ShareFeedbackForm';
6+
import { ShareFeedbackFormValues } from './types';
7+
import { useAppContext } from 'teleterm/ui/appContextProvider';
8+
9+
interface ShareFeedbackProps {
10+
onClose(): void;
11+
}
12+
13+
export function ShareFeedback(props: ShareFeedbackProps) {
14+
const ctx = useAppContext();
15+
ctx.workspacesService.useState();
16+
ctx.clustersService.useState();
17+
18+
function getEmailFromUserName(): string {
19+
const cluster = ctx.clustersService.findCluster(
20+
ctx.workspacesService.getRootClusterUri()
21+
);
22+
const userName = cluster?.loggedInUser?.name;
23+
if (/^\S+@\S+$/.test(userName)) {
24+
return userName;
25+
}
26+
}
27+
28+
const [formValues, setFormValues] = useState<ShareFeedbackFormValues>({
29+
feedback: '',
30+
email: getEmailFromUserName() || '',
31+
newsletterEnabled: false,
32+
salesContactEnabled: false,
33+
});
34+
35+
return (
36+
<Flex bg="primary.main" p={3} borderRadius={3} maxWidth="370px">
37+
<Validation>
38+
{({ validator }) => (
39+
<Flex
40+
flexDirection="column"
41+
as="form"
42+
onSubmit={e => {
43+
e.preventDefault();
44+
if (validator.validate()) {
45+
console.log('submit', formValues); //TODO (gzdunek): connect to a real service
46+
}
47+
}}
48+
>
49+
<Flex justifyContent="space-between" mb={2}>
50+
<Text typography="h4" bold color="text.primary">
51+
Provide your feedback
52+
</Text>
53+
<ButtonIcon
54+
type="button"
55+
onClick={props.onClose}
56+
title="Close"
57+
color="text.secondary"
58+
>
59+
<Close fontSize={5} />
60+
</ButtonIcon>
61+
</Flex>
62+
<Link
63+
href="https://github.com/gravitational/teleport/issues/new?assignees=&labels=bug&template=bug_report.md"
64+
target="_blank"
65+
>
66+
Submit a Bug
67+
</Link>
68+
<Link href="https://goteleport.com/signup/" target="_blank">
69+
Try Teleport Cloud
70+
</Link>
71+
<ShareFeedbackForm
72+
formValues={formValues}
73+
setFormValues={setFormValues}
74+
/>
75+
<ButtonPrimary block type="submit" mt={4}>
76+
Submit
77+
</ButtonPrimary>
78+
</Flex>
79+
)}
80+
</Validation>
81+
</Flex>
82+
);
83+
}

0 commit comments

Comments
 (0)