Commit 3a7eb84d authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab-ce master

parents a3218bf4 cc8cbd92
import { __ } from '~/locale';
import emojiRegex from 'emoji-regex';
import InputValidator from '../validators/input_validator';
const invalidInputClass = 'gl-field-error-outline';
export default class NoEmojiValidator {
export default class NoEmojiValidator extends InputValidator {
constructor(opts = {}) {
super();
const container = opts.container || '';
this.noEmojiEmelents = document.querySelectorAll(`${container} .js-block-emoji`);
......@@ -19,45 +20,14 @@ export default class NoEmojiValidator {
const { value } = this.inputDomElement;
this.errorMessage = __('Invalid input, please avoid emojis');
this.validatePattern(value);
this.setValidationStateAndMessage();
}
validatePattern(value) {
const pattern = emojiRegex();
this.hasEmojis = new RegExp(pattern).test(value);
if (this.hasEmojis) {
this.inputDomElement.setCustomValidity(__('Invalid input, please avoid emojis'));
} else {
this.inputDomElement.setCustomValidity('');
}
}
setValidationStateAndMessage() {
if (!this.inputDomElement.checkValidity()) {
this.setInvalidState();
} else {
this.clearFieldValidationState();
}
}
clearFieldValidationState() {
this.inputDomElement.classList.remove(invalidInputClass);
this.inputErrorMessage.classList.add('hide');
}
setInvalidState() {
this.inputDomElement.classList.add(invalidInputClass);
this.setErrorMessage();
}
setErrorMessage() {
if (this.hasEmojis) {
this.inputErrorMessage.innerHTML = this.inputDomElement.validationMessage;
} else {
this.inputErrorMessage.innerHTML = this.inputDomElement.title;
}
this.inputErrorMessage.classList.remove('hide');
this.invalidInput = new RegExp(pattern).test(value);
}
}
<script>
import _ from 'underscore';
import { mapActions, mapState, mapGetters, createNamespacedHelpers } from 'vuex';
import { mapState, mapGetters, createNamespacedHelpers } from 'vuex';
import { sprintf, __ } from '~/locale';
import consts from '../../stores/modules/commit/constants';
import RadioGroup from './radio_group.vue';
import NewMergeRequestOption from './new_merge_request_option.vue';
const { mapState: mapCommitState, mapGetters: mapCommitGetters } = createNamespacedHelpers(
const { mapState: mapCommitState, mapActions: mapCommitActions } = createNamespacedHelpers(
'commit',
);
export default {
components: {
RadioGroup,
NewMergeRequestOption,
},
computed: {
...mapState(['currentBranchId', 'changedFiles', 'stagedFiles']),
...mapCommitState(['commitAction', 'shouldCreateMR', 'shouldDisableNewMrOption']),
...mapGetters(['currentProject', 'currentBranch', 'currentMergeRequest']),
...mapCommitGetters(['shouldDisableNewMrOption']),
...mapCommitState(['commitAction']),
...mapGetters(['currentBranch']),
commitToCurrentBranchText() {
return sprintf(
__('Commit to %{branchName} branch'),
......@@ -25,12 +26,12 @@ export default {
false,
);
},
disableMergeRequestRadio() {
containsStagedChanges() {
return this.changedFiles.length > 0 && this.stagedFiles.length > 0;
},
},
watch: {
disableMergeRequestRadio() {
containsStagedChanges() {
this.updateSelectedCommitAction();
},
},
......@@ -38,11 +39,11 @@ export default {
this.updateSelectedCommitAction();
},
methods: {
...mapActions('commit', ['updateCommitAction', 'toggleShouldCreateMR']),
...mapCommitActions(['updateCommitAction']),
updateSelectedCommitAction() {
if (this.currentBranch && !this.currentBranch.can_push) {
this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH);
} else if (this.disableMergeRequestRadio) {
} else if (this.containsStagedChanges) {
this.updateCommitAction(consts.COMMIT_TO_CURRENT_BRANCH);
}
},
......@@ -56,7 +57,7 @@ export default {
</script>
<template>
<div class="append-bottom-15 ide-commit-radios">
<div class="append-bottom-15 ide-commit-options">
<radio-group
:value="$options.commitToCurrentBranch"
:disabled="currentBranch && !currentBranch.can_push"
......@@ -69,17 +70,6 @@ export default {
:label="__('Create a new branch')"
:show-input="true"
/>
<hr class="my-2" />
<label class="mb-0">
<input
:checked="shouldCreateMR"
:disabled="shouldDisableNewMrOption"
type="checkbox"
@change="toggleShouldCreateMR"
/>
<span class="prepend-left-10" :class="{ 'text-secondary': shouldDisableNewMrOption }">
{{ __('Start a new merge request') }}
</span>
</label>
<new-merge-request-option />
</div>
</template>
<script>
import { mapGetters, createNamespacedHelpers } from 'vuex';
const {
mapState: mapCommitState,
mapGetters: mapCommitGetters,
mapActions: mapCommitActions,
} = createNamespacedHelpers('commit');
export default {
computed: {
...mapCommitState(['shouldCreateMR']),
...mapCommitGetters(['isCommittingToCurrentBranch', 'isCommittingToDefaultBranch']),
...mapGetters(['hasMergeRequest', 'isOnDefaultBranch']),
currentBranchHasMr() {
return this.hasMergeRequest && this.isCommittingToCurrentBranch;
},
showNewMrOption() {
return (
this.isCommittingToDefaultBranch || !this.currentBranchHasMr || this.isCommittingToNewBranch
);
},
},
mounted() {
this.setShouldCreateMR();
},
methods: {
...mapCommitActions(['toggleShouldCreateMR', 'setShouldCreateMR']),
},
};
</script>
<template>
<div v-if="showNewMrOption">
<hr class="my-2" />
<label class="mb-0">
<input :checked="shouldCreateMR" type="checkbox" @change="toggleShouldCreateMR" />
<span class="prepend-left-10">
{{ __('Start a new merge request') }}
</span>
</label>
</div>
</template>
......@@ -97,7 +97,12 @@ export const lastCommit = (state, getters) => {
export const currentBranch = (state, getters) =>
getters.currentProject && getters.currentProject.branches[state.currentBranchId];
export const branchName = (_state, getters) => getters.currentBranch && getters.currentBranch.name;
export const packageJson = state => state.entries[packageJsonPath];
export const isOnDefaultBranch = (_state, getters) =>
getters.currentProject && getters.currentProject.default_branch === getters.branchName;
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
......@@ -18,15 +18,34 @@ export const discardDraft = ({ commit }) => {
commit(types.UPDATE_COMMIT_MESSAGE, '');
};
export const updateCommitAction = ({ commit, rootGetters }, commitAction) => {
export const updateCommitAction = ({ commit, dispatch }, commitAction) => {
commit(types.UPDATE_COMMIT_ACTION, {
commitAction,
currentMergeRequest: rootGetters.currentMergeRequest,
});
dispatch('setShouldCreateMR');
};
export const toggleShouldCreateMR = ({ commit }) => {
commit(types.TOGGLE_SHOULD_CREATE_MR);
commit(types.INTERACT_WITH_NEW_MR);
};
export const setShouldCreateMR = ({
commit,
getters,
rootGetters,
state: { interactedWithNewMR },
}) => {
const committingToExistingMR =
getters.isCommittingToCurrentBranch &&
rootGetters.hasMergeRequest &&
!rootGetters.isOnDefaultBranch;
if ((getters.isCommittingToDefaultBranch && !interactedWithNewMR) || committingToExistingMR) {
commit(types.TOGGLE_SHOULD_CREATE_MR, false);
} else if (!interactedWithNewMR) {
commit(types.TOGGLE_SHOULD_CREATE_MR, true);
}
};
export const updateBranchName = ({ commit }, branchName) => {
......
......@@ -48,8 +48,11 @@ export const preBuiltCommitMessage = (state, _, rootState) => {
export const isCreatingNewBranch = state => state.commitAction === consts.COMMIT_TO_NEW_BRANCH;
export const shouldDisableNewMrOption = (state, _getters, _rootState, rootGetters) =>
rootGetters.currentMergeRequest && state.commitAction === consts.COMMIT_TO_CURRENT_BRANCH;
export const isCommittingToCurrentBranch = state =>
state.commitAction === consts.COMMIT_TO_CURRENT_BRANCH;
export const isCommittingToDefaultBranch = (_state, getters, _rootState, rootGetters) =>
getters.isCommittingToCurrentBranch && rootGetters.isOnDefaultBranch;
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
......@@ -3,3 +3,4 @@ export const UPDATE_COMMIT_ACTION = 'UPDATE_COMMIT_ACTION';
export const UPDATE_NEW_BRANCH_NAME = 'UPDATE_NEW_BRANCH_NAME';
export const UPDATE_LOADING = 'UPDATE_LOADING';
export const TOGGLE_SHOULD_CREATE_MR = 'TOGGLE_SHOULD_CREATE_MR';
export const INTERACT_WITH_NEW_MR = 'INTERACT_WITH_NEW_MR';
import * as types from './mutation_types';
import consts from './constants';
export default {
[types.UPDATE_COMMIT_MESSAGE](state, commitMessage) {
......@@ -7,14 +6,8 @@ export default {
commitMessage,
});
},
[types.UPDATE_COMMIT_ACTION](state, { commitAction, currentMergeRequest }) {
Object.assign(state, {
commitAction,
shouldCreateMR:
commitAction === consts.COMMIT_TO_CURRENT_BRANCH && currentMergeRequest
? false
: state.shouldCreateMR,
});
[types.UPDATE_COMMIT_ACTION](state, { commitAction }) {
Object.assign(state, { commitAction });
},
[types.UPDATE_NEW_BRANCH_NAME](state, newBranchName) {
Object.assign(state, {
......@@ -26,9 +19,12 @@ export default {
submitCommitLoading,
});
},
[types.TOGGLE_SHOULD_CREATE_MR](state) {
[types.TOGGLE_SHOULD_CREATE_MR](state, shouldCreateMR) {
Object.assign(state, {
shouldCreateMR: !state.shouldCreateMR,
shouldCreateMR: shouldCreateMR === undefined ? !state.shouldCreateMR : shouldCreateMR,
});
},
[types.INTERACT_WITH_NEW_MR](state) {
Object.assign(state, { interactedWithNewMR: true });
},
};
......@@ -4,4 +4,5 @@ export default () => ({
newBranchName: '',
submitCommitLoading: false,
shouldCreateMR: false,
interactedWithNewMR: false,
});
......@@ -172,7 +172,7 @@ export default {
if (!this.hasMetrics) {
this.setGettingStartedEmptyState();
} else {
this.fetchData(getTimeDiff(this.timeWindows.eightHours));
this.fetchData(getTimeDiff(this.selectedTimeWindow));
sidebarMutationObserver = new MutationObserver(this.onSidebarMutation);
sidebarMutationObserver.observe(document.querySelector('.layout-page'), {
......
import $ from 'jquery';
import LengthValidator from './length_validator';
import UsernameValidator from './username_validator';
import NoEmojiValidator from '../../../emoji/no_emoji_validator';
import SigninTabsMemoizer from './signin_tabs_memoizer';
......@@ -6,6 +7,7 @@ import OAuthRememberMe from './oauth_remember_me';
import preserveUrlFragment from './preserve_url_fragment';
document.addEventListener('DOMContentLoaded', () => {
new LengthValidator(); // eslint-disable-line no-new
new UsernameValidator(); // eslint-disable-line no-new
new SigninTabsMemoizer(); // eslint-disable-line no-new
new NoEmojiValidator(); // eslint-disable-line no-new
......
import InputValidator from '../../../validators/input_validator';
const errorMessageClass = 'gl-field-error';
export default class LengthValidator extends InputValidator {
constructor(opts = {}) {
super();
const container = opts.container || '';
const validateLengthElements = document.querySelectorAll(`${container} .js-validate-length`);
validateLengthElements.forEach(element =>
element.addEventListener('input', this.eventHandler.bind(this)),
);
}
eventHandler(event) {
this.inputDomElement = event.target;
this.inputErrorMessage = this.inputDomElement.parentElement.querySelector(
`.${errorMessageClass}`,
);
const { value } = this.inputDomElement;
const { maxLengthMessage, maxLength } = this.inputDomElement.dataset;
this.errorMessage = maxLengthMessage;
this.invalidInput = value.length > parseInt(maxLength, 10);
this.setValidationStateAndMessage();
}
}
const invalidInputClass = 'gl-field-error-outline';
export default class InputValidator {
constructor() {
this.inputDomElement = {};
this.inputErrorMessage = {};
this.errorMessage = null;
this.invalidInput = null;
}
setValidationStateAndMessage() {
this.setValidationMessage();
const isInvalidInput = !this.inputDomElement.checkValidity();
this.inputDomElement.classList.toggle(invalidInputClass, isInvalidInput);
this.inputErrorMessage.classList.toggle('hide', !isInvalidInput);
}
setValidationMessage() {
if (this.invalidInput) {
this.inputDomElement.setCustomValidity(this.errorMessage);
this.inputErrorMessage.innerHTML = this.errorMessage;
} else {
this.resetValidationMessage();
}
}
resetValidationMessage() {
if (this.inputDomElement.validationMessage === this.errorMessage) {
this.inputDomElement.setCustomValidity('');
this.inputErrorMessage.innerHTML = this.inputDomElement.title;
}
}
}
......@@ -719,7 +719,7 @@ $ide-commit-header-height: 48px;
border: 1px solid $white-dark;
}
.ide-commit-radios {
.ide-commit-options {
label {
font-weight: normal;
......
......@@ -73,7 +73,8 @@
.login-body {
font-size: 13px;
input + p {
input + p,
input ~ p.field-validation {
margin-top: 5px;
}
......
- max_name_length = 128
- max_username_length = 255
#register-pane.tab-pane.login-box{ role: 'tabpanel' }
.login-body
= form_for(resource, as: "new_#{resource_name}", url: registration_path(resource_name), html: { class: "new_new_user gl-show-field-errors", "aria-live" => "assertive" }) do |f|
......@@ -5,13 +7,13 @@
= render "devise/shared/error_messages", resource: resource
.name.form-group
= f.label :name, _('Full name'), class: 'label-bold'
= f.text_field :name, class: "form-control top qa-new-user-name js-block-emoji", required: true, title: _("This field is required.")
= f.text_field :name, class: "form-control top qa-new-user-name js-block-emoji js-validate-length", :data => { :max_length => max_name_length, :max_length_message => s_("SignUp|Name is too long (maximum is %{max_length} characters).") % { max_length: max_name_length } }, required: true, title: _("This field is required.")
.username.form-group
= f.label :username, class: 'label-bold'
= f.text_field :username, class: "form-control middle qa-new-user-username js-block-emoji", pattern: Gitlab::PathRegex::NAMESPACE_FORMAT_REGEX_JS, required: true, title: _("Please create a username with only alphanumeric characters.")
%p.validation-error.hide= _('Username is already taken.')
%p.validation-success.hide= _('Username is available.')
%p.validation-pending.hide= _('Checking username availability...')
= f.text_field :username, class: "form-control middle qa-new-user-username js-block-emoji js-validate-length", :data => { :max_length => max_username_length, :max_length_message => s_("SignUp|Username is too long (maximum is %{max_length} characters).") % { max_length: max_username_length } }, pattern: Gitlab::PathRegex::NAMESPACE_FORMAT_REGEX_JS, required: true, title: _("Please create a username with only alphanumeric characters.")
%p.validation-error.field-validation.hide= _('Username is already taken.')
%p.validation-success.field-validation.hide= _('Username is available.')
%p.validation-pending.field-validation.hide= _('Checking username availability...')
.form-group
= f.label :email, class: 'label-bold'
= f.email_field :email, class: "form-control middle qa-new-user-email", required: true, title: _("Please provide a valid email address.")
......
---
title: Default MR checkbox to true in most cases
merge_request: !28665
author:
type: changed
---
title: Update registration form to indicate invalid name or username length on input
merge_request: 28095
author: Jiaan Louw
type: changed
---
title: Use the selected time window for metrics dashboard
merge_request: 29152
author:
type: fixed
......@@ -23,10 +23,10 @@ environments including [Basic Scaling](README.md#basic-scaling) and
### Provide your own PostgreSQL instance **[CORE ONLY]**
If you want to use your own deployed PostgreSQL instance(s),
If you want to use your own deployed PostgreSQL instance(s),
see [Provide your own PostgreSQL instance](#provide-your-own-postgresql-instance-core-only)
for more details. However, you can use the GitLab Omnibus package to easily
deploy the bundled PostgreSQL.
for more details. However, you can use the GitLab Omnibus package to easily
deploy the bundled PostgreSQL.
### Standalone PostgreSQL using GitLab Omnibus **[CORE ONLY]**
......@@ -36,19 +36,19 @@ deploy the bundled PostgreSQL.
- Do not complete any other steps on the download page.
1. Generate a password hash for PostgreSQL. This assumes you will use the default
username of `gitlab` (recommended). The command will request a password
and confirmation. Use the value that is output by this command in the next
and confirmation. Use the value that is output by this command in the next
step as the value of `POSTGRESQL_PASSWORD_HASH`.
```sh
sudo gitlab-ctl pg-password-md5 gitlab
```
1. Edit `/etc/gitlab/gitlab.rb` and add the contents below, updating placeholder
values appropriately.
values appropriately.
- `POSTGRESQL_PASSWORD_HASH` - The value output from the previous step
- `APPLICATION_SERVER_IP_BLOCKS` - A space delimited list of IP subnets or IP
addresses of the GitLab application servers that will connect to the
addresses of the GitLab application servers that will connect to the
database. Example: `%w(123.123.123.123/32 123.123.123.234/32)`
```ruby
......@@ -65,11 +65,11 @@ deploy the bundled PostgreSQL.
postgresql['listen_address'] = '0.0.0.0'
postgresql['port'] = 5432
# Replace POSTGRESQL_PASSWORD_HASH with a generated md5 value
# Replace POSTGRESQL_PASSWORD_HASH with a generated md5 value
postgresql['sql_user_password'] = 'POSTGRESQL_PASSWORD_HASH'
# Replace XXX.XXX.XXX.XXX/YY with Network Address
# ????
# ????
postgresql['trust_auth_cidr_addresses'] = %w(APPLICATION_SERVER_IP_BLOCKS)
# Disable automatic database migrations
......@@ -77,12 +77,12 @@ deploy the bundled PostgreSQL.
```
NOTE: **Note:** The role `postgres_role` was introduced with GitLab 10.3
1. [Reconfigure GitLab] for the changes to take effect.
1. Note the PostgreSQL node's IP address or hostname, port, and
plain text password. These will be necessary when configuring the GitLab
application servers later.
Advanced configuration options are supported and can be added if
needed.
......@@ -101,18 +101,19 @@ environments including [Horizontal](README.md#horizontal),
If you want to use your own deployed PostgreSQL instance(s),
see [Provide your own PostgreSQL instance](#provide-your-own-postgresql-instance-core-only)
for more details. However, you can use the GitLab Omnibus package to easily
deploy the bundled PostgreSQL.
deploy the bundled PostgreSQL.
### High Availability with GitLab Omnibus **[PREMIUM ONLY]**
> Important notes:
>
> - This document will focus only on configuration supported with [GitLab Premium](https://about.gitlab.com/pricing/), using the Omnibus GitLab package.
> - If you are a Community Edition or Starter user, consider using a cloud hosted solution.
> - This document will not cover installations from source.
>
> - If HA setup is not what you were looking for, see the [database configuration document](http://docs.gitlab.com/omnibus/settings/database.html)
> for the Omnibus GitLab packages.
>
> Please read this document fully before attempting to configure PostgreSQL HA
> for GitLab.
>
......@@ -122,9 +123,9 @@ The recommended configuration for a PostgreSQL HA requires:
- A minimum of three database nodes
- Each node will run the following services:
- `PostgreSQL` - The database itself
- `repmgrd` - A service to monitor, and handle failover in case of a failure
- `Consul` agent - Used for service discovery, to alert other nodes when failover occurs
- `PostgreSQL` - The database itself
- `repmgrd` - A service to monitor, and handle failover in case of a failure
- `Consul` agent - Used for service discovery, to alert other nodes when failover occurs
- A minimum of three `Consul` server nodes
- A minimum of one `pgbouncer` service node
......@@ -142,7 +143,7 @@ Database nodes run two services with PostgreSQL:
- Selecting a new master for the cluster.
- Promoting the new node to master.
- Instructing remaining servers to follow the new master node.
On failure, the old master node is automatically evicted from the cluster, and should be rejoined manually once recovered.
- Consul. Monitors the status of each node in the database cluster and tracks its health in a service definition on the consul cluster.
......@@ -171,13 +172,10 @@ Similarly, PostgreSQL access is controlled based on the network source.
This is why you will need:
> IP address of each nodes network interface
> - This can be set to `0.0.0.0` to listen on all interfaces. It cannot
> be set to the loopack address `127.0.0.1`
>
> Network Address
> - This can be in subnet (i.e. `192.168.0.0/255.255.255.0`) or CIDR (i.e.
> `192.168.0.0/24`) form.
- IP address of each nodes network interface. This can be set to `0.0.0.0` to
listen on all interfaces. It cannot be set to the loopack address `127.0.0.1`.
- Network Address. This can be in subnet (i.e. `192.168.0.0/255.255.255.0`)
or CIDR (i.e. `192.168.0.0/24`) form.
##### User information
......@@ -199,7 +197,7 @@ When using default setup, minimum configuration requires:
sudo gitlab-ctl pg-password-md5 CONSUL_USERNAME
```
- `CONSUL_SERVER_NODES`. The IP addresses or DNS records of the Consul server nodes.
- `CONSUL_SERVER_NODES`. The IP addresses or DNS records of the Consul server nodes.
Few notes on the service itself:
......@@ -220,8 +218,7 @@ the number of database nodes in the cluster.
This is used to prevent replication from using up all of the
available database connections.
> Note:
> - In this document we are assuming 3 database nodes, which makes this configuration:
In this document we are assuming 3 database nodes, which makes this configuration:
```
postgresql['max_wal_senders'] = 4
......@@ -277,7 +274,7 @@ be allowed to authenticate with the service.
Few notes on the service itself:
- The service runs under the same system account as the database
- In the package, this is by default `gitlab-psql`
- In the package, this is by default `gitlab-psql`
- The service will have a superuser database user account generated for it
- This defaults to `gitlab_repmgr`
......@@ -327,7 +324,7 @@ On each Consul node perform the following:
Before moving on, make sure Consul is configured correctly. Run the following
command to verify all server nodes are communicating:
```
```sh
/opt/gitlab/embedded/bin/consul members
```
......@@ -401,14 +398,15 @@ check the [Troubleshooting section](#troubleshooting) before proceeding.
repmgr['master_on_initialization'] = false
```
1. [Reconfigure GitLab] for te changes to take effect.
1. [Reconfigure GitLab] for the changes to take effect.
> Please note:
>
> - If you want your database to listen on a specific interface, change the config:
> `postgresql['listen_address'] = '0.0.0.0'`
> `postgresql['listen_address'] = '0.0.0.0'`.
> - If your Pgbouncer service runs under a different user account,
> you also need to specify: `postgresql['pgbouncer_user'] = PGBOUNCER_USERNAME` in
> your configuration
> your configuration.
##### Database nodes post-configuration
......@@ -449,7 +447,6 @@ Select one node as a primary node.
is not an IP address, it will need to be a resolvable name (via DNS or
`/etc/hosts`)
###### Secondary nodes
1. Set up the repmgr standby:
......@@ -500,7 +497,7 @@ Before moving on, make sure the databases are configured correctly. Run the
following command on the **primary** node to verify that replication is working
properly:
```
```sh
gitlab-ctl repmgr cluster show
```
......@@ -518,7 +515,7 @@ If the 'Role' column for any node says "FAILED", check the
Also, check that the check master command works successfully on each node:
```
```sh
su - gitlab-consul
gitlab-ctl repmgr-check-master || echo 'This node is a standby repmgr node'
```
......@@ -649,7 +646,7 @@ in the Troubleshooting section before proceeding.
##### Ensure GitLab is running
At this point, your GitLab instance should be up and running. Verify you are
able to login, and create issues and merge requests. If you have troubles check
able to login, and create issues and merge requests. If you have troubles check
the [Troubleshooting section](#troubleshooting).
#### Example configuration
......@@ -665,13 +662,13 @@ can connect to each freely other on those addresses.
Here is a list and description of each machine and the assigned IP:
* `10.6.0.11`: Consul 1
* `10.6.0.12`: Consul 2
* `10.6.0.13`: Consul 3
* `10.6.0.21`: PostgreSQL master
* `10.6.0.22`: PostgreSQL secondary
* `10.6.0.23`: PostgreSQL secondary
* `10.6.0.31`: GitLab application
- `10.6.0.11`: Consul 1
- `10.6.0.12`: Consul 2
- `10.6.0.13`: Consul 3
- `10.6.0.21`: PostgreSQL master
- `10.6.0.22`: PostgreSQL secondary
- `10.6.0.23`: PostgreSQL secondary
- `10.6.0.31`: GitLab application
All passwords are set to `toomanysecrets`, please do not use this password or derived hashes.
......@@ -735,7 +732,7 @@ consul['configuration'] = {
On secondary nodes, edit `/etc/gitlab/gitlab.rb` and add all the configuration
added to primary node, noted above. In addition, append the following
configuration
configuration:
```
# HA setting to specify if a node should attempt to be master on initialization
......@@ -839,10 +836,10 @@ In this example we start with all servers on the same 10.6.0.0/16 private networ
Here is a list and description of each machine and the assigned IP:
* `10.6.0.21`: PostgreSQL master
* `10.6.0.22`: PostgreSQL secondary
* `10.6.0.23`: PostgreSQL secondary
* `10.6.0.31`: GitLab application
- `10.6.0.21`: PostgreSQL master
- `10.6.0.22`: PostgreSQL secondary
- `10.6.0.23`: PostgreSQL secondary
- `10.6.0.31`: GitLab application
All passwords are set to `toomanysecrets`, please do not use this password or derived hashes.
......@@ -853,6 +850,7 @@ Please note that after the initial configuration, if a failover occurs, the Post
##### Example minimal configuration for database servers
##### Primary node
On primary database node edit `/etc/gitlab/gitlab.rb`:
```ruby
......@@ -1047,7 +1045,6 @@ For example:
repmgr['trust_auth_cidr_addresses'] = %w(192.168.1.44/32 db2.example.com)
```
##### MD5 Authentication
If you are running on an untrusted network, repmgr can use md5 authentication
......@@ -1114,7 +1111,7 @@ steps to fix the problem:
1. Change to the `gitlab-consul` user - `su - gitlab-consul`
1. Try the check command again - `gitlab-ctl repmgr-check-master`.
Now there should not be errors. If errors still occur then there is another problem.
Now there should not be errors. If errors still occur then there is another problem.
#### PGBouncer error `ERROR: pgbouncer cannot connect to server`
......@@ -1157,8 +1154,6 @@ If you're running into an issue with a component not outlined here, be sure to c
**Note**: We recommend that you follow the instructions here for a full [PostgreSQL cluster](#high-availability-with-gitlab-omnibus-premium-only).
If you are reading this section due to an old bookmark, you can find that old documentation [in the repository](https://gitlab.com/gitlab-org/gitlab-ce/blob/v10.1.4/doc/administration/high_availability/database.md#configure-using-omnibus).
---
Read more on high-availability configuration:
1. [Configure Redis](redis.md)
......
---
type: reference
---
# Custom group-level project templates **[PREMIUM]**
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/6861) in [GitLab Premium](https://about.gitlab.com/pricing) 11.6.
......@@ -24,3 +28,15 @@ Projects of nested subgroups of a selected template source cannot be used.
Repository and database information that are copied over to each new project are
identical to the data exported with [GitLab's Project Import/Export](../project/settings/import_export.md).
<!-- ## Troubleshooting
Include any troubleshooting steps that you can foresee. If you know beforehand what issues
one might have when setting this up, or when something is changed, or on upgrading, it's
important to describe those, too. Think of things that may go wrong and include them here.
This is important to minimize requests for support, and to avoid doc comments with
questions that you know someone might ask.
Each scenario can be a third-level heading, e.g. `### Getting error message X`.
If you have none to add when creating a doc, leave this section in place
but commented out to help encourage others to add to it in the future. -->
---
type: reference, howto
---
# Epics **[ULTIMATE]**
> Introduced in [GitLab Ultimate][ee] 10.2.
......@@ -215,3 +219,15 @@ Once you wrote your comment, you can either:
## Notifications
- [Receive notifications](../../../workflow/notifications.md) for epic events.
<!-- ## Troubleshooting
Include any troubleshooting steps that you can foresee. If you know beforehand what issues
one might have when setting this up, or when something is changed, or on upgrading, it's
important to describe those, too. Think of things that may go wrong and include them here.
This is important to minimize requests for support, and to avoid doc comments with
questions that you know someone might ask.
Each scenario can be a third-level heading, e.g. `### Getting error message X`.
If you have none to add when creating a doc, leave this section in place
but commented out to help encourage others to add to it in the future. -->
---
type: reference, howto
---
# Insights **[ULTIMATE]**
> Introduced in [GitLab Ultimate](https://about.gitlab.com/pricing/) 11.9 behind the `insights` feature flag.
......@@ -44,3 +48,15 @@ access to the project they belong to, or because they are confidential) are
filtered out of the Insights charts.
You may also consult the [group permissions table](../../permissions.md#group-members-permissions).
<!-- ## Troubleshooting
Include any troubleshooting steps that you can foresee. If you know beforehand what issues
one might have when setting this up, or when something is changed, or on upgrading, it's
important to describe those, too. Think of things that may go wrong and include them here.
This is important to minimize requests for support, and to avoid doc comments with
questions that you know someone might ask.
Each scenario can be a third-level heading, e.g. `### Getting error message X`.
If you have none to add when creating a doc, leave this section in place
but commented out to help encourage others to add to it in the future. -->
---
type: reference
---
# Roadmap **[ULTIMATE]**
> Introduced in [GitLab Ultimate](https://about.gitlab.com/pricing) 10.5.
......@@ -19,13 +23,20 @@ Epics in the view can be sorted by:
- **Start date**
- **Due date**
Each option contains a button that can toggle the order between **ascending** and **descending**. The sort option and order will be persisted to be used wherever epics are browsed including the [epics list view](../epics/index.md).
Each option contains a button that toggles the sort order between **ascending** and **descending**. The sort option and order will be persisted when browsing Epics,
including the [epics list view](../epics/index.md).
Roadmaps can also be [visualized inside an epic](../epics/index.md#roadmap-in-epics).
## Timeline duration
Starting with [GitLab Ultimate][ee] 11.0, Roadmap supports three different date ranges; Quarters, Months (Default) and Weeks.
> Introduced in [GitLab Ultimate](https://about.gitlab.com/pricing) 11.0.
Roadmap supports the following date ranges:
- Quarters
- Months (Default)
- Weeks
### Quarters
......@@ -62,3 +73,15 @@ and due date. If an epic doesn't have a due date, the timeline bar fades
away towards the future. Similarly, if an epic doesn't have a start date, the
timeline bar becomes more visible as it approaches the epic's due date on the
timeline.
<!-- ## Troubleshooting
Include any troubleshooting steps that you can foresee. If you know beforehand what issues
one might have when setting this up, or when something is changed, or on upgrading, it's
important to describe those, too. Think of things that may go wrong and include them here.
This is important to minimize requests for support, and to avoid doc comments with
questions that you know someone might ask.
Each scenario can be a third-level heading, e.g. `### Getting error message X`.
If you have none to add when creating a doc, leave this section in place
but commented out to help encourage others to add to it in the future. -->
......@@ -25,6 +25,13 @@ describe 'Signup' do
expect(find('.username')).not_to have_css '.gl-field-error-outline'
end
it 'does not show an error border if the username length is not longer than 255 characters' do
fill_in 'new_user_username', with: 'u' * 255
wait_for_requests
expect(find('.username')).not_to have_css '.gl-field-error-outline'
end
it 'shows an error border if the username already exists' do
existing_user = create(:user)
......@@ -41,6 +48,20 @@ describe 'Signup' do
expect(find('.username')).to have_css '.gl-field-error-outline'
end
it 'shows an error border if the username is longer than 255 characters' do
fill_in 'new_user_username', with: 'u' * 256
wait_for_requests
expect(find('.username')).to have_css '.gl-field-error-outline'
end
it 'shows an error message if the username is longer than 255 characters' do
fill_in 'new_user_username', with: 'u' * 256
wait_for_requests
expect(page).to have_content("Username is too long (maximum is 255 characters).")
end
it 'shows an error message on submit if the username contains special characters' do
fill_in 'new_user_username', with: 'new$user!username'
wait_for_requests
......@@ -67,14 +88,35 @@ describe 'Signup' do
before do
visit root_path
click_link 'Register'
simulate_input('#new_user_name', 'Ehsan 🦋')
end
it 'does not show an error border if the user\'s fullname length is not longer than 128 characters' do
fill_in 'new_user_name', with: 'u' * 128
expect(find('.name')).not_to have_css '.gl-field-error-outline'
end
it 'shows an error border if the user\'s fullname contains an emoji' do
simulate_input('#new_user_name', 'Ehsan 🦋')
expect(find('.name')).to have_css '.gl-field-error-outline'
end
it 'shows an error border if the user\'s fullname is longer than 128 characters' do
fill_in 'new_user_name', with: 'n' * 129
expect(find('.name')).to have_css '.gl-field-error-outline'
end
it 'shows an error message if the user\'s fullname is longer than 128 characters' do
fill_in 'new_user_name', with: 'n' * 129
expect(page).to have_content("Name is too long (maximum is 128 characters).")
end
it 'shows an error message if the username contains emojis' do
simulate_input('#new_user_name', 'Ehsan 🦋')
expect(page).to have_content("Invalid input, please avoid emojis")
end
end
......
......@@ -54,5 +54,20 @@ describe('IDE commit module mutations', () => {
expect(state.shouldCreateMR).toBe(false);
});
it('sets shouldCreateMR to given value when passed in', () => {
state.shouldCreateMR = false;
mutations.TOGGLE_SHOULD_CREATE_MR(state, false);
expect(state.shouldCreateMR).toBe(false);
});
});
describe('INTERACT_WITH_NEW_MR', () => {
it('sets interactedWithNewMR to true', () => {
mutations.INTERACT_WITH_NEW_MR(state);
expect(state.interactedWithNewMR).toBe(true);
});
});
});
import { mount, createLocalVue } from '@vue/test-utils';
import DroplabDropdownButton from '~/vue_shared/components/droplab_dropdown_button.vue';
const mockActions = [
{
title: 'Foo',
description: 'Some foo action',
},
{
title: 'Bar',
description: 'Some bar action',
},
];
const createComponent = ({
size = '',
dropdownClass = '',
actions = mockActions,
defaultAction = 0,
}) => {
const localVue = createLocalVue();
return mount(DroplabDropdownButton, {
localVue,
propsData: {
size,
dropdownClass,
actions,
defaultAction,
},
});
};
describe('DroplabDropdownButton', () => {
let wrapper;
beforeEach(() => {
wrapper = createComponent({});
});
afterEach(() => {
wrapper.destroy();
});
describe('data', () => {
it('contains `selectedAction` representing value of `defaultAction` prop', () => {
expect(wrapper.vm.selectedAction).toBe(0);
});
});
describe('computed', () => {
describe('selectedActionTitle', () => {
it('returns string containing title of selected action', () => {
wrapper.setData({ selectedAction: 0 });
expect(wrapper.vm.selectedActionTitle).toBe(mockActions[0].title);
wrapper.setData({ selectedAction: 1 });
expect(wrapper.vm.selectedActionTitle).toBe(mockActions[1].title);
});
});
describe('buttonSizeClass', () => {
it('returns string containing button sizing class based on `size` prop', done => {
const wrapperWithSize = createComponent({
size: 'sm',
});
wrapperWithSize.vm.$nextTick(() => {
expect(wrapperWithSize.vm.buttonSizeClass).toBe('btn-sm');
done();
wrapperWithSize.destroy();
});
});
});
});
describe('methods', () => {
describe('handlePrimaryActionClick', () => {
it('emits `onActionClick` event on component with selectedAction object as param', () => {
jest.spyOn(wrapper.vm, '$emit');
wrapper.setData({ selectedAction: 0 });
wrapper.vm.handlePrimaryActionClick();
expect(wrapper.vm.$emit).toHaveBeenCalledWith('onActionClick', mockActions[0]);
});
});
describe('handleActionClick', () => {
it('emits `onActionSelect` event on component with selectedAction index as param', () => {
jest.spyOn(wrapper.vm, '$emit');
wrapper.vm.handleActionClick(1);
expect(wrapper.vm.$emit).toHaveBeenCalledWith('onActionSelect', 1);
});
});
});
describe('template', () => {
it('renders default action button', () => {
const defaultButton = wrapper.findAll('.btn').at(0);
expect(defaultButton.text()).toBe(mockActions[0].title);
});
it('renders dropdown button', () => {
const dropdownButton = wrapper.findAll('.dropdown-toggle').at(0);
expect(dropdownButton.isVisible()).toBe(true);
});
it('renders dropdown actions', () => {
const dropdownActions = wrapper.findAll('.dropdown-menu li button');
Array(dropdownActions.length)
.fill()
.forEach((_, index) => {
const actionContent = dropdownActions.at(index).find('.description');
expect(actionContent.find('strong').text()).toBe(mockActions[index].title);
expect(actionContent.find('p').text()).toBe(mockActions[index].description);
});
});
it('renders divider between dropdown actions', () => {
const dropdownDivider = wrapper.find('.dropdown-menu .divider');
expect(dropdownDivider.isVisible()).toBe(true);
});
});
});
......@@ -73,47 +73,4 @@ describe('IDE commit sidebar actions', () => {
expect(vm.commitToCurrentBranchText).not.toContain(injectedSrc);
});
});
describe('create new MR checkbox', () => {
it('disables `createMR` button when an MR already exists and committing to current branch', () => {
createComponent({ hasMR: true, commitAction: consts.COMMIT_TO_CURRENT_BRANCH });
expect(vm.$el.querySelector('input[type="checkbox"]').disabled).toBe(true);
});
it('does not disable checkbox if MR does not exist', () => {
createComponent({ hasMR: false });
expect(vm.$el.querySelector('input[type="checkbox"]').disabled).toBe(false);
});
it('does not disable checkbox when creating a new branch', () => {
createComponent({ commitAction: consts.COMMIT_TO_NEW_BRANCH });
expect(vm.$el.querySelector('input[type="checkbox"]').disabled).toBe(false);
});
it('toggles off new MR when switching back to commit to current branch and MR exists', () => {
createComponent({
commitAction: consts.COMMIT_TO_NEW_BRANCH,
shouldCreateMR: true,
});
const currentBranchRadio = vm.$el.querySelector(
`input[value="${consts.COMMIT_TO_CURRENT_BRANCH}"`,
);
currentBranchRadio.click();
vm.$nextTick(() => {
expect(vm.$store.state.commit.shouldCreateMR).toBe(false);
});
});
it('toggles `shouldCreateMR` when clicking checkbox', () => {
createComponent();
const el = vm.$el.querySelector('input[type="checkbox"]');
el.dispatchEvent(new Event('change'));
expect(vm.$store.state.commit.shouldCreateMR).toBe(true);
});
});
});
import Vue from 'vue';
import store from '~/ide/stores';
import consts from '~/ide/stores/modules/commit/constants';
import NewMergeRequestOption from '~/ide/components/commit_sidebar/new_merge_request_option.vue';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { projectData } from 'spec/ide/mock_data';
import { resetStore } from 'spec/ide/helpers';
describe('create new MR checkbox', () => {
let vm;
const createComponent = ({
hasMR = false,
commitAction = consts.COMMIT_TO_NEW_BRANCH,
currentBranchId = 'master',
} = {}) => {
const Component = Vue.extend(NewMergeRequestOption);
vm = createComponentWithStore(Component, store);
vm.$store.state.currentBranchId = currentBranchId;
vm.$store.state.currentProjectId = 'abcproject';
vm.$store.state.commit.commitAction = commitAction;
Vue.set(vm.$store.state.projects, 'abcproject', { ...projectData });
if (hasMR) {
vm.$store.state.currentMergeRequestId = '1';
vm.$store.state.projects[store.state.currentProjectId].mergeRequests[
store.state.currentMergeRequestId
] = { foo: 'bar' };
}
return vm.$mount();
};
afterEach(() => {
vm.$destroy();
resetStore(vm.$store);
});
it('is hidden when an MR already exists and committing to current branch', () => {
createComponent({
hasMR: true,
commitAction: consts.COMMIT_TO_CURRENT_BRANCH,
currentBranchId: 'feature',
});
expect(vm.$el.textContent).toBe('');
});
it('does not hide checkbox if MR does not exist', () => {
createComponent({ hasMR: false });
expect(vm.$el.querySelector('input[type="checkbox"]').hidden).toBe(false);
});
it('does not hide checkbox when creating a new branch', () => {
createComponent({ commitAction: consts.COMMIT_TO_NEW_BRANCH });
expect(vm.$el.querySelector('input[type="checkbox"]').hidden).toBe(false);
});
it('dispatches toggleShouldCreateMR when clicking checkbox', () => {
createComponent();
const el = vm.$el.querySelector('input[type="checkbox"]');
spyOn(vm.$store, 'dispatch');
el.dispatchEvent(new Event('change'));
expect(vm.$store.dispatch.calls.allArgs()).toEqual(
jasmine.arrayContaining([['commit/toggleShouldCreateMR', jasmine.any(Object)]]),
);
});
});
......@@ -16,6 +16,7 @@ export const projectData = {
},
mergeRequests: {},
merge_requests_enabled: true,
default_branch: 'master',
};
export const pipelines = [
......
......@@ -180,6 +180,38 @@ describe('IDE store getters', () => {
});
});
describe('isOnDefaultBranch', () => {
it('returns false when no project exists', () => {
const localGetters = {
currentProject: undefined,
};
expect(getters.isOnDefaultBranch({}, localGetters)).toBeFalsy();
});
it("returns true when project's default branch matches current branch", () => {
const localGetters = {
currentProject: {
default_branch: 'master',
},
branchName: 'master',
};
expect(getters.isOnDefaultBranch({}, localGetters)).toBeTruthy();
});
it("returns false when project's default branch doesn't match current branch", () => {
const localGetters = {
currentProject: {
default_branch: 'master',
},
branchName: 'feature',
};
expect(getters.isOnDefaultBranch({}, localGetters)).toBeFalsy();
});
});
describe('packageJson', () => {
it('returns package.json entry', () => {
localState.entries['package.json'] = { name: 'package.json' };
......
import actions from '~/ide/stores/actions';
import rootActions from '~/ide/stores/actions';
import store from '~/ide/stores';
import service from '~/ide/services';
import router from '~/ide/ide_router';
import eventHub from '~/ide/eventhub';
import consts from '~/ide/stores/modules/commit/constants';
import * as mutationTypes from '~/ide/stores/modules/commit/mutation_types';
import * as actions from '~/ide/stores/modules/commit/actions';
import testAction from '../../../../helpers/vuex_action_helper';
import { commitActionTypes } from '~/ide/constants';
import { resetStore, file } from 'spec/ide/helpers';
......@@ -225,7 +228,7 @@ describe('IDE commit module actions', () => {
let visitUrl;
beforeEach(() => {
visitUrl = spyOnDependency(actions, 'visitUrl');
visitUrl = spyOnDependency(rootActions, 'visitUrl');
document.body.innerHTML += '<div class="flash-container"></div>';
......@@ -523,4 +526,154 @@ describe('IDE commit module actions', () => {
});
});
});
describe('toggleShouldCreateMR', () => {
it('commits both toggle and interacting with MR checkbox actions', done => {
testAction(
actions.toggleShouldCreateMR,
{},
store.state,
[
{ type: mutationTypes.TOGGLE_SHOULD_CREATE_MR },
{ type: mutationTypes.INTERACT_WITH_NEW_MR },
],
[],
done,
);
});
});
describe('setShouldCreateMR', () => {
beforeEach(() => {
store.state.projects = {
project: {
default_branch: 'master',
branches: {
master: {
name: 'master',
},
feature: {
name: 'feature',
},
},
},
};
store.state.currentProjectId = 'project';
});
it('sets to false when the current branch already has an MR', done => {
store.state.commit.currentMergeRequestId = 1;
store.state.commit.commitAction = consts.COMMIT_TO_CURRENT_BRANCH;
store.state.currentMergeRequestId = '1';
store.state.currentBranchId = 'feature';
spyOn(store, 'commit').and.callThrough();
store
.dispatch('commit/setShouldCreateMR')
.then(() => {
expect(store.commit.calls.allArgs()[0]).toEqual(
jasmine.arrayContaining([`commit/${mutationTypes.TOGGLE_SHOULD_CREATE_MR}`, false]),
);
done();
})
.catch(done.fail);
});
it('changes to false when current branch is the default branch and user has not interacted', done => {
store.state.commit.interactedWithNewMR = false;
store.state.currentBranchId = 'master';
store.state.commit.commitAction = consts.COMMIT_TO_CURRENT_BRANCH;
spyOn(store, 'commit').and.callThrough();
store
.dispatch('commit/setShouldCreateMR')
.then(() => {
expect(store.commit.calls.allArgs()[0]).toEqual(
jasmine.arrayContaining([`commit/${mutationTypes.TOGGLE_SHOULD_CREATE_MR}`, false]),
);
done();
})
.catch(done.fail);
});
it('changes to true when "create new branch" is selected and user has not interacted', done => {
store.state.commit.commitAction = consts.COMMIT_TO_NEW_BRANCH;
store.state.commit.interactedWithNewMR = false;
spyOn(store, 'commit').and.callThrough();
store
.dispatch('commit/setShouldCreateMR')
.then(() => {
expect(store.commit.calls.allArgs()[0]).toEqual(
jasmine.arrayContaining([`commit/${mutationTypes.TOGGLE_SHOULD_CREATE_MR}`, true]),
);
done();
})
.catch(done.fail);
});
it('does not change anything if user has interacted and comitting to new branch', done => {
store.state.commit.commitAction = consts.COMMIT_TO_NEW_BRANCH;
store.state.commit.interactedWithNewMR = true;
spyOn(store, 'commit').and.callThrough();
store
.dispatch('commit/setShouldCreateMR')
.then(() => {
expect(store.commit).not.toHaveBeenCalled();
done();
})
.catch(done.fail);
});
it('does not change anything if user has interacted and comitting to branch without MR', done => {
store.state.commit.commitAction = consts.COMMIT_TO_CURRENT_BRANCH;
store.state.commit.currentMergeRequestId = null;
store.state.commit.interactedWithNewMR = true;
spyOn(store, 'commit').and.callThrough();
store
.dispatch('commit/setShouldCreateMR')
.then(() => {
expect(store.commit).not.toHaveBeenCalled();
done();
})
.catch(done.fail);
});
it('still changes to false if hiding the checkbox', done => {
store.state.currentBranchId = 'feature';
store.state.commit.commitAction = consts.COMMIT_TO_CURRENT_BRANCH;
store.state.currentMergeRequestId = '1';
store.state.commit.interactedWithNewMR = true;
spyOn(store, 'commit').and.callThrough();
store
.dispatch('commit/setShouldCreateMR')
.then(() => {
expect(store.commit.calls.allArgs()[0]).toEqual(
jasmine.arrayContaining([`commit/${mutationTypes.TOGGLE_SHOULD_CREATE_MR}`, false]),
);
done();
})
.catch(done.fail);
});
it('does not change to false when on master and user has interacted even if MR exists', done => {
store.state.currentBranchId = 'master';
store.state.commit.commitAction = consts.COMMIT_TO_CURRENT_BRANCH;
store.state.currentMergeRequestId = '1';
store.state.commit.interactedWithNewMR = true;
spyOn(store, 'commit').and.callThrough();
store
.dispatch('commit/setShouldCreateMR')
.then(() => {
expect(store.commit).not.toHaveBeenCalled();
done();
})
.catch(done.fail);
});
});
});
......@@ -144,33 +144,4 @@ describe('IDE commit module getters', () => {
});
});
});
describe('shouldDisableNewMrOption', () => {
it('returns false if commitAction `COMMIT_TO_NEW_BRANCH`', () => {
state.commitAction = consts.COMMIT_TO_NEW_BRANCH;
const rootState = {
currentMergeRequest: { foo: 'bar' },
};
expect(getters.shouldDisableNewMrOption(state, null, null, rootState)).toBeFalsy();
});
it('returns false if there is no current merge request', () => {
state.commitAction = consts.COMMIT_TO_CURRENT_BRANCH;
const rootState = {
currentMergeRequest: null,
};
expect(getters.shouldDisableNewMrOption(state, null, null, rootState)).toBeFalsy();
});
it('returns true an MR exists and commit action is `COMMIT_TO_CURRENT_BRANCH`', () => {
state.commitAction = consts.COMMIT_TO_CURRENT_BRANCH;
const rootState = {
currentMergeRequest: { foo: 'bar' },
};
expect(getters.shouldDisableNewMrOption(state, null, null, rootState)).toBeTruthy();
});
});
});
......@@ -38,6 +38,7 @@ describe('Dashboard', () => {
let DashboardComponent;
let mock;
let store;
let component;
beforeEach(() => {
setFixtures(`
......@@ -59,12 +60,13 @@ describe('Dashboard', () => {
});
afterEach(() => {
component.$destroy();
mock.restore();
});
describe('no metrics are available yet', () => {
it('shows a getting started empty state when no metrics are present', () => {
const component = new DashboardComponent({
component = new DashboardComponent({
el: document.querySelector('.prometheus-graphs'),
propsData: { ...propsData, showTimeWindowDropdown: false },
store,
......@@ -81,7 +83,7 @@ describe('Dashboard', () => {
});
it('shows up a loading state', done => {
const component = new DashboardComponent({
component = new DashboardComponent({
el: document.querySelector('.prometheus-graphs'),
propsData: { ...propsData, hasMetrics: true, showTimeWindowDropdown: false },
store,
......@@ -94,7 +96,7 @@ describe('Dashboard', () => {
});
it('hides the legend when showLegend is false', done => {
const component = new DashboardComponent({
component = new DashboardComponent({
el: document.querySelector('.prometheus-graphs'),
propsData: {
...propsData,
......@@ -114,7 +116,7 @@ describe('Dashboard', () => {
});
it('hides the group panels when showPanels is false', done => {
const component = new DashboardComponent({
component = new DashboardComponent({
el: document.querySelector('.prometheus-graphs'),
propsData: {
...propsData,
......@@ -134,7 +136,7 @@ describe('Dashboard', () => {
});
it('renders the environments dropdown with a number of environments', done => {
const component = new DashboardComponent({
component = new DashboardComponent({
el: document.querySelector('.prometheus-graphs'),
propsData: {
...propsData,
......@@ -165,7 +167,7 @@ describe('Dashboard', () => {
});
it('hides the environments dropdown list when there is no environments', done => {
const component = new DashboardComponent({
component = new DashboardComponent({
el: document.querySelector('.prometheus-graphs'),
propsData: {
...propsData,
......@@ -195,7 +197,7 @@ describe('Dashboard', () => {
});
it('renders the environments dropdown with a single active element', done => {
const component = new DashboardComponent({
component = new DashboardComponent({
el: document.querySelector('.prometheus-graphs'),
propsData: {
...propsData,
......@@ -228,7 +230,7 @@ describe('Dashboard', () => {
});
it('hides the dropdown', done => {
const component = new DashboardComponent({
component = new DashboardComponent({
el: document.querySelector('.prometheus-graphs'),
propsData: {
...propsData,
......@@ -249,7 +251,7 @@ describe('Dashboard', () => {
});
it('does not show the time window dropdown when the feature flag is not set', done => {
const component = new DashboardComponent({
component = new DashboardComponent({
el: document.querySelector('.prometheus-graphs'),
propsData: {
...propsData,
......@@ -270,7 +272,7 @@ describe('Dashboard', () => {
});
it('renders the time window dropdown with a set of options', done => {
const component = new DashboardComponent({
component = new DashboardComponent({
el: document.querySelector('.prometheus-graphs'),
propsData: {
...propsData,
......@@ -295,10 +297,46 @@ describe('Dashboard', () => {
});
});
it('fetches the metrics data with proper time window', done => {
component = new DashboardComponent({
el: document.querySelector('.prometheus-graphs'),
propsData: {
...propsData,
hasMetrics: true,
showPanels: false,
showTimeWindowDropdown: true,
},
store,
});
spyOn(component.$store, 'dispatch').and.stub();
const getTimeDiffSpy = spyOnDependency(Dashboard, 'getTimeDiff');
component.$store.commit(
`monitoringDashboard/${types.SET_ENVIRONMENTS_ENDPOINT}`,
'/environments',
);
component.$store.commit(
`monitoringDashboard/${types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS}`,
environmentData,
);
component.$mount();
Vue.nextTick()
.then(() => {
expect(component.$store.dispatch).toHaveBeenCalled();
expect(getTimeDiffSpy).toHaveBeenCalledWith(component.selectedTimeWindow);
done();
})
.catch(done.fail);
});
it('shows a specific time window selected from the url params', done => {
spyOnDependency(Dashboard, 'getParameterValues').and.returnValue(['thirtyMinutes']);
const component = new DashboardComponent({
component = new DashboardComponent({
el: document.querySelector('.prometheus-graphs'),
propsData: { ...propsData, hasMetrics: true, showTimeWindowDropdown: true },
store,
......@@ -319,7 +357,7 @@ describe('Dashboard', () => {
'<script>alert("XSS")</script>',
]);
const component = new DashboardComponent({
component = new DashboardComponent({
el: document.querySelector('.prometheus-graphs'),
propsData: { ...propsData, hasMetrics: true, showTimeWindowDropdown: true },
store,
......@@ -344,7 +382,7 @@ describe('Dashboard', () => {
});
it('sets elWidth to page width when the sidebar is resized', done => {
const component = new DashboardComponent({
component = new DashboardComponent({
el: document.querySelector('.prometheus-graphs'),
propsData: {
...propsData,
......@@ -374,16 +412,10 @@ describe('Dashboard', () => {
});
describe('external dashboard link', () => {
let component;
beforeEach(() => {
mock.onGet(mockApiEndpoint).reply(200, metricsGroupsAPIResponse);
});
afterEach(() => {
component.$destroy();
});
describe('with feature flag enabled', () => {
beforeEach(() => {
component = new DashboardComponent({
......
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