Skip to content

Commit a970811

Browse files
authored
Add cli (codesandbox#28)
* Add fetching from github * Update components to fetch git repo * Apply linting rules * Remove redundant message check * Remove console statements * Remove duplicate import * Adjust url generation to use git * DRY up the statistics * Change url-generator to encode git paths * Prevent githubUrl from creating an empty text node * Remove unnecessary eslint-disable-lines * Add CLI authorization page * Fix wrong authorization when you need to sign in to GitHub * Change port check to strict check
1 parent 5e78936 commit a970811

File tree

7 files changed

+238
-41
lines changed

7 files changed

+238
-41
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@
113113
"redbox-react": "^1.4.2",
114114
"redux": "^3.7.0",
115115
"reselect": "^3.0.0",
116+
"sockjs-client": "^1.1.4",
116117
"store": "^2.0.4",
117118
"styled-components": "^2.1.0",
118119
"tern": "^0.21.0"

src/app/components/buttons/Button.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ import styled, { css, keyframes } from 'styled-components';
44

55
import theme from 'common/theme';
66

7-
const getBackgroundColor = ({ disabled }) => {
7+
const getBackgroundColor = ({ disabled, red }) => {
88
if (disabled) return `background: ${theme.background2.darken(0.1)()}`;
9+
if (red)
10+
return `background-image: linear-gradient(270deg, #F27777, #400000);`;
911
return `background-image: linear-gradient(270deg, #fed29d, #A58B66, #7abae8, #56a0d6);`;
1012
};
1113

@@ -57,7 +59,9 @@ const styles = css`
5759
user-select: none;
5860
text-decoration: none;
5961
60-
${props => !props.disabled && `
62+
${props =>
63+
!props.disabled &&
64+
`
6165
cursor: pointer;
6266
&:hover {
6367
animation-name: ${forward};

src/app/components/text/SubTitle.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ export default styled.h2`
1010
font-weight: 300;
1111
margin-top: 0;
1212
margin-bottom: 1.5rem;
13+
line-height: 1.4;
1314
`;

src/app/pages/CLI/index.js

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
// @flow
2+
import React from 'react';
3+
import styled from 'styled-components';
4+
import { connect } from 'react-redux';
5+
import { bindActionCreators } from 'redux';
6+
import SockJS from 'sockjs-client';
7+
8+
import type { CurrentUser } from 'common/types';
9+
10+
import Centered from 'app/components/flex/Centered';
11+
import Title from 'app/components/text/Title';
12+
import SubTitle from 'app/components/text/SubTitle';
13+
import Button from '../../components/buttons/Button';
14+
import { currentUserSelector } from '../../store/user/selectors';
15+
import userActionCreators from '../../store/user/actions';
16+
17+
const Buttons = styled.div`
18+
display: flex;
19+
20+
> button {
21+
margin: 1rem;
22+
}
23+
`;
24+
25+
type State = {
26+
success: boolean,
27+
loading: boolean,
28+
error: ?string,
29+
};
30+
31+
type Props = {
32+
user: CurrentUser,
33+
signIn: typeof userActionCreators.signIn,
34+
};
35+
36+
const mapStateToProps = state => ({
37+
user: currentUserSelector(state),
38+
});
39+
const mapDispatchToProps = dispatch => ({
40+
signIn: bindActionCreators(userActionCreators.signIn, dispatch),
41+
});
42+
class CLI extends React.PureComponent {
43+
props: Props;
44+
state: State = {
45+
success: false,
46+
loading: false,
47+
error: null,
48+
};
49+
50+
constructor(props) {
51+
super(props);
52+
53+
const port = this.getPort();
54+
if (port === null) {
55+
document.location.href = '/';
56+
return;
57+
}
58+
59+
this.port = port;
60+
}
61+
62+
port: number;
63+
64+
getPort = () => {
65+
const match = document.location.search.match(/\?port=(.*)/);
66+
if (match) {
67+
const portString = match[1];
68+
return +portString;
69+
}
70+
return null;
71+
};
72+
73+
$authorizeCLI = (user: CurrentUser) =>
74+
new Promise((resolve, reject) => {
75+
try {
76+
this.setState({ loading: true });
77+
const sock = new SockJS(`http://localhost:${this.port}/login`);
78+
let signedIn = false;
79+
80+
sock.onopen = () => {
81+
signedIn = true;
82+
sock.send(JSON.stringify(user));
83+
resolve();
84+
};
85+
86+
sock.onmessage = () => {
87+
sock.close();
88+
};
89+
90+
sock.onclose = () => {
91+
if (!signedIn) {
92+
reject(
93+
new Error(
94+
'Could not connect with the CLI, please send a message to @Ives13.',
95+
),
96+
);
97+
}
98+
};
99+
} catch (e) {
100+
reject(e);
101+
}
102+
});
103+
104+
authorize = async () => {
105+
try {
106+
await this.$authorizeCLI(this.props.user);
107+
108+
// Close window after succesfull authorization
109+
window.close();
110+
this.setState({ success: true });
111+
} catch (e) {
112+
this.setState({ error: e.message });
113+
}
114+
};
115+
116+
signIn = async () => {
117+
try {
118+
await this.props.signIn();
119+
await this.authorize();
120+
} catch (e) {
121+
this.setState({ error: e.message });
122+
}
123+
};
124+
125+
render() {
126+
const { user } = this.props;
127+
const { error, loading, success } = this.state;
128+
129+
if (success) {
130+
return (
131+
<Centered horizontal vertical>
132+
<Title>Succesfully authorized the CLI</Title>
133+
<SubTitle>You can now close this window</SubTitle>
134+
</Centered>
135+
);
136+
}
137+
138+
if (error) {
139+
return (
140+
<Centered horizontal vertical>
141+
<Title>An error occured:</Title>
142+
<SubTitle>{error}</SubTitle>
143+
<Buttons>
144+
<Button href="/">Go to homepage</Button>
145+
</Buttons>
146+
</Centered>
147+
);
148+
}
149+
150+
if (loading) {
151+
return (
152+
<Centered horizontal vertical>
153+
<Title>Authorizing...</Title>
154+
</Centered>
155+
);
156+
}
157+
158+
if (!user.jwt) {
159+
return (
160+
<Centered horizontal vertical>
161+
<Title>Welcome to CodeSandbox!</Title>
162+
<SubTitle>
163+
To use the CLI you need to sign in with your GitHub account.
164+
</SubTitle>
165+
<Buttons>
166+
<Button onClick={this.signIn}>Sign in with Github</Button>
167+
</Buttons>
168+
</Centered>
169+
);
170+
}
171+
172+
return (
173+
<Centered horizontal vertical>
174+
<Title>Hello {user.username}!</Title>
175+
<SubTitle>
176+
To make use of the CLI you need to authorize it first,<br /> you can
177+
authorize by pressing {"'"}login{"'"}.
178+
</SubTitle>
179+
<Buttons>
180+
<Button red>Cancel</Button>
181+
<Button onClick={this.authorize}>Login</Button>
182+
</Buttons>
183+
</Centered>
184+
);
185+
}
186+
}
187+
188+
export default connect(mapStateToProps, mapDispatchToProps)(CLI);

src/app/pages/index.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ const Profile = Loadable({
4646
loader: () => import('./Profile'),
4747
LoadingComponent: Loading,
4848
});
49+
const CLI = Loadable({
50+
loader: () => import('./CLI'),
51+
LoadingComponent: Loading,
52+
});
4953

5054
type Props = {
5155
hasLogin: boolean,
@@ -92,6 +96,7 @@ class Routes extends React.PureComponent {
9296
<Route path="/s/:id*" component={Sandbox} />
9397
<Route path="/signin/:jwt?" component={SignIn} />
9498
<Route path="/u/:username" component={Profile} />
99+
<Route path="/cli/login" component={CLI} />
95100
<Route component={NotFound} />
96101
</Switch>
97102
</Content>

src/app/store/user/actions.js

Lines changed: 24 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -48,35 +48,38 @@ const getCurrentUser = () => async (dispatch: Function, getState: Function) => {
4848
identify(data);
4949

5050
dispatch({ type: SET_CURRENT_USER, data });
51+
return data;
5152
} catch (e) {
5253
dispatch(signOut());
5354
}
5455
}
5556
};
5657

57-
const signIn = () => async (dispatch: Function) => {
58-
dispatch({
59-
type: SIGN_IN,
60-
});
61-
const popup = openPopup(signInUrl(), 'sign in');
62-
63-
window.addEventListener('message', function(e) {
64-
if (e.data.type === 'signin') {
65-
const jwt = e.data.data.jwt;
66-
window.removeEventListener('message', this);
67-
popup.close();
68-
69-
if (jwt) {
70-
dispatch({
71-
type: SIGN_IN_SUCCESFULL,
72-
jwt,
73-
});
74-
75-
dispatch(getCurrentUser());
58+
const signIn = () => (dispatch: Function) =>
59+
new Promise((resolve, reject) => {
60+
dispatch({
61+
type: SIGN_IN,
62+
});
63+
const popup = openPopup(signInUrl(), 'sign in');
64+
65+
window.addEventListener('message', function(e) {
66+
if (e.data.type === 'signin') {
67+
const jwt = e.data.data.jwt;
68+
window.removeEventListener('message', this);
69+
popup.close();
70+
71+
if (jwt) {
72+
dispatch({
73+
type: SIGN_IN_SUCCESFULL,
74+
jwt,
75+
});
76+
resolve(dispatch(getCurrentUser()));
77+
} else {
78+
reject();
79+
}
7680
}
77-
}
81+
});
7882
});
79-
};
8083

8184
const loadUserSandboxes = () => async (dispatch: Function) => {
8285
const { data } = await dispatch(doRequest(LOAD_USER_SANDBOXES, `sandboxes`));
@@ -105,22 +108,6 @@ const sendFeedback = (message: string) => async (dispatch: Function) => {
105108
);
106109
};
107110

108-
// const updateCurrentUser = (user: Object) => async (dispatch: Function) => {
109-
// await dispatch(
110-
// doRequest(UPDATE_CURRENT_USER_API, `users/current/update`, {
111-
// method: 'POST',
112-
// body: {
113-
// user,
114-
// },
115-
// }),
116-
// );
117-
// };
118-
119-
// const setShowcasedSandbox = (showcasedSandboxId: string) =>
120-
// updateCurrentUser({
121-
// showcasedSandboxShortid: showcasedSandboxId,
122-
// });
123-
124111
export default {
125112
signOut,
126113
signIn,

yarn.lock

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2054,7 +2054,7 @@ [email protected]:
20542054
dependencies:
20552055
ms "2.0.0"
20562056

2057-
debug@^2.1.1, debug@^2.2.0, debug@^2.4.5, debug@^2.6.0, debug@^2.6.3, debug@^2.6.8:
2057+
debug@^2.1.1, debug@^2.2.0, debug@^2.4.5, debug@^2.6.0, debug@^2.6.3, debug@^2.6.6, debug@^2.6.8:
20582058
version "2.6.8"
20592059
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.8.tgz#e731531ca2ede27d188222427da17821d68ff4fc"
20602060
dependencies:
@@ -6618,6 +6618,17 @@ [email protected]:
66186618
json3 "^3.3.2"
66196619
url-parse "^1.1.1"
66206620

6621+
sockjs-client@^1.1.4:
6622+
version "1.1.4"
6623+
resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.1.4.tgz#5babe386b775e4cf14e7520911452654016c8b12"
6624+
dependencies:
6625+
debug "^2.6.6"
6626+
eventsource "0.1.6"
6627+
faye-websocket "~0.11.0"
6628+
inherits "^2.0.1"
6629+
json3 "^3.3.2"
6630+
url-parse "^1.1.8"
6631+
66216632
66226633
version "0.3.18"
66236634
resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.18.tgz#d9b289316ca7df77595ef299e075f0f937eb4207"
@@ -7174,7 +7185,7 @@ [email protected]:
71747185
querystringify "0.0.x"
71757186
requires-port "1.0.x"
71767187

7177-
url-parse@^1.1.1:
7188+
url-parse@^1.1.1, url-parse@^1.1.8:
71787189
version "1.1.8"
71797190
resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.1.8.tgz#7a65b3a8d57a1e86af6b4e2276e34774167c0156"
71807191
dependencies:

0 commit comments

Comments
 (0)