Commit 77350d96 authored by Phil Hughes's avatar Phil Hughes

Merge branch '39727-axios-all-the-things' into 'master'

Use axios instead of vue resource - step 1

See merge request gitlab-org/gitlab-ce!15339
parents 4f09d099 9400ed3b
import axios from 'axios'; import axios from '../../lib/utils/axios_utils';
import setAxiosCsrfToken from '../../lib/utils/axios_utils';
export default class ClusterService { export default class ClusterService {
constructor(options = {}) { constructor(options = {}) {
setAxiosCsrfToken();
this.options = options; this.options = options;
this.appInstallEndpointMap = { this.appInstallEndpointMap = {
helm: this.options.installHelmEndpoint, helm: this.options.installHelmEndpoint,
...@@ -18,7 +15,6 @@ export default class ClusterService { ...@@ -18,7 +15,6 @@ export default class ClusterService {
} }
installApplication(appId) { installApplication(appId) {
const endpoint = this.appInstallEndpointMap[appId]; return axios.post(this.appInstallEndpointMap[appId]);
return axios.post(endpoint);
} }
} }
...@@ -29,8 +29,8 @@ export default class JobMediator { ...@@ -29,8 +29,8 @@ export default class JobMediator {
this.poll = new Poll({ this.poll = new Poll({
resource: this.service, resource: this.service,
method: 'getJob', method: 'getJob',
successCallback: this.successCallback.bind(this), successCallback: response => this.successCallback(response),
errorCallback: this.errorCallback.bind(this), errorCallback: () => this.errorCallback(),
}); });
if (!Visibility.hidden()) { if (!Visibility.hidden()) {
...@@ -57,7 +57,7 @@ export default class JobMediator { ...@@ -57,7 +57,7 @@ export default class JobMediator {
successCallback(response) { successCallback(response) {
this.state.isLoading = false; this.state.isLoading = false;
return response.json().then(data => this.store.storeJob(data)); return this.store.storeJob(response.data);
} }
errorCallback() { errorCallback() {
......
import Vue from 'vue'; import axios from '../../lib/utils/axios_utils';
import VueResource from 'vue-resource';
Vue.use(VueResource);
export default class JobService { export default class JobService {
constructor(endpoint) { constructor(endpoint) {
this.job = Vue.resource(endpoint); this.job = endpoint;
} }
getJob() { getJob() {
return this.job.get(); return axios.get(this.job);
} }
} }
import axios from 'axios'; import axios from 'axios';
import csrf from './csrf'; import csrf from './csrf';
export default function setAxiosCsrfToken() { axios.defaults.headers.common[csrf.headerKey] = csrf.token;
axios.defaults.headers.common[csrf.headerKey] = csrf.token;
} // Maintain a global counter for active requests
// see: spec/support/wait_for_requests.rb
axios.interceptors.request.use((config) => {
window.activeVueResources = window.activeVueResources || 0;
window.activeVueResources += 1;
return config;
});
// Remove the global counter
axios.interceptors.response.use((config) => {
window.activeVueResources -= 1;
return config;
});
export default axios;
...@@ -3,7 +3,9 @@ import { normalizeHeaders } from './common_utils'; ...@@ -3,7 +3,9 @@ import { normalizeHeaders } from './common_utils';
/** /**
* Polling utility for handling realtime updates. * Polling utility for handling realtime updates.
* Service for vue resouce and method need to be provided as props * Requirements: Promise based HTTP client
*
* Service for promise based http client and method need to be provided as props
* *
* @example * @example
* new Poll({ * new Poll({
......
# Axios
We use [axios][axios] to communicate with the server in Vue applications and most new code.
In order to guarantee all defaults are set you *should not use `axios` directly*, you should import `axios` from `axios_utils`.
## CSRF token
All our request require a CSRF token.
To guarantee this token is set, we are importing [axios][axios], setting the token, and exporting `axios` .
This exported module should be used instead of directly using `axios` to ensure the token is set.
## Usage
```javascript
import axios from '~/lib/utils/axios_utils';
axios.get(url)
.then((response) => {
// `data` is the response that was provided by the server
const data = response.data;
// `headers` the headers that the server responded with
// All header names are lower cased
const paginationData = response.headers;
})
.catch(() => {
//handle the error
});
```
## Mock axios response on tests
To help us mock the responses we need we use [axios-mock-adapter][axios-mock-adapter]
```javascript
import axios from '~/lib/utils/axios_utils';
import MockAdapter from 'axios-mock-adapter';
let mock;
beforeEach(() => {
// This sets the mock adapter on the default instance
mock = new MockAdapter(axios);
// Mock any GET request to /users
// arguments for reply are (status, data, headers)
mock.onGet('/users').reply(200, {
users: [
{ id: 1, name: 'John Smith' }
]
});
});
afterEach(() => {
mock.reset();
});
```
### Mock poll requests on tests with axios
Because polling function requires an header object, we need to always include an object as the third argument:
```javascript
mock.onGet('/users').reply(200, { foo: 'bar' }, {});
```
[axios]: https://github.com/axios/axios
[axios-instance]: #creating-an-instance
[axios-interceptors]: https://github.com/axios/axios#interceptors
[axios-mock-adapter]: https://github.com/ctimmerm/axios-mock-adapter
...@@ -71,8 +71,8 @@ Vue specific design patterns and practices. ...@@ -71,8 +71,8 @@ Vue specific design patterns and practices.
--- ---
## [Vue Resource](vue_resource.md) ## [Axios](axios.md)
Vue resource specific practices and gotchas. Axios specific practices and gotchas.
## [Icons](icons.md) ## [Icons](icons.md)
How we use SVG for our Icons. How we use SVG for our Icons.
......
...@@ -178,16 +178,13 @@ itself, please read this guide: [State Management][state-management] ...@@ -178,16 +178,13 @@ itself, please read this guide: [State Management][state-management]
The Service is a class used only to communicate with the server. The Service is a class used only to communicate with the server.
It does not store or manipulate any data. It is not aware of the store or the components. It does not store or manipulate any data. It is not aware of the store or the components.
We use [vue-resource][vue-resource-repo] to communicate with the server. We use [axios][axios] to communicate with the server.
Refer to [vue resource](vue_resource.md) for more details. Refer to [axios](axios.md) for more details.
Vue Resource should only be imported in the service file. Axios instance should only be imported in the service file.
```javascript ```javascript
import Vue from 'vue'; import axios from 'javascripts/lib/utils/axios_utils';
import VueResource from 'vue-resource';
Vue.use(VueResource);
``` ```
### End Result ### End Result
...@@ -230,15 +227,14 @@ export default class Store { ...@@ -230,15 +227,14 @@ export default class Store {
} }
// service.js // service.js
import Vue from 'vue'; import axios from 'javascripts/lib/utils/axios_utils'
import VueResource from 'vue-resource';
import 'vue_shared/vue_resource_interceptor';
Vue.use(VueResource);
export default class Service { export default class Service {
constructor(options) { constructor(options) {
this.todos = Vue.resource(endpoint.todosEndpoint); this.todos = axios.create({
baseURL: endpoint.todosEndpoint
});
} }
getTodos() { getTodos() {
...@@ -477,50 +473,8 @@ The main return value of a Vue component is the rendered output. In order to tes ...@@ -477,50 +473,8 @@ The main return value of a Vue component is the rendered output. In order to tes
need to test the rendered output. [Vue][vue-test] guide's to unit test show us exactly that: need to test the rendered output. [Vue][vue-test] guide's to unit test show us exactly that:
### Stubbing API responses ### Stubbing API responses
[Vue Resource Interceptors][vue-resource-interceptor] allow us to add a interceptor with Refer to [mock axios](axios.md#mock-axios-response-on-tests)
the response we need:
```javascript
// Mock the service to return data
const interceptor = (request, next) => {
next(request.respondWith(JSON.stringify([{
title: 'This is a todo',
body: 'This is the text'
}]), {
status: 200,
}));
};
beforeEach(() => {
Vue.http.interceptors.push(interceptor);
});
afterEach(() => {
Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor);
});
it('should do something', (done) => {
setTimeout(() => {
// Test received data
done();
}, 0);
});
```
1. Headers interceptor
Refer to [this section](vue.md#headers)
1. Use `$.mount()` to mount the component
```javascript
// bad
new Component({
el: document.createElement('div')
});
// good
new Component().$mount();
```
## Vuex ## Vuex
To manage the state of an application you may use [Vuex][vuex-docs]. To manage the state of an application you may use [Vuex][vuex-docs].
...@@ -721,7 +675,6 @@ describe('component', () => { ...@@ -721,7 +675,6 @@ describe('component', () => {
[component-system]: https://vuejs.org/v2/guide/#Composing-with-Components [component-system]: https://vuejs.org/v2/guide/#Composing-with-Components
[state-management]: https://vuejs.org/v2/guide/state-management.html#Simple-State-Management-from-Scratch [state-management]: https://vuejs.org/v2/guide/state-management.html#Simple-State-Management-from-Scratch
[one-way-data-flow]: https://vuejs.org/v2/guide/components.html#One-Way-Data-Flow [one-way-data-flow]: https://vuejs.org/v2/guide/components.html#One-Way-Data-Flow
[vue-resource-interceptor]: https://github.com/pagekit/vue-resource/blob/develop/docs/http.md#interceptors
[vue-test]: https://vuejs.org/v2/guide/unit-testing.html [vue-test]: https://vuejs.org/v2/guide/unit-testing.html
[issue-boards-service]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/assets/javascripts/boards/services/board_service.js.es6 [issue-boards-service]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/assets/javascripts/boards/services/board_service.js.es6
[flux]: https://facebook.github.io/flux [flux]: https://facebook.github.io/flux
...@@ -729,3 +682,6 @@ describe('component', () => { ...@@ -729,3 +682,6 @@ describe('component', () => {
[vuex-structure]: https://vuex.vuejs.org/en/structure.html [vuex-structure]: https://vuex.vuejs.org/en/structure.html
[vuex-mutations]: https://vuex.vuejs.org/en/mutations.html [vuex-mutations]: https://vuex.vuejs.org/en/mutations.html
[vuex-testing]: https://vuex.vuejs.org/en/testing.html [vuex-testing]: https://vuex.vuejs.org/en/testing.html
[axios]: https://github.com/axios/axios
[axios-interceptors]: https://github.com/axios/axios#interceptors
# Vue Resouce
In Vue applications we use [vue-resource][vue-resource-repo] to communicate with the server.
## HTTP Status Codes
### `.json()`
When making a request to the server, you will most likely need to access the body of the response.
Use `.json()` to convert. Because `.json()` returns a Promise the follwoing structure should be used:
```javascript
service.get('url')
.then(resp => resp.json())
.then((data) => {
this.store.storeData(data);
})
.catch(() => new Flash('Something went wrong'));
```
When using `Poll` (`app/assets/javascripts/lib/utils/poll.js`), the `successCallback` needs to handle `.json()` as a Promise:
```javascript
successCallback: (response) => {
return response.json().then((data) => {
// handle the response
});
}
```
### 204
Some endpoints - usually `delete` endpoints - return `204` as the success response.
When handling `204 - No Content` responses, we cannot use `.json()` since it tries to parse the non-existant body content.
When handling `204` responses, do not use `.json`, otherwise the promise will throw an error and will enter the `catch` statement:
```javascript
Vue.http.delete('path')
.then(() => {
// success!
})
.catch(() => {
// handle error
})
```
## Headers
Headers are being parsed into a plain object in an interceptor.
In Vue-resource 1.x `headers` object was changed into an `Headers` object. In order to not change all old code, an interceptor was added.
If you need to write a unit test that takes the headers in consideration, you need to include an interceptor to parse the headers after your test interceptor.
You can see an example in `spec/javascripts/environments/environment_spec.js`:
```javascript
import { headersInterceptor } from './helpers/vue_resource_helper';
beforeEach(() => {
Vue.http.interceptors.push(myInterceptor);
Vue.http.interceptors.push(headersInterceptor);
});
afterEach(() => {
Vue.http.interceptors = _.without(Vue.http.interceptors, myInterceptor);
Vue.http.interceptors = _.without(Vue.http.interceptors, headersInterceptor);
});
```
## CSRF token
We use a Vue Resource interceptor to manage the CSRF token.
`app/assets/javascripts/vue_shared/vue_resource_interceptor.js` holds all our common interceptors.
Note: You don't need to load `app/assets/javascripts/vue_shared/vue_resource_interceptor.js`
since it's already being loaded by `common_vue.js`.
[vue-resource-repo]: https://github.com/pagekit/vue-resource
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
"dependencies": { "dependencies": {
"autosize": "^4.0.0", "autosize": "^4.0.0",
"axios": "^0.16.2", "axios": "^0.16.2",
"axios-mock-adapter": "^1.10.0",
"babel-core": "^6.22.1", "babel-core": "^6.22.1",
"babel-eslint": "^7.2.1", "babel-eslint": "^7.2.1",
"babel-loader": "^7.1.1", "babel-loader": "^7.1.1",
......
import Vue from 'vue'; import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import JobMediator from '~/jobs/job_details_mediator'; import JobMediator from '~/jobs/job_details_mediator';
import job from './mock_data'; import job from './mock_data';
describe('JobMediator', () => { describe('JobMediator', () => {
let mediator; let mediator;
let mock;
beforeEach(() => { beforeEach(() => {
mediator = new JobMediator({ endpoint: 'foo' }); mediator = new JobMediator({ endpoint: 'jobs/40291672.json' });
mock = new MockAdapter(axios);
}); });
it('should set defaults', () => { it('should set defaults', () => {
expect(mediator.store).toBeDefined(); expect(mediator.store).toBeDefined();
expect(mediator.service).toBeDefined(); expect(mediator.service).toBeDefined();
expect(mediator.options).toEqual({ endpoint: 'foo' }); expect(mediator.options).toEqual({ endpoint: 'jobs/40291672.json' });
expect(mediator.state.isLoading).toEqual(false); expect(mediator.state.isLoading).toEqual(false);
}); });
describe('request and store data', () => { describe('request and store data', () => {
const interceptor = (request, next) => {
next(request.respondWith(JSON.stringify(job), {
status: 200,
}));
};
beforeEach(() => { beforeEach(() => {
Vue.http.interceptors.push(interceptor); mock.onGet().reply(200, job, {});
}); });
afterEach(() => { afterEach(() => {
Vue.http.interceptors = _.without(Vue.http.interceptor, interceptor); mock.restore();
}); });
it('should store received data', (done) => { it('should store received data', (done) => {
mediator.fetchJob(); mediator.fetchJob();
setTimeout(() => { setTimeout(() => {
expect(mediator.store.state.job).toEqual(job); expect(mediator.store.state.job).toEqual(job);
done(); done();
......
...@@ -264,6 +264,12 @@ aws4@^1.2.1: ...@@ -264,6 +264,12 @@ aws4@^1.2.1:
version "1.6.0" version "1.6.0"
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e" resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e"
axios-mock-adapter@^1.10.0:
version "1.10.0"
resolved "https://registry.yarnpkg.com/axios-mock-adapter/-/axios-mock-adapter-1.10.0.tgz#3ccee65466439a2c7567e932798fc0377d39209d"
dependencies:
deep-equal "^1.0.1"
axios@^0.16.2: axios@^0.16.2:
version "0.16.2" version "0.16.2"
resolved "https://registry.yarnpkg.com/axios/-/axios-0.16.2.tgz#ba4f92f17167dfbab40983785454b9ac149c3c6d" resolved "https://registry.yarnpkg.com/axios/-/axios-0.16.2.tgz#ba4f92f17167dfbab40983785454b9ac149c3c6d"
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment