Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/doc
diff options
context:
space:
mode:
Diffstat (limited to 'doc')
-rw-r--r--doc/README.md1
-rw-r--r--doc/api/users.md69
-rw-r--r--doc/development/fe_guide/droplab/droplab.md256
-rw-r--r--doc/development/fe_guide/droplab/plugins/ajax.md37
-rw-r--r--doc/development/fe_guide/droplab/plugins/filter.md45
-rw-r--r--doc/development/fe_guide/droplab/plugins/input_setter.md60
-rw-r--r--doc/development/fe_guide/img/boards_diagram.pngbin0 -> 30538 bytes
-rw-r--r--doc/development/fe_guide/img/vue_arch.pngbin0 -> 9848 bytes
-rw-r--r--doc/development/fe_guide/index.md63
-rw-r--r--doc/development/fe_guide/style_guide_js.md23
-rw-r--r--doc/development/fe_guide/testing.md5
-rw-r--r--doc/development/fe_guide/vue.md343
-rw-r--r--doc/user/admin_area/img/cohorts.pngbin0 -> 439635 bytes
-rw-r--r--doc/user/admin_area/settings/usage_statistics.md102
-rw-r--r--doc/user/admin_area/user_cohorts.md37
-rw-r--r--doc/workflow/groups.md2
16 files changed, 1026 insertions, 17 deletions
diff --git a/doc/README.md b/doc/README.md
index 7703c64e152..9e6a5b4ed44 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -69,6 +69,7 @@ All technical content published by GitLab lives in the documentation, including:
- [Sidekiq Troubleshooting](administration/troubleshooting/sidekiq.md) Debug when Sidekiq appears hung and is not processing jobs.
- [System hooks](system_hooks/system_hooks.md) Notifications when users, projects and keys are changed.
- [Update](update/README.md) Update guides to upgrade your installation.
+- [User cohorts](user/admin_area/user_cohorts.md) View user activity over time.
- [Web terminals](administration/integration/terminal.md) Provide terminal access to environments from within GitLab.
- [Welcome message](customization/welcome_message.md) Add a custom welcome message to the sign-in page.
diff --git a/doc/api/users.md b/doc/api/users.md
index 2ada4d09c84..ff5bffa178f 100644
--- a/doc/api/users.md
+++ b/doc/api/users.md
@@ -72,6 +72,7 @@ GET /users
"organization": "",
"last_sign_in_at": "2012-06-01T11:41:01Z",
"confirmed_at": "2012-05-23T09:05:22Z",
+ "last_activity_on": "2012-05-23",
"color_scheme_id": 2,
"projects_limit": 100,
"current_sign_in_at": "2012-06-02T06:36:55Z",
@@ -104,6 +105,7 @@ GET /users
"organization": "",
"last_sign_in_at": null,
"confirmed_at": "2012-05-30T16:53:06.148Z",
+ "last_activity_on": "2012-05-23",
"color_scheme_id": 3,
"projects_limit": 100,
"current_sign_in_at": "2014-03-19T17:54:13Z",
@@ -130,6 +132,18 @@ For example:
GET /users?username=jack_smith
```
+You can also lookup users by external UID and provider:
+
+```
+GET /users?extern_uid=:extern_uid&provider=:provider
+```
+
+For example:
+
+```
+GET /users?extern_uid=1234567&provider=github
+```
+
You can search for users who are external with: `/users?external=true`
## Single user
@@ -196,6 +210,7 @@ Parameters:
"organization": "",
"last_sign_in_at": "2012-06-01T11:41:01Z",
"confirmed_at": "2012-05-23T09:05:22Z",
+ "last_activity_on": "2012-05-23",
"color_scheme_id": 2,
"projects_limit": 100,
"current_sign_in_at": "2012-06-02T06:36:55Z",
@@ -320,6 +335,7 @@ GET /user
"organization": "",
"last_sign_in_at": "2012-06-01T11:41:01Z",
"confirmed_at": "2012-05-23T09:05:22Z",
+ "last_activity_on": "2012-05-23",
"color_scheme_id": 2,
"projects_limit": 100,
"current_sign_in_at": "2012-06-02T06:36:55Z",
@@ -365,6 +381,7 @@ GET /user
"organization": "",
"last_sign_in_at": "2012-06-01T11:41:01Z",
"confirmed_at": "2012-05-23T09:05:22Z",
+ "last_activity_on": "2012-05-23",
"color_scheme_id": 2,
"projects_limit": 100,
"current_sign_in_at": "2012-06-02T06:36:55Z",
@@ -986,3 +1003,55 @@ Parameters:
| --------- | ---- | -------- | ----------- |
| `user_id` | integer | yes | The ID of the user |
| `impersonation_token_id` | integer | yes | The ID of the impersonation token |
+
+### Get user activities (admin only)
+
+>**Note:** This API endpoint is only available on 8.15 (EE) and 9.1 (CE) and above.
+
+Get the last activity date for all users, sorted from oldest to newest.
+
+The activities that update the timestamp are:
+
+ - Git HTTP/SSH activities (such as clone, push)
+ - User logging in into GitLab
+
+By default, it shows the activity for all users in the last 6 months, but this can be
+amended by using the `from` parameter.
+
+```
+GET /user/activities
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `from` | string | no | Date string in the format YEAR-MONTH-DAY, e.g. `2016-03-11`. Defaults to 6 months ago. |
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/user/activities
+```
+
+Example response:
+
+```json
+[
+ {
+ "username": "user1",
+ "last_activity_on": "2015-12-14",
+ "last_activity_at": "2015-12-14"
+ },
+ {
+ "username": "user2",
+ "last_activity_on": "2015-12-15",
+ "last_activity_at": "2015-12-15"
+ },
+ {
+ "username": "user3",
+ "last_activity_on": "2015-12-16",
+ "last_activity_at": "2015-12-16"
+ }
+]
+```
+
+Please note that `last_activity_at` is deprecated, please use `last_activity_on`.
diff --git a/doc/development/fe_guide/droplab/droplab.md b/doc/development/fe_guide/droplab/droplab.md
new file mode 100644
index 00000000000..8f0b6b21953
--- /dev/null
+++ b/doc/development/fe_guide/droplab/droplab.md
@@ -0,0 +1,256 @@
+# DropLab
+
+A generic dropdown for all of your custom dropdown needs.
+
+## Usage
+
+DropLab can be used by simply adding a `data-dropdown-trigger` HTML attribute.
+This attribute allows us to find the "trigger" _(toggle)_ for the dropdown,
+whether that is a button, link or input.
+
+The value of the `data-dropdown-trigger` should be a CSS selector that
+DropLab can use to find the trigger's dropdown list.
+
+You should also add the `data-dropdown` attribute to declare the dropdown list.
+The value is irrelevant.
+
+The DropLab class has no side effects, so you must always call `.init` when
+the DOM is ready. `DropLab.prototype.init` takes the same arguments as `DropLab.prototype.addHook`.
+If you do not provide any arguments, it will globally query and instantiate all droplab compatible dropdowns.
+
+```html
+<a href="#" data-dropdown-trigger="#list">Toggle</a>
+
+<ul id="list" data-dropdown>
+ <!-- ... -->
+<ul>
+```
+```js
+const droplab = new DropLab();
+droplab.init();
+```
+
+As you can see, we have a "Toggle" link, that is declared as a trigger.
+It provides a selector to find the dropdown list it should control.
+
+### Static data
+
+You can add static list items.
+
+```html
+<a href="#" data-dropdown-trigger="#list">Toggle</a>
+
+<ul id="list" data-dropdown>
+ <li>Static value 1</li>
+ <li>Static value 2</li>
+<ul>
+```
+```js
+const droplab = new DropLab();
+droplab.init();
+```
+
+### Explicit instantiation
+
+You can pass the trigger and list elements as constructor arguments to return
+a non-global instance of DropLab using the `DropLab.prototype.init` method.
+
+```html
+<a href="#" id="trigger" data-dropdown-trigger="#list">Toggle</a>
+
+<ul id="list" data-dropdown>
+ <!-- ... -->
+<ul>
+```
+```js
+const trigger = document.getElementById('trigger');
+const list = document.getElementById('list');
+
+const droplab = new DropLab();
+droplab.init(trigger, list);
+```
+
+You can also add hooks to an existing DropLab instance using `DropLab.prototype.addHook`.
+
+```html
+<a href="#" data-dropdown-trigger="#auto-dropdown">Toggle</a>
+<ul id="auto-dropdown" data-dropdown><!-- ... --><ul>
+
+<a href="#" id="trigger" data-dropdown-trigger="#list">Toggle</a>
+<ul id="list" data-dropdown><!-- ... --><ul>
+```
+```js
+const droplab = new DropLab();
+
+droplab.init();
+
+const trigger = document.getElementById('trigger');
+const list = document.getElementById('list');
+
+droplab.addHook(trigger, list);
+```
+
+
+### Dynamic data
+
+Adding `data-dynamic` to your dropdown element will enable dynamic list rendering.
+
+You can template a list item using the keys of the data object provided.
+Use the handlebars syntax `{{ value }}` to HTML escape the value.
+Use the `<%= value %>` syntax to simply interpolate the value.
+Use the `<%= value %>` syntax to evaluate the value.
+
+Passing an array of objects to `DropLab.prototype.addData` will render that data
+for all `data-dynamic` dropdown lists tracked by that DropLab instance.
+
+```html
+<a href="#" data-dropdown-trigger="#list">Toggle</a>
+
+<ul id="list" data-dropdown data-dynamic>
+ <li><a href="#" data-id="{{id}}">{{text}}</a></li>
+</ul>
+```
+```js
+const droplab = new DropLab();
+
+droplab.init().addData([{
+ id: 0,
+ text: 'Jacob',
+}, {
+ id: 1,
+ text: 'Jeff',
+}]);
+```
+
+Alternatively, you can specify a specific dropdown to add this data to but passing
+the data as the second argument and and the `id` of the trigger element as the first argument.
+
+```html
+<a href="#" data-dropdown-trigger="#list" id="trigger">Toggle</a>
+
+<ul id="list" data-dropdown data-dynamic>
+ <li><a href="#" data-id="{{id}}">{{text}}</a></li>
+</ul>
+```
+```js
+const droplab = new DropLab();
+
+droplab.init().addData('trigger', [{
+ id: 0,
+ text: 'Jacob',
+}, {
+ id: 1,
+ text: 'Jeff',
+}]);
+```
+
+This allows you to mix static and dynamic content with ease, even with one trigger.
+
+Note the use of scoping regarding the `data-dropdown` attribute to capture both
+dropdown lists, one of which is dynamic.
+
+```html
+<input id="trigger" data-dropdown-trigger="#list">
+<div id="list" data-dropdown>
+ <ul>
+ <li><a href="#">Static item 1</a></li>
+ <li><a href="#">Static item 2</a></li>
+ </ul>
+ <ul data-dynamic>
+ <li><a href="#" data-id="{{id}}">{{text}}</a></li>
+ </ul>
+</div>
+```
+```js
+const droplab = new DropLab();
+
+droplab.init().addData('trigger', [{
+ id: 0,
+ text: 'Jacob',
+}, {
+ id: 1,
+ text: 'Jeff',
+}]);
+```
+
+## Internal selectors
+
+DropLab adds some CSS classes to help lower the barrier to integration.
+
+For example,
+
+* The `droplab-item-selected` css class is added to items that have been selected
+either by a mouse click or by enter key selection.
+* The `droplab-item-active` css class is added to items that have been selected
+using arrow key navigation.
+
+## Internal events
+
+DropLab uses some custom events to help lower the barrier to integration.
+
+For example,
+
+* The `click.dl` event is fired when an `li` list item has been clicked. It is also
+fired when a list item has been selected with the keyboard. It is also fired when a
+`HookButton` button is clicked (a registered `button` tag or `a` tag trigger).
+* The `input.dl` event is fired when a `HookInput` (a registered `input` tag trigger) triggers an `input` event.
+* The `mousedown.dl` event is fired when a `HookInput` triggers a `mousedown` event.
+* The `keyup.dl` event is fired when a `HookInput` triggers a `keyup` event.
+* The `keydown.dl` event is fired when a `HookInput` triggers a `keydown` event.
+
+These custom events add a `detail` object to the vanilla `Event` object that provides some potentially useful data.
+
+## Plugins
+
+Plugins are objects that are registered to be executed when a hook is added (when a droplab trigger and dropdown are instantiated).
+
+If no modules API is detected, the library will fall back as it does with `window.DropLab` and will add `window.DropLab.plugins.PluginName`.
+
+### Usage
+
+To use plugins, you can pass them in an array as the third argument of `DropLab.prototype.init` or `DropLab.prototype.addHook`.
+Some plugins require configuration values, the config object can be passed as the fourth argument.
+
+```html
+<a href="#" id="trigger" data-dropdown-trigger="#list">Toggle</a>
+<ul id="list" data-dropdown><!-- ... --><ul>
+```
+```js
+const droplab = new DropLab();
+
+const trigger = document.getElementById('trigger');
+const list = document.getElementById('list');
+
+droplab.init(trigger, list, [droplabAjax], {
+ droplabAjax: {
+ endpoint: '/some-endpoint',
+ method: 'setData',
+ },
+});
+```
+
+### Documentation
+
+* [Ajax plugin](plugins/ajax.md)
+* [Filter plugin](plugins/filter.md)
+* [InputSetter plugin](plugins/input_setter.md)
+
+### Development
+
+When plugins are initialised for a droplab trigger+dropdown, DropLab will
+call the plugins `init` function, so this must be implemented in the plugin.
+
+```js
+class MyPlugin {
+ static init() {
+ this.someProp = 'someProp';
+ this.someMethod();
+ }
+
+ static someMethod() {
+ this.otherProp = 'otherProp';
+ }
+}
+
+export default MyPlugin;
+```
diff --git a/doc/development/fe_guide/droplab/plugins/ajax.md b/doc/development/fe_guide/droplab/plugins/ajax.md
new file mode 100644
index 00000000000..9c7e56fa448
--- /dev/null
+++ b/doc/development/fe_guide/droplab/plugins/ajax.md
@@ -0,0 +1,37 @@
+# Ajax
+
+`Ajax` is a droplab plugin that allows for retrieving and rendering list data from a server.
+
+## Usage
+
+Add the `Ajax` object to the plugins array of a `DropLab.prototype.init` or `DropLab.prototype.addHook` call.
+
+`Ajax` requires 2 config values, the `endpoint` and `method`.
+
+* `endpoint` should be a URL to the request endpoint.
+* `method` should be `setData` or `addData`.
+* `setData` completely replaces the dropdown with the response data.
+* `addData` appends the response data to the current dropdown list.
+
+```html
+<a href="#" id="trigger" data-dropdown-trigger="#list">Toggle</a>
+<ul id="list" data-dropdown><!-- ... --><ul>
+```
+```js
+ const droplab = new DropLab();
+
+ const trigger = document.getElementById('trigger');
+ const list = document.getElementById('list');
+
+ droplab.addHook(trigger, list, [Ajax], {
+ Ajax: {
+ endpoint: '/some-endpoint',
+ method: 'setData',
+ },
+ });
+```
+
+Optionally you can set `loadingTemplate` to a HTML string. This HTML string will
+replace the dropdown list whilst the request is pending.
+
+Additionally, you can set `onError` to a function to catch any XHR errors.
diff --git a/doc/development/fe_guide/droplab/plugins/filter.md b/doc/development/fe_guide/droplab/plugins/filter.md
new file mode 100644
index 00000000000..0853ea4d320
--- /dev/null
+++ b/doc/development/fe_guide/droplab/plugins/filter.md
@@ -0,0 +1,45 @@
+# Filter
+
+`Filter` is a plugin that allows for filtering data that has been added
+to the dropdown using a simple fuzzy string search of an input value.
+
+## Usage
+
+Add the `Filter` object to the plugins array of a `DropLab.prototype.init` or `DropLab.prototype.addHook` call.
+
+* `Filter` requires a config value for `template`.
+* `template` should be the key of the objects within your data array that you want to compare
+to the user input string, for filtering.
+
+```html
+<input href="#" id="trigger" data-dropdown-trigger="#list">
+<ul id="list" data-dropdown data-dynamic>
+ <li><a href="#" data-id="{{id}}">{{text}}</a></li>
+<ul>
+```
+```js
+ const droplab = new DropLab();
+
+ const trigger = document.getElementById('trigger');
+ const list = document.getElementById('list');
+
+ droplab.init(trigger, list, [Filter], {
+ Filter: {
+ template: 'text',
+ },
+ });
+
+ droplab.addData('trigger', [{
+ id: 0,
+ text: 'Jacob',
+ }, {
+ id: 1,
+ text: 'Jeff',
+ }]);
+```
+
+Above, the input string will be compared against the `test` key of the passed data objects.
+
+Optionally you can set `filterFunction` to a function. This function will be used instead
+of `Filter`'s built in string search. `filterFunction` is passed 2 arguments, the first
+is one of the data objects, the second is the current input value.
diff --git a/doc/development/fe_guide/droplab/plugins/input_setter.md b/doc/development/fe_guide/droplab/plugins/input_setter.md
new file mode 100644
index 00000000000..a549603c20d
--- /dev/null
+++ b/doc/development/fe_guide/droplab/plugins/input_setter.md
@@ -0,0 +1,60 @@
+# InputSetter
+
+`InputSetter` is a plugin that allows for udating DOM out of the scope of droplab when a list item is clicked.
+
+## Usage
+
+Add the `InputSetter` object to the plugins array of a `DropLab.prototype.init` or `DropLab.prototype.addHook` call.
+
+* `InputSetter` requires a config value for `input` and `valueAttribute`.
+* `input` should be the DOM element that you want to manipulate.
+* `valueAttribute` should be a string that is the name of an attribute on your list items that is used to get the value
+to update the `input` element with.
+
+You can also set the `InputSetter` config to an array of objects, which will allow you to update multiple elements.
+
+
+```html
+<input id="input" value="">
+<div id="div" data-selected-id=""></div>
+
+<input href="#" id="trigger" data-dropdown-trigger="#list">
+<ul id="list" data-dropdown data-dynamic>
+ <li><a href="#" data-id="{{id}}">{{text}}</a></li>
+<ul>
+```
+```js
+ const droplab = new DropLab();
+
+ const trigger = document.getElementById('trigger');
+ const list = document.getElementById('list');
+
+ const input = document.getElementById('input');
+ const div = document.getElementById('div');
+
+ droplab.init(trigger, list, [InputSetter], {
+ InputSetter: [{
+ input: input,
+ valueAttribute: 'data-id',
+ } {
+ input: div,
+ valueAttribute: 'data-id',
+ inputAttribute: 'data-selected-id',
+ }],
+ });
+
+ droplab.addData('trigger', [{
+ id: 0,
+ text: 'Jacob',
+ }, {
+ id: 1,
+ text: 'Jeff',
+ }]);
+```
+
+Above, if the second list item was clicked, it would update the `#input` element
+to have a `value` of `1`, it would also update the `#div` element's `data-selected-id` to `1`.
+
+Optionally you can set `inputAttribute` to a string that is the name of an attribute on your `input` element that you want to update.
+If you do not provide an `inputAttribute`, `InputSetter` will update the `value` of the `input` element if it is an `INPUT` element,
+or the `textContent` of the `input` element if it is not an `INPUT` element.
diff --git a/doc/development/fe_guide/img/boards_diagram.png b/doc/development/fe_guide/img/boards_diagram.png
new file mode 100644
index 00000000000..7a2cf972fd0
--- /dev/null
+++ b/doc/development/fe_guide/img/boards_diagram.png
Binary files differ
diff --git a/doc/development/fe_guide/img/vue_arch.png b/doc/development/fe_guide/img/vue_arch.png
new file mode 100644
index 00000000000..a67706c7c1e
--- /dev/null
+++ b/doc/development/fe_guide/img/vue_arch.png
Binary files differ
diff --git a/doc/development/fe_guide/index.md b/doc/development/fe_guide/index.md
index f963bffde37..e2a198f637f 100644
--- a/doc/development/fe_guide/index.md
+++ b/doc/development/fe_guide/index.md
@@ -27,6 +27,59 @@ For our currently-supported browsers, see our [requirements][requirements].
---
+## Development Process
+
+When you are assigned an issue please follow the next steps:
+
+### Divide a big feature into small Merge Requests
+1. Big Merge Request are painful to review. In order to make this process easier we
+must break a big feature into smaller ones and create a Merge Request for each step.
+1. First step is to create a branch from `master`, let's call it `new-feature`. This branch
+will be the recipient of all the smaller Merge Requests. Only this one will be merged to master.
+1. Don't do any work on this one, let's keep it synced with master.
+1. Create a new branch from `new-feature`, let's call it `new-feature-step-1`. We advise you
+to clearly identify which step the branch represents.
+1. Do the first part of the modifications in this branch. The target branch of this Merge Request
+should be `new-feature`.
+1. Once `new-feature-step-1` gets merged into `new-feature` we can continue our work. Create a new
+branch from `new-feature`, let's call it `new-feature-step-2` and repeat the process done before.
+
+```shell
+* master
+|\
+| * new-feature
+| |\
+| | * new-feature-step-1
+| |\
+| | * new-feature-step-2
+| |\
+| | * new-feature-step-3
+```
+
+**Tips**
+- Make sure `new-feature` branch is always synced with `master`: merge master frequently.
+- Do the same for the feature branch you have opened. This can be accomplished by merging `master` into `new-feature` and `new-feature` into `new-feature-step-*`
+- Avoid rewriting history.
+
+### Share your work early
+1. Before writing code guarantee your vision of the architecture is aligned with
+GitLab's architecture.
+1. Add a diagram to the issue and ask a Frontend Architecture about it.
+
+ ![Diagram of Issue Boards Architecture](img/boards_diagram.png)
+
+1. Don't take more than one week between starting work on a feature and
+sharing a Merge Request with a reviewer or a maintainer.
+
+### Vue features
+1. Follow the steps in [Vue.js Best Practices](vue.md)
+1. Follow the style guide.
+1. Only a handful of people are allowed to merge Vue related features.
+Reach out to @jschatz, @iamphill, @fatihacet or @filipa early in this process.
+
+
+---
+
## [Architecture](architecture.md)
How we go about making fundamental design decisions in GitLab's frontend team
or make changes to our frontend development guidelines.
@@ -90,3 +143,13 @@ Our accessibility standards and resources.
[scss-lint]: https://github.com/brigade/scss-lint
[install]: ../../install/installation.md#4-node
[requirements]: ../../install/requirements.md#supported-web-browsers
+
+---
+
+## [DropLab](droplab/droplab.md)
+Our internal `DropLab` dropdown library.
+
+* [DropLab](droplab/droplab.md)
+* [Ajax plugin](droplab/plugins/ajax.md)
+* [Filter plugin](droplab/plugins/filter.md)
+* [InputSetter plugin](droplab/plugins/input_setter.md)
diff --git a/doc/development/fe_guide/style_guide_js.md b/doc/development/fe_guide/style_guide_js.md
index abd241c0bc8..ed656476a96 100644
--- a/doc/development/fe_guide/style_guide_js.md
+++ b/doc/development/fe_guide/style_guide_js.md
@@ -58,7 +58,7 @@ See [our current .eslintrc][eslintrc] for specific rules and patterns.
import Bar from './bar';
```
-- **Never** disable the `no-undef` rule. Declare globals with `/* global Foo */` instead.
+- **Never** disable the `no-undef` rule. Declare globals with `/* global Foo */` instead.
- When declaring multiple globals, always use one `/* global [name] */` line per variable.
@@ -183,6 +183,19 @@ See [our current .eslintrc][eslintrc] for specific rules and patterns.
parseInt('10', 10);
```
+#### CSS classes used for JavaScript
+- If the class is being used in Javascript it needs to be prepend with `js-`
+ ```html
+ // bad
+ <button class="add-user">
+ Add User
+ </button>
+
+ // good
+ <button class="js-add-user">
+ Add User
+ </button>
+ ```
### Vue.js
@@ -200,6 +213,7 @@ See [our current .eslintrc][eslintrc] for specific rules and patterns.
#### Naming
- **Extensions**: Use `.vue` extension for Vue components.
- **Reference Naming**: Use PascalCase for Vue components and camelCase for their instances:
+
```javascript
// bad
import cardBoard from 'cardBoard';
@@ -217,6 +231,7 @@ See [our current .eslintrc][eslintrc] for specific rules and patterns.
cardBoard: CardBoard
};
```
+
- **Props Naming:**
- Avoid using DOM component prop names.
- Use kebab-case instead of camelCase to provide props in templates.
@@ -243,12 +258,18 @@ See [our current .eslintrc][eslintrc] for specific rules and patterns.
<component v-if="bar"
param="baz" />
+ <button class="btn">Click me</button>
+
// good
<component
v-if="bar"
param="baz"
/>
+ <button class="btn">
+ Click me
+ </button>
+
// if props fit in one line then keep it on the same line
<component bar="bar" />
```
diff --git a/doc/development/fe_guide/testing.md b/doc/development/fe_guide/testing.md
index a4631fd0073..66afbf4db4d 100644
--- a/doc/development/fe_guide/testing.md
+++ b/doc/development/fe_guide/testing.md
@@ -26,6 +26,10 @@ browser and you will not have access to certain APIs, such as
[`Notification`](https://developer.mozilla.org/en-US/docs/Web/API/notification),
which will have to be stubbed.
+### Writing tests
+### Vue.js unit tests
+See this [section][vue-test].
+
### Running frontend tests
`rake karma` runs the frontend-only (JavaScript) tests.
@@ -134,3 +138,4 @@ Scenario: Developer can approve merge request
[jasmine-focus]: https://jasmine.github.io/2.5/focused_specs.html
[jasmine-jquery]: https://github.com/velesin/jasmine-jquery
[karma]: http://karma-runner.github.io/
+[vue-test]:https://docs.gitlab.com/ce/development/fe_guide/vue.html#testing-vue-components
diff --git a/doc/development/fe_guide/vue.md b/doc/development/fe_guide/vue.md
index 3e3406e7d6a..45c8300d9de 100644
--- a/doc/development/fe_guide/vue.md
+++ b/doc/development/fe_guide/vue.md
@@ -19,13 +19,31 @@ We don't want to refactor all GitLab frontend code into Vue.js, here are some gu
when not to use Vue.js:
- Adding or changing static information;
-- Features that highly depend on jQuery will be hard to work with Vue.js
+- Features that highly depend on jQuery will be hard to work with Vue.js;
+- Features without reactive data;
As always, the Frontend Architectural Experts are available to help with any Vue or JavaScript questions.
-## How to build a new feature with Vue.js
+## Vue architecture
-**Components, Stores and Services**
+All new features built with Vue.js must follow a [Flux architecture][flux].
+The main goal we are trying to achieve is to have only one data flow and only one data entry.
+In order to achieve this goal, each Vue bundle needs a Store - where we keep all the data -,
+a Service - that we use to communicate with the server - and a main Vue component.
+
+Think of the Main Vue Component as the entry point of your application. This is the only smart
+component that should exist in each Vue feature.
+This component is responsible for:
+1. Calling the Service to get data from the server
+1. Calling the Store to store the data received
+1. Mounting all the other components
+
+ ![Vue Architecture](img/vue_arch.png)
+
+You can also read about this architecture in vue docs about [state management][state-management]
+and about [one way data flow][one-way-data-flow].
+
+### Components, Stores and Services
In some features implemented with Vue.js, like the [issue board][issue-boards]
or [environments table][environments-table]
@@ -46,16 +64,17 @@ _For consistency purposes, we recommend you to follow the same structure._
Let's look into each of them:
-**A `*_bundle.js` file**
+### A `*_bundle.js` file
This is the index file of your new feature. This is where the root Vue instance
of the new feature should be.
-The Store and the Service should be imported and initialized in this file and provided as a prop to the main component.
+The Store and the Service should be imported and initialized in this file and
+provided as a prop to the main component.
Don't forget to follow [these steps.][page_specific_javascript]
-**A folder for Components**
+### A folder for Components
This folder holds all components that are specific of this new feature.
If you need to use or create a component that will probably be used somewhere
@@ -70,29 +89,320 @@ in one table would not be a good use of this pattern.
You can read more about components in Vue.js site, [Component System][component-system]
-**A folder for the Store**
+### A folder for the Store
The Store is a class that allows us to manage the state in a single
-source of truth.
+source of truth. It is not aware of the service or the components.
The concept we are trying to follow is better explained by Vue documentation
itself, please read this guide: [State Management][state-management]
-**A folder for the Service**
+### A folder for the Service
+
+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.
+
+### End Result
+
+The following example shows an application:
+
+```javascript
+// store.js
+export default class Store {
+
+ /**
+ * This is where we will iniatialize the state of our data.
+ * Usually in a small SPA you don't need any options when starting the store. In the case you do
+ * need guarantee it's an Object and it's documented.
+ *
+ * @param {Object} options
+ */
+ constructor(options) {
+ this.options = options;
+
+ // Create a state object to handle all our data in the same place
+ this.todos = []:
+ }
+
+ setTodos(todos = []) {
+ this.todos = todos;
+ }
+
+ addTodo(todo) {
+ this.todos.push(todo);
+ }
+
+ removeTodo(todoID) {
+ const state = this.todos;
+
+ const newState = state.filter((element) => {element.id !== todoID});
+
+ this.todos = newState;
+ }
+}
+
+// service.js
+import Vue from 'vue';
+import VueResource from 'vue-resource';
+import 'vue_shared/vue_resource_interceptor';
+
+Vue.use(VueResource);
+
+export default class Service {
+ constructor(options) {
+ this.todos = Vue.resource(endpoint.todosEndpoint);
+ }
+
+ getTodos() {
+ return this.todos.get();
+ }
+
+ addTodo(todo) {
+ return this.todos.put(todo);
+ }
+}
+// todo_component.vue
+<script>
+export default {
+ props: {
+ data: {
+ type: Object,
+ required: true,
+ },
+ }
+}
+</script>
+<template>
+ <div>
+ <h1>
+ Title: {{data.title}}
+ </h1>
+ <p>
+ {{data.text}}
+ </p>
+ </div>
+</template>
+
+// todos_main_component.vue
+<script>
+import Store from 'store';
+import Service from 'service';
+import TodoComponent from 'todoComponent';
+export default {
+ /**
+ * Although most data belongs in the store, each component it's own state.
+ * We want to show a loading spinner while we are fetching the todos, this state belong
+ * in the component.
+ *
+ * We need to access the store methods through all methods of our component.
+ * We need to access the state of our store.
+ */
+ data() {
+ const store = new Store();
+
+ return {
+ isLoading: false,
+ store: store,
+ todos: store.todos,
+ };
+ },
+
+ components: {
+ todo: TodoComponent,
+ },
+
+ created() {
+ this.service = new Service('todos');
+
+ this.getTodos();
+ },
+
+ methods: {
+ getTodos() {
+ this.isLoading = true;
+
+ this.service.getTodos()
+ .then(response => response.json())
+ .then((response) => {
+ this.store.setTodos(response);
+ this.isLoading = false;
+ })
+ .catch(() => {
+ this.isLoading = false;
+ // Show an error
+ });
+ },
+
+ addTodo(todo) {
+ this.service.addTodo(todo)
+ then(response => response.json())
+ .then((response) => {
+ this.store.addTodo(response);
+ })
+ .catch(() => {
+ // Show an error
+ });
+ }
+ }
+}
+</script>
+<template>
+ <div class="container">
+ <div v-if="isLoading">
+ <i
+ class="fa fa-spin fa-spinner"
+ aria-hidden="true" />
+ </div>
+
+ <div
+ v-if="!isLoading"
+ class="js-todo-list">
+ <template v-for='todo in todos'>
+ <todo :data="todo" />
+ </template>
+
+ <button
+ @click="addTodo"
+ class="js-add-todo">
+ Add Todo
+ </button>
+ </div>
+ <div>
+</template>
+
+// bundle.js
+import todoComponent from 'todos_main_component.vue';
+
+new Vue({
+ el: '.js-todo-app',
+ components: {
+ todoComponent,
+ },
+ render: createElement => createElement('todo-component' {
+ props: {
+ someProp: [],
+ }
+ }),
+});
-The Service is used only to communicate with the server.
-It does not store or manipulate any data.
-We use [vue-resource][vue-resource-repo] to
-communicate with the server.
+```
-The [issue boards service][issue-boards-service]
-is a good example of this pattern.
+The [issue boards service][issue-boards-service] is a good example of this pattern.
## Style guide
Please refer to the Vue section of our [style guide](style_guide_js.md#vuejs)
for best practices while writing your Vue components and templates.
+## Testing Vue Components
+
+Each Vue component has a unique output. This output is always present in the render function.
+
+Although we can test each method of a Vue component individually, our goal must be to test the output
+of the render/template function, which represents the state at all times.
+
+Make use of Vue Resource Interceptors to mock data returned by the service.
+
+Here's how we would test the Todo App above:
+
+```javascript
+import component from 'todos_main_component';
+
+describe('Todos App', () => {
+ it('should render the loading state while the request is being made', () => {
+ const Component = Vue.extend(component);
+
+ const vm = new Component().$mount();
+
+ expect(vm.$el.querySelector('i.fa-spin')).toBeDefined();
+ });
+
+ describe('with data', () => {
+ // 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,
+ }));
+ };
+
+ let vm;
+
+ beforeEach(() => {
+ Vue.http.interceptors.push(interceptor);
+
+ const Component = Vue.extend(component);
+
+ vm = new Component().$mount();
+ });
+
+ afterEach(() => {
+ Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor);
+ });
+
+
+ it('should render todos', (done) => {
+ setTimeout(() => {
+ expect(vm.$el.querySelectorAll('.js-todo-list div').length).toBe(1);
+ done();
+ }, 0);
+ });
+ });
+
+ describe('add todo', () => {
+ let vm;
+ beforeEach(() => {
+ const Component = Vue.extend(component);
+ vm = new Component().$mount();
+ });
+ it('should add a todos', (done) => {
+ setTimeout(() => {
+ vm.$el.querySelector('.js-add-todo').click();
+
+ // Add a new interceptor to mock the add Todo request
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelectorAll('.js-todo-list div').length).toBe(2);
+ });
+ }, 0);
+ });
+ });
+});
+```
+
+### 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,
+ }));
+ };
+
+ 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);
+ });
+```
+
[vue-docs]: http://vuejs.org/guide/index.html
[issue-boards]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/assets/javascripts/boards
@@ -100,5 +410,8 @@ for best practices while writing your Vue components and templates.
[page_specific_javascript]: https://docs.gitlab.com/ce/development/frontend.html#page-specific-javascript
[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-repo]: https://github.com/pagekit/vue-resource
+[vue-resource-interceptor]: https://github.com/pagekit/vue-resource/blob/develop/docs/http.md#interceptors
[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
diff --git a/doc/user/admin_area/img/cohorts.png b/doc/user/admin_area/img/cohorts.png
new file mode 100644
index 00000000000..8bae7faff07
--- /dev/null
+++ b/doc/user/admin_area/img/cohorts.png
Binary files differ
diff --git a/doc/user/admin_area/settings/usage_statistics.md b/doc/user/admin_area/settings/usage_statistics.md
new file mode 100644
index 00000000000..c3f3179d99e
--- /dev/null
+++ b/doc/user/admin_area/settings/usage_statistics.md
@@ -0,0 +1,102 @@
+# Usage statistics
+
+GitLab Inc. will periodically collect information about your instance in order
+to perform various actions.
+
+All statistics are opt-in and you can always disable them from the admin panel.
+
+## Version check
+
+GitLab can inform you when an update is available and the importance of it.
+
+No information other than the GitLab version and the instance's domain name
+are collected.
+
+In the **Overview** tab you can see if your GitLab version is up to date. There
+are three cases: 1) you are up to date (green), 2) there is an update available
+(yellow) and 3) your version is vulnerable and a security fix is released (red).
+
+In any case, you will see a message informing you of the state and the
+importance of the update.
+
+If enabled, the version status will also be shown in the help page (`/help`)
+for all signed in users.
+
+## Usage ping
+
+> [Introduced][ee-557] in GitLab Enterprise Edition 8.10. More statistics
+[were added][ee-735] in GitLab Enterprise Edition
+8.12. [Moved to GitLab Community Edition][ce-23361] in 9.1.
+
+GitLab Inc. can collect non-sensitive information about how GitLab users
+use their GitLab instance upon the activation of a ping feature
+located in the admin panel (`/admin/application_settings`).
+
+You can see the **exact** JSON payload that your instance sends to GitLab
+in the "Usage statistics" section of the admin panel.
+
+Nothing qualitative is collected. Only quantitative. That means no project
+names, author names, comment bodies, names of labels, etc.
+
+The usage ping is sent in order for GitLab Inc. to have a better understanding
+of how our users use our product, and to be more data-driven when creating or
+changing features.
+
+The total number of the following is sent back to GitLab Inc.:
+
+- Comments
+- Groups
+- Users
+- Projects
+- Issues
+- Labels
+- CI builds
+- Snippets
+- Milestones
+- Todos
+- Pushes
+- Merge requests
+- Environments
+- Triggers
+- Deploy keys
+- Pages
+- Project Services
+- Projects using the Prometheus service
+- Issue Boards
+- CI Runners
+- Deployments
+- Geo Nodes
+- LDAP Groups
+- LDAP Keys
+- LDAP Users
+- LFS objects
+- Protected branches
+- Releases
+- Remote mirrors
+- Uploads
+- Web hooks
+
+Also, we track if you've installed Mattermost with GitLab.
+For example: `"mattermost_enabled":true"`.
+
+More data will be added over time. The goal of this ping is to be as light as
+possible, so it won't have any performance impact on your installation when
+the calculation is made.
+
+### Deactivate the usage ping
+
+By default, usage ping is opt-out. If you want to deactivate this feature, go to
+the Settings page of your administration panel and uncheck the Usage ping
+checkbox.
+
+## Privacy policy
+
+GitLab Inc. does **not** collect any sensitive information, like project names
+or the content of the comments. GitLab Inc. does not disclose or otherwise make
+available any of the data collected on a customer specific basis.
+
+Read more about this in the [Privacy policy](https://about.gitlab.com/privacy).
+
+[ee-557]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/557
+[ee-735]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/735
+[ce-23361]: https://gitlab.com/gitlab-org/gitlab-ce/issues/23361
diff --git a/doc/user/admin_area/user_cohorts.md b/doc/user/admin_area/user_cohorts.md
new file mode 100644
index 00000000000..e25e7a8bbc3
--- /dev/null
+++ b/doc/user/admin_area/user_cohorts.md
@@ -0,0 +1,37 @@
+# Cohorts
+
+> **Notes:**
+> [Introduced][ce-23361] in GitLab 9.1.
+
+As a benefit of having the [usage ping active](settings/usage_statistics.md),
+GitLab lets you analyze the users' activities of your GitLab installation.
+Under `/admin/cohorts`, when the usage ping is active, GitLab will show the
+monthly cohorts of new users and their activities over time.
+
+## Overview
+
+How do we read the user cohorts table? Let's take an example with the following
+user cohorts.
+
+![User cohort example](img/cohorts.png)
+
+For the cohort of June 2016, 163 users have been added on this server and have
+been active since this month. One month later, in July 2016, out of
+these 163 users, 155 users (or 95% of the June cohort) are still active. Two
+months later, 139 users (or 85%) are still active. 9 months later, we can see
+that only 6% of this cohort are still active.
+
+The Inactive users column shows the number of users who have been added during
+the month, but who have never actually had any activity in the instance.
+
+How do we measure the activity of users? GitLab considers a user active if:
+
+* the user signs in
+* the user has Git activity (whether push or pull).
+
+## Setup
+
+1. [Activate the usage ping](settings/usage_statistics.md)
+2. Go to `/admin/cohorts` to see the user cohorts of the server
+
+[ce-23361]: https://gitlab.com/gitlab-org/gitlab-ce/issues/23361
diff --git a/doc/workflow/groups.md b/doc/workflow/groups.md
index 882747e14e9..1cb3c940f00 100644
--- a/doc/workflow/groups.md
+++ b/doc/workflow/groups.md
@@ -1,6 +1,6 @@
# GitLab Groups
-GitLab groups allow you to group projects into directories and give users to several projects at once.
+GitLab groups allow you to group projects into directories and give users access to several projects at once.
When you create a new project in GitLab, the default namespace for the project is the personal namespace associated with your GitLab user.
In this document we will see how to create groups, put projects in groups and manage who can access the projects in a group.