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

Commit1b9ea3c

Browse files
committed
feat: add port forward dropdown component
1 parentd1496ed commit1b9ea3c

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={document.body}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: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import{screen}from"@testing-library/react"
2+
importReactfrom"react"
3+
import{render}from"../../testHelpers/renderHelpers"
4+
import{Language,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={document.body}/>)
26+
27+
// Then
28+
letportNameElement=awaitscreen.queryByText(Language.portListing(22,"sshd"))
29+
expect(portNameElement).toBeNull()
30+
portNameElement=awaitscreen.findByText(Language.portListing(8080,"code-server"))
31+
expect(portNameElement).toBeDefined()
32+
})
33+
})
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
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+
exportconstLanguage={
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+
portListing:(port:number,name:string)=>`${port} (${name})`,
23+
portInputLabel:"Port",
24+
formButtonText:"Open URL",
25+
}
26+
27+
exporttypePortForwardDropdownProps=Pick<PopoverProps,"onClose"|"open"|"anchorEl">&{
28+
/**
29+
* The netstat response to render. Undefined is taken to mean "loading".
30+
*/
31+
netstat?:NetstatResponse
32+
/**
33+
* Given a port return the URL for accessing that port.
34+
*/
35+
urlFormatter:(port:number|string)=>string
36+
}
37+
38+
constportFilter=({ port}:NetstatPort):boolean=>{
39+
if(port===443||port===80){
40+
// These are standard HTTP ports.
41+
returntrue
42+
}elseif(port<=1023){
43+
// Assume a privileged port is probably not being used for HTTP. This will
44+
// catch things like sshd.
45+
returnfalse
46+
}
47+
returntrue
48+
}
49+
50+
exportconstPortForwardDropdown:React.FC<PortForwardDropdownProps>=({ netstat, open, urlFormatter, ...rest})=>{
51+
conststyles=useStyles()
52+
const[port,setPort]=useState<number|string>(3000)
53+
constports=netstat?.ports?.filter(portFilter)
54+
55+
return(
56+
<Popover
57+
open={!!open}
58+
transformOrigin={{
59+
vertical:"top",
60+
horizontal:"center",
61+
}}
62+
anchorOrigin={{
63+
vertical:"bottom",
64+
horizontal:"center",
65+
}}
66+
{...rest}
67+
>
68+
<divclassName={styles.root}>
69+
<Typographyvariant="h6"className={styles.title}>
70+
{Language.title}
71+
</Typography>
72+
73+
<TypographyclassName={styles.paragraph}>{Language.automaticPortText}</Typography>
74+
75+
{typeofnetstat==="undefined"&&(
76+
<divclassName={styles.loader}>
77+
<CircularProgresssize="1rem"/>
78+
</div>
79+
)}
80+
81+
{netstat?.error&&<Alertseverity="error">{netstat.error}</Alert>}
82+
83+
{ports&&ports.length>0&&(
84+
<divclassName={styles.ports}>
85+
{ports.map(({ port, name})=>(
86+
<LinkclassName={styles.portLink}key={port}href={urlFormatter(port)}target="_blank">
87+
<OpenInNewIcon/>
88+
{Language.portListing(port,name)}
89+
</Link>
90+
))}
91+
</div>
92+
)}
93+
94+
{ports&&ports.length===0&&<Alertseverity="info">No HTTP ports were detected.</Alert>}
95+
96+
<TypographyclassName={styles.paragraph}>{Language.manualPortText}</Typography>
97+
98+
<CodeExamplecode={urlFormatter(port)}/>
99+
100+
<TypographyclassName={styles.paragraph}>{Language.formPortText}</Typography>
101+
102+
<Stackdirection="row">
103+
<TextField
104+
className={styles.textField}
105+
onChange={(event)=>setPort(event.target.value)}
106+
value={port}
107+
autoFocus
108+
label={Language.portInputLabel}
109+
variant="outlined"
110+
/>
111+
<Buttoncomponent={Link}href={urlFormatter(port)}target="_blank"className={styles.linkButton}>
112+
{Language.formButtonText}
113+
</Button>
114+
</Stack>
115+
</div>
116+
</Popover>
117+
)
118+
}
119+
120+
constuseStyles=makeStyles((theme)=>({
121+
root:{
122+
padding:`${theme.spacing(3)}px`,
123+
maxWidth:500,
124+
},
125+
title:{
126+
fontWeight:600,
127+
},
128+
ports:{
129+
margin:`${theme.spacing(2)}px 0`,
130+
},
131+
portLink:{
132+
alignItems:"center",
133+
color:theme.palette.text.secondary,
134+
display:"flex",
135+
136+
"& svg":{
137+
width:16,
138+
height:16,
139+
marginRight:theme.spacing(1.5),
140+
},
141+
},
142+
loader:{
143+
margin:`${theme.spacing(2)}px 0`,
144+
textAlign:"center",
145+
},
146+
paragraph:{
147+
color:theme.palette.text.secondary,
148+
margin:`${theme.spacing(2)}px 0`,
149+
},
150+
textField:{
151+
flex:1,
152+
margin:0,
153+
},
154+
linkButton:{
155+
color:"inherit",
156+
flex:1,
157+
158+
"&:hover":{
159+
textDecoration:"none",
160+
},
161+
},
162+
}))

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp