Skip to content

Commit 9f4ac84

Browse files
authored
Merge pull request #5661 from getsentry/ui/expand-tests
ui: various form improvements
2 parents 37abaa3 + de9723a commit 9f4ac84

21 files changed

+748
-264
lines changed

.eslintrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"require": false,
1515
"expect": false,
1616
"sinon": false,
17+
"MockApiClient": true,
1718
"TestStubs": true,
1819
"Raven": true,
1920
"jest": true

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,5 +104,8 @@
104104
"sinon": "1.17.2",
105105
"sinon-chai": "2.8.0",
106106
"webpack-livereload-plugin": "^0.11.0"
107+
},
108+
"optionalDependencies": {
109+
"fsevents": "^1.1.2"
107110
}
108111
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
export class Request {}
2+
3+
export class Client {
4+
static mockResponses = [];
5+
6+
static clearMockResponses() {
7+
Client.mockResponses = [];
8+
}
9+
10+
static addMockResponse(response) {
11+
Client.mockResponses.push({
12+
statusCode: 200,
13+
body: '',
14+
method: 'GET',
15+
...response
16+
});
17+
}
18+
19+
static findMockResponse(url, options) {
20+
return Client.mockResponses.find(response => {
21+
return url === response.url && (options.method || 'GET') === response.method;
22+
});
23+
}
24+
25+
request(url, options) {
26+
let response = Client.findMockResponse(url, options);
27+
if (!response) {
28+
console.error(
29+
'No mocked response found for request.',
30+
url,
31+
options.method || 'GET'
32+
);
33+
options.error &&
34+
options.error({
35+
status: 404,
36+
responseText: 'HTTP 404',
37+
responseJSON: null
38+
});
39+
} else if (response.statusCode !== 200) {
40+
options.error &&
41+
options.error({
42+
status: response.statusCode,
43+
responseText: JSON.stringify(response.body),
44+
responseJSON: response.body
45+
});
46+
} else {
47+
options.success && options.success(response.body);
48+
}
49+
options.complete && options.complete();
50+
}
51+
}

src/sentry/static/sentry/app/components/forms/rangeField.jsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ export default class RangeField extends InputField {
2121

2222
static defaultProps = {
2323
...InputField.defaultProps,
24-
onChange: value => {},
2524
formatLabel: value => value,
2625
min: 0,
2726
max: 100,
@@ -55,7 +54,7 @@ export default class RangeField extends InputField {
5554
.on('slider:changed', (e, data) => {
5655
let value = parseInt(data.value, 10);
5756
$value.html(this.props.formatLabel(value));
58-
this.props.onChange(value);
57+
this.setValue(value);
5958
})
6059
.simpleSlider({
6160
value: this.props.defaultValue || this.props.value,

src/sentry/static/sentry/app/views/asyncView.jsx

Lines changed: 58 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,18 @@ class AsyncView extends React.Component {
3333

3434
// XXX: cant call this getInitialState as React whines
3535
getDefaultState() {
36-
return {
37-
data: null,
36+
let endpoints = this.getEndpoints();
37+
let state = {
38+
// has all data finished requesting?
3839
loading: true,
39-
error: false
40+
// is there an error loading ANY data?
41+
error: false,
42+
errors: {}
4043
};
44+
endpoints.forEach(([stateKey, endpoint]) => {
45+
state[stateKey] = null;
46+
});
47+
return state;
4148
}
4249

4350
remountComponent() {
@@ -46,41 +53,74 @@ class AsyncView extends React.Component {
4653

4754
// TODO(dcramer): we'd like to support multiple initial api requests
4855
fetchData() {
49-
let endpoint = this.getEndpoint();
50-
if (!endpoint) {
56+
let endpoints = this.getEndpoints();
57+
if (!endpoints.length) {
5158
this.setState({
5259
loading: false,
5360
error: false
5461
});
55-
} else {
62+
return;
63+
}
64+
// TODO(dcramer): this should cancel any existing API requests
65+
this.setState({
66+
loading: true,
67+
error: false,
68+
remainingRequests: endpoints.length
69+
});
70+
endpoints.forEach(([stateKey, endpoint, params]) => {
5671
this.api.request(endpoint, {
5772
method: 'GET',
58-
params: this.getEndpointParams(),
73+
params: params,
5974
success: (data, _, jqXHR) => {
60-
this.setState({
61-
loading: false,
62-
error: false,
63-
data: data
75+
this.setState(prevState => {
76+
return {
77+
[stateKey]: data,
78+
remainingRequests: prevState.remainingRequests - 1,
79+
loading: prevState.remainingRequests > 1
80+
};
6481
});
6582
},
6683
error: error => {
67-
this.setState({
68-
loading: false,
69-
error: error
84+
this.setState(prevState => {
85+
return {
86+
[stateKey]: null,
87+
errors: {
88+
...prevState.errors,
89+
[stateKey]: error
90+
},
91+
remainingRequests: prevState.remainingRequests - 1,
92+
loading: prevState.remainingRequests > 1,
93+
error: true
94+
};
7095
});
7196
}
7297
});
73-
}
98+
});
7499
}
75100

101+
// DEPRECATED: use getEndpoints()
76102
getEndpointParams() {
77103
return {};
78104
}
79105

106+
// DEPRECATED: use getEndpoints()
80107
getEndpoint() {
81108
return null;
82109
}
83110

111+
/**
112+
* Return a list of endpoint queries to make.
113+
*
114+
* return [
115+
* ['stateKeyName', '/endpoint/', {optional: 'query params'}]
116+
* ]
117+
*/
118+
getEndpoints() {
119+
let endpoint = this.getEndpoint();
120+
if (!endpoint) return [];
121+
return [['data', endpoint, this.getEndpointParams()]];
122+
}
123+
84124
getTitle() {
85125
return 'Sentry';
86126
}
@@ -98,7 +138,9 @@ class AsyncView extends React.Component {
98138
<DocumentTitle title={this.getTitle()}>
99139
{this.state.loading
100140
? this.renderLoading()
101-
: this.state.error ? this.renderError(this.state.error) : this.renderBody()}
141+
: this.state.error
142+
? this.renderError(new Error('Unable to load all required endpoints'))
143+
: this.renderBody()}
102144
</DocumentTitle>
103145
);
104146
}

src/sentry/static/sentry/app/views/organizationCreate.jsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ export default class OrganizationCreate extends AsyncView {
3232
submitLabel={t('Create Organization')}
3333
apiEndpoint="/organizations/"
3434
apiMethod="POST"
35-
onSubmitSuccess={this.onSubmitSuccess}>
35+
onSubmitSuccess={this.onSubmitSuccess}
36+
requireChanges={true}>
3637
<TextField
3738
name="name"
3839
label={t('Organization Name')}

0 commit comments

Comments
 (0)