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 setAxiosCsrfToken from '../../lib/utils/axios_utils';
import axios from '../../lib/utils/axios_utils';
export default class ClusterService {
constructor(options = {}) {
setAxiosCsrfToken();
this.options = options;
this.appInstallEndpointMap = {
helm: this.options.installHelmEndpoint,
......@@ -18,7 +15,6 @@ export default class ClusterService {
}
installApplication(appId) {
const endpoint = this.appInstallEndpointMap[appId];
return axios.post(endpoint);
return axios.post(this.appInstallEndpointMap[appId]);
}
}
......@@ -29,8 +29,8 @@ export default class JobMediator {
this.poll = new Poll({
resource: this.service,
method: 'getJob',
successCallback: this.successCallback.bind(this),
errorCallback: this.errorCallback.bind(this),
successCallback: response => this.successCallback(response),
errorCallback: () => this.errorCallback(),
});
if (!Visibility.hidden()) {
......@@ -57,7 +57,7 @@ export default class JobMediator {
successCallback(response) {
this.state.isLoading = false;
return response.json().then(data => this.store.storeJob(data));
return this.store.storeJob(response.data);
}
errorCallback() {
......
import Vue from 'vue';
import VueResource from 'vue-resource';
Vue.use(VueResource);
import axios from '../../lib/utils/axios_utils';
export default class JobService {
constructor(endpoint) {
this.job = Vue.resource(endpoint);
this.job = endpoint;
}
getJob() {
return this.job.get();
return axios.get(this.job);
}
}
import axios from 'axios';
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';
/**
* 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
* 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.
---
## [Vue Resource](vue_resource.md)
Vue resource specific practices and gotchas.
## [Axios](axios.md)
Axios specific practices and gotchas.
## [Icons](icons.md)
How we use SVG for our Icons.
......
......@@ -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.
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.
Refer to [vue resource](vue_resource.md) for more details.
We use [axios][axios] to communicate with the server.
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
import Vue from 'vue';
import VueResource from 'vue-resource';
Vue.use(VueResource);
import axios from 'javascripts/lib/utils/axios_utils';
```
### End Result
......@@ -230,15 +227,14 @@ export default class Store {
}
// service.js
import Vue from 'vue';
import VueResource from 'vue-resource';
import 'vue_shared/vue_resource_interceptor';
Vue.use(VueResource);
import axios from 'javascripts/lib/utils/axios_utils'
export default class Service {
constructor(options) {
this.todos = Vue.resource(endpoint.todosEndpoint);
this.todos = axios.create({
baseURL: endpoint.todosEndpoint
});
}
getTodos() {
......@@ -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:
### Stubbing API responses
[Vue Resource Interceptors][vue-resource-interceptor] allow us to add a interceptor with
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,
}));
};
Refer to [mock axios](axios.md#mock-axios-response-on-tests)
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
To manage the state of an application you may use [Vuex][vuex-docs].
......@@ -721,7 +675,6 @@ describe('component', () => {
[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
[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
[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
......@@ -729,3 +682,6 @@ describe('component', () => {
[vuex-structure]: https://vuex.vuejs.org/en/structure.html
[vuex-mutations]: https://vuex.vuejs.org/en/mutations.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 @@
"dependencies": {
"autosize": "^4.0.0",
"axios": "^0.16.2",
"axios-mock-adapter": "^1.10.0",
"babel-core": "^6.22.1",
"babel-eslint": "^7.2.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 job from './mock_data';
describe('JobMediator', () => {
let mediator;
let mock;
beforeEach(() => {
mediator = new JobMediator({ endpoint: 'foo' });
mediator = new JobMediator({ endpoint: 'jobs/40291672.json' });
mock = new MockAdapter(axios);
});
it('should set defaults', () => {
expect(mediator.store).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);
});
describe('request and store data', () => {
const interceptor = (request, next) => {
next(request.respondWith(JSON.stringify(job), {
status: 200,
}));
};
beforeEach(() => {
Vue.http.interceptors.push(interceptor);
mock.onGet().reply(200, job, {});
});
afterEach(() => {
Vue.http.interceptors = _.without(Vue.http.interceptor, interceptor);
mock.restore();
});
it('should store received data', (done) => {
mediator.fetchJob();
setTimeout(() => {
expect(mediator.store.state.job).toEqual(job);
done();
......
......@@ -264,6 +264,12 @@ aws4@^1.2.1:
version "1.6.0"
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:
version "0.16.2"
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