From 94c24b89bd238a3af16e7887c4a1420462a8b0f0 Mon Sep 17 00:00:00 2001 From: winh Date: Thu, 10 Aug 2017 21:09:29 +0200 Subject: Make comment form dropdown style consistent --- app/assets/stylesheets/pages/note_form.scss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index 9558924bbcb..9600120b24f 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -27,6 +27,8 @@ .new-note, .note-edit-form { .note-form-actions { + @include new-style-dropdown; + position: relative; margin: $gl-padding 0 0; } -- cgit v1.2.1 From 14a1fe7b6b4575ec6ff7fe4c09a51c7b49b5a0c7 Mon Sep 17 00:00:00 2001 From: winh Date: Tue, 15 Aug 2017 14:07:56 +0200 Subject: Make project selection dropdown for issue editing consistent --- app/assets/stylesheets/framework/selects.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss index a39927eb0df..1c064349968 100644 --- a/app/assets/stylesheets/framework/selects.scss +++ b/app/assets/stylesheets/framework/selects.scss @@ -270,6 +270,7 @@ body[data-page="projects:new"] #select2-drop, body[data-page="projects:blob:new"] #select2-drop, body[data-page="profiles:show"] #select2-drop, +body[data-page="projects:issues:show"] #select2-drop, body[data-page="projects:blob:edit"] #select2-drop { &.select2-drop { color: $gl-text-color; -- cgit v1.2.1 From 2d6733c81f4e02e07d8c4e85289ff2e56bf8371b Mon Sep 17 00:00:00 2001 From: winh Date: Wed, 16 Aug 2017 20:37:31 +0200 Subject: Make protected branches / tags permission dropdowns consistent --- app/assets/stylesheets/pages/projects.scss | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 39c4264e496..2d671c23543 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -822,8 +822,10 @@ pre.light-well { } } -.new_protected_branch, +.new-protected-branch, .new-protected-tag { + @include new-style-dropdown; + label { margin-top: 6px; font-weight: $gl-font-weight-normal; @@ -843,19 +845,9 @@ pre.light-well { .protected-branches-list, .protected-tags-list { - margin-bottom: 30px; - - a { - color: $gl-text-color; - - &:hover { - color: $gl-link-color; - } + @include new-style-dropdown; - &.is-active { - font-weight: $gl-font-weight-bold; - } - } + margin-bottom: 30px; .settings-message { margin: 0; -- cgit v1.2.1 From 39a4c70a9b299f7be4419f0768be010032aec862 Mon Sep 17 00:00:00 2001 From: winh Date: Tue, 15 Aug 2017 14:07:56 +0200 Subject: Make transfer project dropdown in project settings consistent --- app/assets/stylesheets/framework/selects.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss index a39927eb0df..f62dc70186d 100644 --- a/app/assets/stylesheets/framework/selects.scss +++ b/app/assets/stylesheets/framework/selects.scss @@ -267,6 +267,7 @@ // TODO: change global style .ajax-project-dropdown, +body[data-page="projects:edit"] #select2-drop, body[data-page="projects:new"] #select2-drop, body[data-page="projects:blob:new"] #select2-drop, body[data-page="profiles:show"] #select2-drop, -- cgit v1.2.1 From b354754f234055ac8bbb4b02cc9524d6da3843c3 Mon Sep 17 00:00:00 2001 From: winh Date: Mon, 4 Sep 2017 15:16:01 +0200 Subject: Avoid @extend for .text-danger in dropdowns --- app/assets/stylesheets/framework/dropdowns.scss | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index a49f9f99872..e29e1806b38 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -756,7 +756,7 @@ // make sure the text color is not overriden &.text-danger { - @extend .text-danger; + color: $brand-danger; } &.is-focused, @@ -765,6 +765,11 @@ &:focus { background-color: $dropdown-item-hover-bg; color: $gl-text-color; + + // make sure the text color is not overriden + &.text-danger { + color: $brand-danger; + } } &.is-active { -- cgit v1.2.1 From 2065a2d1d9034169d42b06d331fe06b4772b6df4 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Mon, 4 Sep 2017 18:07:37 +0200 Subject: attempt to fix random import file spec error --- spec/features/projects/import_export/import_file_spec.rb | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/spec/features/projects/import_export/import_file_spec.rb b/spec/features/projects/import_export/import_file_spec.rb index 2eb6fab129d..6d1caed61fd 100644 --- a/spec/features/projects/import_export/import_file_spec.rb +++ b/spec/features/projects/import_export/import_file_spec.rb @@ -18,23 +18,25 @@ feature 'Import/Export - project import integration test', js: true do context 'when selecting the namespace' do let(:user) { create(:admin) } - let!(:namespace) { create(:namespace, name: "asd", owner: user) } + let!(:namespace) { create(:namespace, name: "asd" + SecureRandom.hex, path: SecureRandom.hex, owner: user) } + let(:project_path) { 'test-project-path' + SecureRandom.hex } context 'prefilled the path' do scenario 'user imports an exported project successfully' do visit new_project_path select2(namespace.id, from: '#project_namespace_id') - fill_in :project_path, with: 'test-project-path', visible: true + fill_in :project_path, with: project_path, visible: true click_link 'GitLab export' expect(page).to have_content('Import an exported GitLab project') - expect(URI.parse(current_url).query).to eq("namespace_id=#{namespace.id}&path=test-project-path") - expect(Gitlab::ImportExport).to receive(:import_upload_path).with(filename: /\A\h{32}_test-project-path\z/).and_call_original + expect(URI.parse(current_url).query).to eq("namespace_id=#{namespace.id}&path=#{project_path}") + #expect(Gitlab::ImportExport).to receive(:import_upload_path).with(filename: /\A\h{32}_test-project-path#{project_path}\z/).and_call_original attach_file('file', file) + click_on 'Import project' - expect { click_on 'Import project' }.to change { Project.count }.by(1) + expect(Project.count).to eq(1) project = Project.last expect(project).not_to be_nil @@ -64,7 +66,7 @@ feature 'Import/Export - project import integration test', js: true do end scenario 'invalid project' do - namespace = create(:namespace, name: "asd", owner: user) + namespace = create(:namespace, name: "asd" + SecureRandom.hex, owner: user) project = create(:project, namespace: namespace) visit new_project_path -- cgit v1.2.1 From af374be863de648d89f1571f5c311ec9b761294d Mon Sep 17 00:00:00 2001 From: James Lopez Date: Mon, 4 Sep 2017 18:17:37 +0200 Subject: refactor spec --- spec/features/projects/import_export/import_file_spec.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/features/projects/import_export/import_file_spec.rb b/spec/features/projects/import_export/import_file_spec.rb index 6d1caed61fd..bc6b6989a3b 100644 --- a/spec/features/projects/import_export/import_file_spec.rb +++ b/spec/features/projects/import_export/import_file_spec.rb @@ -18,7 +18,7 @@ feature 'Import/Export - project import integration test', js: true do context 'when selecting the namespace' do let(:user) { create(:admin) } - let!(:namespace) { create(:namespace, name: "asd" + SecureRandom.hex, path: SecureRandom.hex, owner: user) } + let!(:namespace) { create(:namespace, name: 'asd', owner: user) } let(:project_path) { 'test-project-path' + SecureRandom.hex } context 'prefilled the path' do @@ -31,7 +31,7 @@ feature 'Import/Export - project import integration test', js: true do expect(page).to have_content('Import an exported GitLab project') expect(URI.parse(current_url).query).to eq("namespace_id=#{namespace.id}&path=#{project_path}") - #expect(Gitlab::ImportExport).to receive(:import_upload_path).with(filename: /\A\h{32}_test-project-path#{project_path}\z/).and_call_original + expect(Gitlab::ImportExport).to receive(:import_upload_path).with(filename: /\A\h{32}_test-project-path.*\z/).and_call_original attach_file('file', file) click_on 'Import project' @@ -66,7 +66,7 @@ feature 'Import/Export - project import integration test', js: true do end scenario 'invalid project' do - namespace = create(:namespace, name: "asd" + SecureRandom.hex, owner: user) + namespace = create(:namespace, name: 'asdf', owner: user) project = create(:project, namespace: namespace) visit new_project_path -- cgit v1.2.1 From 48115be509ce00120d0609f5f18a5bc3804bb21f Mon Sep 17 00:00:00 2001 From: Nick Thomas Date: Wed, 30 Aug 2017 12:00:39 +0100 Subject: Add a system check for the git user's custom SSH configuration --- .../37204-deprecate-git-user-manual-ssh-config.yml | 5 ++ doc/ssh/README.md | 32 +++++++++ .../app/git_user_default_ssh_config_check.rb | 69 +++++++++++++++++++ lib/tasks/gitlab/check.rake | 1 + .../app/git_user_default_ssh_config_check_spec.rb | 79 ++++++++++++++++++++++ 5 files changed, 186 insertions(+) create mode 100644 changelogs/unreleased/37204-deprecate-git-user-manual-ssh-config.yml create mode 100644 lib/system_check/app/git_user_default_ssh_config_check.rb create mode 100644 spec/lib/system_check/app/git_user_default_ssh_config_check_spec.rb diff --git a/changelogs/unreleased/37204-deprecate-git-user-manual-ssh-config.yml b/changelogs/unreleased/37204-deprecate-git-user-manual-ssh-config.yml new file mode 100644 index 00000000000..593e74593c4 --- /dev/null +++ b/changelogs/unreleased/37204-deprecate-git-user-manual-ssh-config.yml @@ -0,0 +1,5 @@ +--- +title: Deprecate custom SSH client configuration for the git user +merge_request: 13930 +author: +type: deprecated diff --git a/doc/ssh/README.md b/doc/ssh/README.md index cf28f1a2eca..793de9d777c 100644 --- a/doc/ssh/README.md +++ b/doc/ssh/README.md @@ -193,6 +193,38 @@ How to add your SSH key to Eclipse: https://wiki.eclipse.org/EGit/User_Guide#Ecl [winputty]: https://the.earth.li/~sgtatham/putty/0.67/htmldoc/Chapter8.html#pubkey-puttygen +## SSH on the GitLab server + +GitLab integrates with the system-installed SSH daemon, designating a user +(typically named `git`) through which all access requests are handled. Users +connecting to the GitLab server over SSH are identified by their SSH key instead +of their username. + +SSH *client* operations performed on the GitLab server wil be executed as this +user. Although it is possible to modify the SSH configuration for this user to, +e.g., provide a private SSH key to authenticate these requests by, this practice +is **not supported** and is strongly discouraged as it presents significant +security risks. + +The GitLab check process includes a check for this condition, and will direct you +to this section if your server is configured like this, e.g.: + +``` +$ gitlab-rake gitlab:check +# ... +Git user has default SSH configuration? ... no + Try fixing it: + mkdir ~/gitlab-check-backup-1504540051 + sudo mv /var/lib/git/.ssh/id_rsa ~/gitlab-check-backup-1504540051 + sudo mv /var/lib/git/.ssh/id_rsa.pub ~/gitlab-check-backup-1504540051 + For more information see: + doc/ssh/README.md in section "SSH on the GitLab server" + Please fix the error above and rerun the checks. +``` + +Remove the custom configuration as soon as you're able to. These customizations +are *explicitly not supported* and may stop working at any time. + ## Troubleshooting If on Git clone you are prompted for a password like `git@gitlab.com's password:` diff --git a/lib/system_check/app/git_user_default_ssh_config_check.rb b/lib/system_check/app/git_user_default_ssh_config_check.rb new file mode 100644 index 00000000000..7b486d78cf0 --- /dev/null +++ b/lib/system_check/app/git_user_default_ssh_config_check.rb @@ -0,0 +1,69 @@ +module SystemCheck + module App + class GitUserDefaultSSHConfigCheck < SystemCheck::BaseCheck + # These files are allowed in the .ssh directory. The `config` file is not + # whitelisted as it may change the SSH client's behaviour dramatically. + WHITELIST = %w[ + authorized_keys + authorized_keys2 + known_hosts + ].freeze + + set_name 'Git user has default SSH configuration?' + set_skip_reason 'skipped (git user is not present or configured)' + + def skip? + !home_dir || !File.directory?(home_dir) + end + + def check? + forbidden_files.empty? + end + + def show_error + backup_dir = "~/gitlab-check-backup-#{Time.now.to_i}" + + instructions = forbidden_files.map do |filename| + "sudo mv #{Shellwords.escape(filename)} #{backup_dir}" + end + + try_fixing_it("mkdir #{backup_dir}", *instructions) + for_more_information('doc/ssh/README.md in section "SSH on the GitLab server"') + fix_and_rerun + end + + private + + def git_user + Gitlab.config.gitlab.user + end + + def home_dir + return @home_dir if defined?(@home_dir) + + @home_dir = + begin + File.expand_path("~#{git_user}") + rescue ArgumentError + nil + end + end + + def ssh_dir + return nil unless home_dir + + File.join(home_dir, '.ssh') + end + + def forbidden_files + @forbidden_files ||= + begin + present = Dir[File.join(ssh_dir, '*')] + whitelisted = WHITELIST.map { |basename| File.join(ssh_dir, basename) } + + present - whitelisted + end + end + end + end +end diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake index 1bd36bbe20a..92a3f503fcb 100644 --- a/lib/tasks/gitlab/check.rake +++ b/lib/tasks/gitlab/check.rake @@ -33,6 +33,7 @@ namespace :gitlab do SystemCheck::App::RedisVersionCheck, SystemCheck::App::RubyVersionCheck, SystemCheck::App::GitVersionCheck, + SystemCheck::App::GitUserDefaultSSHConfigCheck, SystemCheck::App::ActiveUsersCheck ] diff --git a/spec/lib/system_check/app/git_user_default_ssh_config_check_spec.rb b/spec/lib/system_check/app/git_user_default_ssh_config_check_spec.rb new file mode 100644 index 00000000000..7125bfcab59 --- /dev/null +++ b/spec/lib/system_check/app/git_user_default_ssh_config_check_spec.rb @@ -0,0 +1,79 @@ +require 'spec_helper' + +describe SystemCheck::App::GitUserDefaultSSHConfigCheck do + let(:username) { '_this_user_will_not_exist_unless_it_is_stubbed' } + let(:base_dir) { Dir.mktmpdir } + let(:home_dir) { File.join(base_dir, "/var/lib/#{username}") } + let(:ssh_dir) { File.join(home_dir, '.ssh') } + let(:forbidden_file) { 'id_rsa' } + + before do + allow(Gitlab.config.gitlab).to receive(:user).and_return(username) + end + + after do + FileUtils.rm_rf(base_dir) + end + + it 'only whitelists safe files' do + expect(described_class::WHITELIST).to contain_exactly('authorized_keys', 'authorized_keys2', 'known_hosts') + end + + describe '#skip?' do + subject { described_class.new.skip? } + + where(user_exists: [true, false], home_dir_exists: [true, false]) + + with_them do + let(:expected_result) { !user_exists || !home_dir_exists } + + before do + stub_user if user_exists + stub_home_dir if home_dir_exists + end + + it { is_expected.to eq(expected_result) } + end + end + + describe '#check?' do + subject { described_class.new.check? } + + before do + stub_user + end + + it 'fails if a forbidden file exists' do + stub_ssh_file(forbidden_file) + + is_expected.to be_falsy + end + + it "succeeds if the SSH directory doesn't exist" do + FileUtils.rm_rf(ssh_dir) + + is_expected.to be_truthy + end + + it 'succeeds if all the whitelisted files exist' do + described_class::WHITELIST.each do |filename| + stub_ssh_file(filename) + end + + is_expected.to be_truthy + end + end + + def stub_user + allow(File).to receive(:expand_path).with("~#{username}").and_return(home_dir) + end + + def stub_home_dir + FileUtils.mkdir_p(home_dir) + end + + def stub_ssh_file(filename) + FileUtils.mkdir_p(ssh_dir) + FileUtils.touch(File.join(ssh_dir, filename)) + end +end -- cgit v1.2.1 From 7831e5cabacad8af5a0a7fcba0dd3a4153c723fa Mon Sep 17 00:00:00 2001 From: James Lopez Date: Mon, 4 Sep 2017 20:03:59 +0200 Subject: update regex --- spec/features/projects/import_export/import_file_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/features/projects/import_export/import_file_spec.rb b/spec/features/projects/import_export/import_file_spec.rb index bc6b6989a3b..ad2db1a34f4 100644 --- a/spec/features/projects/import_export/import_file_spec.rb +++ b/spec/features/projects/import_export/import_file_spec.rb @@ -31,7 +31,7 @@ feature 'Import/Export - project import integration test', js: true do expect(page).to have_content('Import an exported GitLab project') expect(URI.parse(current_url).query).to eq("namespace_id=#{namespace.id}&path=#{project_path}") - expect(Gitlab::ImportExport).to receive(:import_upload_path).with(filename: /\A\h{32}_test-project-path.*\z/).and_call_original + expect(Gitlab::ImportExport).to receive(:import_upload_path).with(filename: /\A\h{32}_test-project-path\h*\z/).and_call_original attach_file('file', file) click_on 'Import project' -- cgit v1.2.1 From 3c1dfcc0323bac1a932b1a7b3fd5df63c259d5cc Mon Sep 17 00:00:00 2001 From: winh Date: Mon, 4 Sep 2017 22:10:59 +0200 Subject: Make dropdowns on new issue page consistent --- app/assets/stylesheets/framework/dropdowns.scss | 2 ++ app/assets/stylesheets/pages/issues.scss | 8 ++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index a45d5a6dca0..377cd3315e5 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -728,6 +728,8 @@ @mixin new-style-dropdown($selector: '') { #{$selector}.dropdown-menu, #{$selector}.dropdown-menu-nav { + margin-bottom: 24px; + li { padding: 0 1px; diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index e2177f96aee..fe748ff7f0e 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -143,8 +143,12 @@ ul.related-merge-requests > li { } } -.issue-form .select2-container { - width: 250px !important; +.issue-form { + @include new-style-dropdown; + + .select2-container { + width: 250px !important; + } } .issues-footer { -- cgit v1.2.1 From b282e174dc802dcfe269a825fa3a873137f7fc1e Mon Sep 17 00:00:00 2001 From: winh Date: Tue, 15 Aug 2017 14:07:56 +0200 Subject: Make target branch dropdown on merge request edit page consistent --- app/assets/stylesheets/framework/selects.scss | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss index a39927eb0df..3bdf2bd11be 100644 --- a/app/assets/stylesheets/framework/selects.scss +++ b/app/assets/stylesheets/framework/selects.scss @@ -268,13 +268,21 @@ // TODO: change global style .ajax-project-dropdown, body[data-page="projects:new"] #select2-drop, +body[data-page="projects:merge_requests:edit"] #select2-drop, body[data-page="projects:blob:new"] #select2-drop, body[data-page="profiles:show"] #select2-drop, body[data-page="projects:blob:edit"] #select2-drop { &.select2-drop { + border: 1px solid $dropdown-border-color; + border-radius: $border-radius-base; color: $gl-text-color; } + &.select2-drop-above { + border-top: none; + margin-top: -4px; + } + .select2-results { .select2-no-results, .select2-searching, -- cgit v1.2.1 From 9f037034a9a74901f5fa05a5281a4127d8a46d24 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Tue, 5 Sep 2017 07:39:52 +0000 Subject: Adds documentation to use Vuex --- doc/development/fe_guide/vue.md | 291 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 288 insertions(+), 3 deletions(-) diff --git a/doc/development/fe_guide/vue.md b/doc/development/fe_guide/vue.md index 0742b202807..2607353782a 100644 --- a/doc/development/fe_guide/vue.md +++ b/doc/development/fe_guide/vue.md @@ -28,8 +28,9 @@ As always, the Frontend Architectural Experts are available to help with any Vue 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. +In order to achieve this goal, you can either use [vuex](#vuex) or use the [store pattern][state-management], explained below: + +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. @@ -74,6 +75,59 @@ provided as a prop to the main component. Don't forget to follow [these steps.][page_specific_javascript] +### Bootstrapping Gotchas +#### Providing data from Haml to JavaScript +While mounting a Vue application may be a need to provide data from Rails to JavaScript. +To do that, provide the data through `data` attributes in the HTML element and query them while mounting the application. + +_Note:_ You should only do this while initing the application, because the mounted element will be replaced with Vue-generated DOM. + +The advantage of providing data from the DOM to the Vue instance through `props` in the `render` function +instead of querying the DOM inside the main vue component is that makes tests easier by avoiding the need to +create a fixture or an HTML element in the unit test. See the following example: + +```javascript +// haml +.js-vue-app{ data: { endpoint: 'foo' }} + +document.addEventListener('DOMContentLoaded', () => new Vue({ + el: '.js-vue-app', + data() { + const dataset = this.$options.el.dataset; + return { + endpoint: dataset.endpoint, + }; + }, + render(createElement) { + return createElement('my-component', { + props: { + endpoint: this.isLoading, + }, + }); + }, +})); +``` + +#### Accessing the `gl` object +When we need to query the `gl` object for data that won't change during the application's lyfecyle, we should do it in the same place where we query the DOM. +By following this practice, we can avoid the need to mock the `gl` object, which will make tests easier. +It should be done while initializing our Vue instance, and the data should be provided as `props` to the main component: + +##### example: +```javascript + +document.addEventListener('DOMContentLoaded', () => new Vue({ + el: '.js-vue-app', + render(createElement) { + return createElement('my-component', { + props: { + username: gon.current_username, + }, + }); + }, +})); +``` + ### A folder for Components This folder holds all components that are specific of this new feature. @@ -89,6 +143,29 @@ 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] +#### Components Gotchas +1. Using SVGs in components: To use an SVG in a template we need to make it a property we can access through the component. +A `prop` and a property returned by the `data` functions require `vue` to set a `getter` and a `setter` for each of them. +The SVG should be a computed property in order to improve performance, note that computed properties are cached based on their dependencies. + +```javascript +// bad +import svg from 'svg.svg'; +data() { + return { + myIcon: svg, + }; +}; + +// good +import svg from 'svg.svg'; +computed: { + myIcon() { + return svg; + } +} +``` + ### A folder for the Store The Store is a class that allows us to manage the state in a single @@ -430,11 +507,23 @@ describe('Todos App', () => { }); }); ``` +#### `mountComponent` helper +There is an helper in `spec/javascripts/helpers/vue_mount_component_helper.js` that allows you to mount a component with the given props: + +```javascript +import Vue from 'vue'; +import mountComponent from 'helpers/vue_mount_component_helper.js' +import component from 'component.vue' + +const Component = Vue.extend(component); +const data = {prop: 'foo'}; +const vm = mountComponent(Component, data); +``` + #### Test the component's output The main return value of a Vue component is the rendered output. In order to test the component we 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: @@ -481,6 +570,198 @@ new Component({ new Component().$mount(); ``` +## Vuex +To manage the state of an application you may use [Vuex][vuex-docs]. + +_Note:_ All of the below is explained in more detail in the official [Vuex documentation][vuex-docs]. + +### Separation of concerns +Vuex is composed of State, Getters, Mutations, Actions and Modules. + +When a user clicks on an action, we need to `dispatch` it. This action will `commit` a mutation that will change the state. +_Note:_ The action itself will not update the state, only a mutation should update the state. + +#### File structure +When using Vuex at GitLab, separate this concerns into different files to improve readability. If you can, separate the Mutation Types as well: + +``` +└── store + ├── index.js # where we assemble modules and export the store + ├── actions.js # actions + ├── mutations.js # mutations + ├── getters.js # getters + └── mutation_types.js # mutation types +``` +The following examples show an application that lists and adds users to the state. + +##### `index.js` +This is the entry point for our store. You can use the following as a guide: + +```javascript +import Vue from 'vue'; +import Vuex from 'vuex'; +import * as actions from './actions'; +import * as mutations from './mutations'; + +Vue.use(Vuex); + +export default new Vuex.Store({ + actions, + getters, + state: { + users: [], + }, +}); +``` +_Note:_ If the state of the application is too complex, an individual file for the state may be better. + +#### `actions.js` +An action commits a mutatation. In this file, we will write the actions that will call the respective mutation: + +```javascript + import * as types from './mutation-types' + + export const addUser = ({ commit }, user) => { + commit(types.ADD_USER, user); + }; +``` + +To dispatch an action from a component, use the `mapActions` helper: +```javascript +import { mapActions } from 'vuex'; + +{ + methods: { + ...mapActions([ + 'addUser', + ]), + onClickUser(user) { + this.addUser(user); + }, + }, +}; +``` + +#### `getters.js` +Sometimes we may need to get derived state based on store state, like filtering for a specific prop. This can be done through the `getters`: + +```javascript +// get all the users with pets +export getUsersWithPets = (state, getters) => { + return state.users.filter(user => user.pet !== undefined); +}; +``` + +To access a getter from a component, use the `mapGetters` helper: +```javascript +import { mapGetters } from 'vuex'; + +{ + computed: { + ...mapGetters([ + 'getUsersWithPets', + ]), + }, +}; +``` + +#### `mutations.js` +The only way to actually change state in a Vuex store is by committing a mutation. + +```javascript + import * as types from './mutation-types' + export default { + [types.ADD_USER](state, user) { + state.users.push(user); + }, + }; +``` + +#### `mutations_types.js` +From [vuex mutations docs][vuex-mutations]: +> It is a commonly seen pattern to use constants for mutation types in various Flux implementations. This allows the code to take advantage of tooling like linters, and putting all constants in a single file allows your collaborators to get an at-a-glance view of what mutations are possible in the entire application. + +```javascript +export const ADD_USER = 'ADD_USER'; +``` + +### How to include the store in your application +The store should be included in the main component of your application: +```javascript + // app.vue + import store from 'store'; // it will include the index.js file + + export default { + name: 'application', + store, + ... + }; +``` + +### Vuex Gotchas +1. Avoid calling a mutation directly. Always use an action to commit a mutation. Doing so will keep consistency through out the application. From Vuex docs: + + > why don't we just call store.commit('action') directly? Well, remember that mutations must be synchronous? Actions aren't. We can perform asynchronous operations inside an action. + + ```javascript + // component.vue + + // bad + created() { + this.$store.commit('mutation'); + } + + // good + created() { + this.$store.dispatch('action'); + } + ``` +1. When possible, use mutation types instead of hardcoding strings. It will be less error prone. +1. The State will be accessible in all components descending from the use where the store is instantiated. + +### Testing Vuex +#### Testing Vuex concerns +Refer to [vuex docs][vuex-testing] regarding testing Actions, Getters and Mutations. + +#### Testing components that need a store +Smaller components might use `store` properties to access the data. +In order to write unit tests for those components, we need to include the store and provide the correct state: + +```javascript +//component_spec.js +import Vue from 'vue'; +import store from './store'; +import component from './component.vue' + +describe('component', () => { + let vm; + let Component; + + beforeEach(() => { + Component = Vue.extend(issueActions); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('should show a user', () => { + const user = { + name: 'Foo', + age: '30', + }; + + // populate the store + store.dipatch('addUser', user); + + vm = new Component({ + store, + propsData: props, + }).$mount(); + }); +}); +``` + [vue-docs]: http://vuejs.org/guide/index.html [issue-boards]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/assets/javascripts/boards [environments-table]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/assets/javascripts/environments @@ -493,3 +774,7 @@ new Component().$mount(); [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 +[vuex-docs]: https://vuex.vuejs.org +[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 -- cgit v1.2.1