Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit93f0422

Browse files
committed
feat: add port forward dropdown component
1 parent7aad88c commit93f0422

File tree

4 files changed

+291
-0
lines changed

4 files changed

+291
-0
lines changed

‎site/src/api/types.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,14 @@ export interface ReconnectingPTYRequest {
1414
exporttypeWorkspaceBuildTransition="start"|"stop"|"delete"
1515

1616
exporttypeMessage={message:string}
17+
18+
exportinterfaceNetstatPort{
19+
name:string
20+
port:number
21+
}
22+
23+
exportinterfaceNetstatResponse{
24+
readonlyports?:NetstatPort[]
25+
readonlyerror?:string
26+
readonlytook?:number
27+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import{Story}from"@storybook/react"
2+
importReactfrom"react"
3+
import{PortForwardDropdown,PortForwardDropdownProps}from"./PortForwardDropdown"
4+
5+
exportdefault{
6+
title:"components/PortForwardDropdown",
7+
component:PortForwardDropdown,
8+
}
9+
10+
constTemplate:Story<PortForwardDropdownProps>=(args:PortForwardDropdownProps)=>(
11+
<PortForwardDropdownanchorEl={null}urlFormatter={urlFormatter}open{...args}/>
12+
)
13+
14+
consturlFormatter=(port:number|string):string=>{
15+
return`https://${port}--user--workspace.coder.com`
16+
}
17+
18+
exportconstError=Template.bind({})
19+
Error.args={
20+
netstat:{
21+
error:"Unable to get listening ports",
22+
},
23+
}
24+
25+
exportconstLoading=Template.bind({})
26+
Loading.args={}
27+
28+
exportconstNone=Template.bind({})
29+
None.args={
30+
netstat:{
31+
ports:[],
32+
},
33+
}
34+
35+
exportconstExcluded=Template.bind({})
36+
Excluded.args={
37+
netstat:{
38+
ports:[
39+
{
40+
name:"sshd",
41+
port:22,
42+
},
43+
],
44+
},
45+
}
46+
47+
exportconstSingle=Template.bind({})
48+
Single.args={
49+
netstat:{
50+
ports:[
51+
{
52+
name:"code-server",
53+
port:8080,
54+
},
55+
],
56+
},
57+
}
58+
59+
exportconstMultiple=Template.bind({})
60+
Multiple.args={
61+
netstat:{
62+
ports:[
63+
{
64+
name:"code-server",
65+
port:8080,
66+
},
67+
{
68+
name:"coder",
69+
port:8000,
70+
},
71+
{
72+
name:"coder",
73+
port:3000,
74+
},
75+
{
76+
name:"node",
77+
port:8001,
78+
},
79+
{
80+
name:"sshd",
81+
port:22,
82+
},
83+
],
84+
},
85+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import{screen}from"@testing-library/react"
2+
importReactfrom"react"
3+
import{render}from"../../testHelpers/renderHelpers"
4+
import{PortForwardDropdown}from"./PortForwardDropdown"
5+
6+
consturlFormatter=(port:number|string):string=>{
7+
return`https://${port}--user--workspace.coder.com`
8+
}
9+
10+
describe("PortForwardDropdown",()=>{
11+
it("skips known non-http ports",async()=>{
12+
// When
13+
constnetstat={
14+
ports:[
15+
{
16+
name:"sshd",
17+
port:22,
18+
},
19+
{
20+
name:"code-server",
21+
port:8080,
22+
},
23+
],
24+
}
25+
render(<PortForwardDropdownurlFormatter={urlFormatter}opennetstat={netstat}anchorEl={null}/>)
26+
27+
// Then
28+
letportNameElement=awaitscreen.findByText("sshd")
29+
expect(portNameElement).not.toBeDefined()
30+
31+
portNameElement=awaitscreen.findByText("code-server")
32+
expect(portNameElement).toBeDefined()
33+
})
34+
})
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
importButtonfrom"@material-ui/core/Button"
2+
importCircularProgressfrom"@material-ui/core/CircularProgress"
3+
importLinkfrom"@material-ui/core/Link"
4+
importPopover,{PopoverProps}from"@material-ui/core/Popover"
5+
import{makeStyles}from"@material-ui/core/styles"
6+
importTextFieldfrom"@material-ui/core/TextField"
7+
importTypographyfrom"@material-ui/core/Typography"
8+
importOpenInNewIconfrom"@material-ui/icons/OpenInNew"
9+
importAlertfrom"@material-ui/lab/Alert"
10+
importReact,{useState}from"react"
11+
import{NetstatPort,NetstatResponse}from"../../api/types"
12+
import{CodeExample}from"../CodeExample/CodeExample"
13+
import{Stack}from"../Stack/Stack"
14+
15+
constLanguage={
16+
title:"Port forward",
17+
automaticPortText:
18+
"Here are the applications we detected are listening on ports in this resource. Click to open them in a new tab.",
19+
manualPortText:
20+
"You can manually port forward this resource by typing the port and your username in the URL like below.",
21+
formPortText:"Or you can use the following form to open the port in a new tab.",
22+
portInputLabel:"Port",
23+
formButtonText:"Open URL",
24+
}
25+
26+
exporttypePortForwardDropdownProps=Pick<PopoverProps,"onClose"|"open"|"anchorEl">&{
27+
/**
28+
* The netstat response to render. Undefined is taken to mean "loading".
29+
*/
30+
netstat?:NetstatResponse
31+
/**
32+
* Given a port return the URL for accessing that port.
33+
*/
34+
urlFormatter:(port:number|string)=>string
35+
}
36+
37+
constportFilter=({ port}:NetstatPort):boolean=>{
38+
if(port===443||port===80){
39+
// These are standard HTTP ports.
40+
returntrue
41+
}elseif(port<=1023){
42+
// Assume a privileged port is probably not being used for HTTP. This will
43+
// catch things like sshd.
44+
returnfalse
45+
}
46+
returntrue
47+
}
48+
49+
exportconstPortForwardDropdown:React.FC<PortForwardDropdownProps>=({ netstat, open, urlFormatter, ...rest})=>{
50+
conststyles=useStyles()
51+
const[port,setPort]=useState<number|string>(3000)
52+
constports=netstat?.ports?.filter(portFilter)
53+
54+
return(
55+
<Popover
56+
open={!!open}
57+
transformOrigin={{
58+
vertical:"top",
59+
horizontal:"center",
60+
}}
61+
anchorOrigin={{
62+
vertical:"bottom",
63+
horizontal:"center",
64+
}}
65+
{...rest}
66+
>
67+
<divclassName={styles.root}>
68+
<Typographyvariant="h6"className={styles.title}>
69+
{Language.title}
70+
</Typography>
71+
72+
<TypographyclassName={styles.paragraph}>{Language.automaticPortText}</Typography>
73+
74+
{typeofnetstat==="undefined"&&(
75+
<divclassName={styles.loader}>
76+
<CircularProgresssize="1rem"/>
77+
</div>
78+
)}
79+
80+
{netstat?.error&&<Alertseverity="error">{netstat.error}</Alert>}
81+
82+
{ports&&ports.length>0&&(
83+
<divclassName={styles.ports}>
84+
{ports.map(({ port, name})=>(
85+
<LinkclassName={styles.portLink}key={port}href={urlFormatter(port)}target="_blank">
86+
<OpenInNewIcon/>
87+
{port} ({name})
88+
</Link>
89+
))}
90+
</div>
91+
)}
92+
93+
{ports&&ports.length===0&&<Alertseverity="info">No HTTP ports were detected.</Alert>}
94+
95+
<TypographyclassName={styles.paragraph}>{Language.manualPortText}</Typography>
96+
97+
<CodeExamplecode={urlFormatter(port)}/>
98+
99+
<TypographyclassName={styles.paragraph}>{Language.formPortText}</Typography>
100+
101+
<Stackdirection="row">
102+
<TextField
103+
className={styles.textField}
104+
onChange={(event)=>setPort(event.target.value)}
105+
value={port}
106+
autoFocus
107+
label={Language.portInputLabel}
108+
variant="outlined"
109+
/>
110+
<Buttoncomponent={Link}href={urlFormatter(port)}target="_blank"className={styles.linkButton}>
111+
{Language.formButtonText}
112+
</Button>
113+
</Stack>
114+
</div>
115+
</Popover>
116+
)
117+
}
118+
119+
constuseStyles=makeStyles((theme)=>({
120+
root:{
121+
padding:`${theme.spacing(3)}px`,
122+
maxWidth:500,
123+
},
124+
title:{
125+
fontWeight:600,
126+
},
127+
ports:{
128+
margin:`${theme.spacing(2)}px 0`,
129+
},
130+
portLink:{
131+
alignItems:"center",
132+
color:theme.palette.text.secondary,
133+
display:"flex",
134+
135+
"& svg":{
136+
width:16,
137+
height:16,
138+
marginRight:theme.spacing(1.5),
139+
},
140+
},
141+
loader:{
142+
margin:`${theme.spacing(2)}px 0`,
143+
textAlign:"center",
144+
},
145+
paragraph:{
146+
color:theme.palette.text.secondary,
147+
margin:`${theme.spacing(2)}px 0`,
148+
},
149+
textField:{
150+
flex:1,
151+
margin:0,
152+
},
153+
linkButton:{
154+
color:"inherit",
155+
flex:1,
156+
157+
"&:hover":{
158+
textDecoration:"none",
159+
},
160+
},
161+
}))

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp