Commit 42b933ef authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent 3d67f14e
...@@ -8,7 +8,6 @@ globals: ...@@ -8,7 +8,6 @@ globals:
IS_EE: false IS_EE: false
plugins: plugins:
- import - import
- html
- "@gitlab/i18n" - "@gitlab/i18n"
- "@gitlab/vue-i18n" - "@gitlab/vue-i18n"
settings: settings:
......
<script>
import DropdownSearchInput from '~/vue_shared/components/dropdown/dropdown_search_input.vue';
import DropdownHiddenInput from '~/vue_shared/components/dropdown/dropdown_hidden_input.vue';
import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.vue';
export default {
components: {
DropdownButton,
DropdownSearchInput,
DropdownHiddenInput,
},
props: {
fieldName: {
type: String,
required: false,
default: '',
},
placeholder: {
type: String,
required: false,
default: '',
},
defaultValue: {
type: String,
required: false,
default: '',
},
value: {
type: Object,
required: false,
default: () => null,
},
labelProperty: {
type: String,
required: false,
default: 'name',
},
valueProperty: {
type: String,
required: false,
default: 'value',
},
items: {
type: Array,
required: false,
default: () => [],
},
loading: {
type: Boolean,
required: false,
default: false,
},
disabled: {
type: Boolean,
required: false,
default: false,
},
loadingText: {
type: String,
required: false,
default: '',
},
disabledText: {
type: String,
required: false,
default: '',
},
hasErrors: {
type: Boolean,
required: false,
default: false,
},
errorMessage: {
type: String,
required: false,
default: '',
},
searchFieldPlaceholder: {
type: String,
required: false,
default: '',
},
emptyText: {
type: String,
required: false,
default: '',
},
searchFn: {
type: Function,
required: false,
default: searchQuery => item => item.name.toLowerCase().indexOf(searchQuery) > -1,
},
},
data() {
return {
searchQuery: '',
selectedItem: null,
};
},
computed: {
toggleText() {
if (this.loading && this.loadingText) {
return this.loadingText;
}
if (this.disabled && this.disabledText) {
return this.disabledText;
}
if (!this.selectedItem) {
return this.placeholder;
}
return this.selectedItemLabel;
},
results() {
if (!this.items) {
return [];
}
return this.items.filter(this.searchFn(this.searchQuery));
},
selectedItemLabel() {
return this.selectedItem && this.selectedItem[this.labelProperty];
},
selectedItemValue() {
return (this.selectedItem && this.selectedItem[this.valueProperty]) || '';
},
},
methods: {
select(item) {
this.selectedItem = item;
this.$emit('input', item);
},
},
};
</script>
<template>
<div>
<div class="js-gcp-machine-type-dropdown dropdown">
<dropdown-hidden-input :name="fieldName" :value="selectedItemValue" />
<dropdown-button
:class="{ 'border-danger': hasErrors }"
:is-disabled="disabled"
:is-loading="loading"
:toggle-text="toggleText"
/>
<div class="dropdown-menu dropdown-select">
<dropdown-search-input v-model="searchQuery" :placeholder-text="searchFieldPlaceholder" />
<div class="dropdown-content">
<ul>
<li v-if="!results.length">
<span class="js-empty-text menu-item">
{{ emptyText }}
</span>
</li>
<li v-for="item in results" :key="item.id">
<button class="js-dropdown-item" type="button" @click.prevent="select(item)">
<slot name="item" :item="item">
{{ item.name }}
</slot>
</button>
</li>
</ul>
</div>
</div>
</div>
<span
v-if="hasErrors && errorMessage"
:class="[
'form-text js-eks-dropdown-error-message',
{
'text-danger': hasErrors,
'text-muted': !hasErrors,
},
]"
>
{{ errorMessage }}
</span>
</div>
</template>
...@@ -14,5 +14,12 @@ export default { ...@@ -14,5 +14,12 @@ export default {
}; };
</script> </script>
<template> <template>
<form name="eks-cluster-configuration-form"></form> <form name="eks-cluster-configuration-form">
<div class="form-group">
<label class="label-bold" name="role" for="eks-role">
{{ s__('ClusterIntegration|Role name') }}
</label>
<role-name-dropdown />
</div>
</form>
</template> </template>
<script>
import { sprintf, s__ } from '~/locale';
import ClusterFormDropdown from './cluster_form_dropdown.vue';
export default {
components: {
ClusterFormDropdown,
},
props: {
roles: {
type: Array,
required: false,
default: () => [],
},
loading: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
helpText() {
return sprintf(
s__(
'ClusterIntegration|Select the IAM Role to allow Amazon EKS and the Kubernetes control plane to manage AWS resources on your behalf. To use a new role name, first create one on %{startLink}Amazon Web Services%{endLink}.',
),
{
startLink:
'<a href="https://console.aws.amazon.com/iam/home?#roles" target="_blank" rel="noopener noreferrer">',
endLink: '</a>',
},
false,
);
},
},
};
</script>
<template>
<div>
<cluster-form-dropdown
field-id="eks-role-name"
field-name="eks-role-name"
:items="roles"
:loading="loading"
:loading-text="s__('ClusterIntegration|Loading IAM Roles')"
:placeholder="s__('ClusterIntergation|Select role name')"
:search-field-placeholder="s__('ClusterIntegration|Search IAM Roles')"
:empty-text="s__('ClusterIntegration|No IAM Roles found')"
/>
<p class="form-text text-muted" v-html="helpText"></p>
</div>
</template>
...@@ -49,12 +49,12 @@ ...@@ -49,12 +49,12 @@
= render_if_exists 'projects/mirrors/table_pull_row' = render_if_exists 'projects/mirrors/table_pull_row'
- @project.remote_mirrors.each_with_index do |mirror, index| - @project.remote_mirrors.each_with_index do |mirror, index|
- next if mirror.new_record? - next if mirror.new_record?
%tr.qa-mirrored-repository-row.rspec-mirrored-repository-row{ class: ('bg-secondary' if mirror.disabled?) } %tr.rspec-mirrored-repository-row{ class: ('bg-secondary' if mirror.disabled?), data: { qa_selector: 'mirrored_repository_row' } }
%td.qa-mirror-repository-url= mirror.safe_url || _('Invalid URL') %td{ data: { qa_selector: 'mirror_repository_url_cell' } }= mirror.safe_url || _('Invalid URL')
%td= _('Push') %td= _('Push')
%td %td
= mirror.last_update_started_at.present? ? time_ago_with_tooltip(mirror.last_update_started_at) : _('Never') = mirror.last_update_started_at.present? ? time_ago_with_tooltip(mirror.last_update_started_at) : _('Never')
%td.qa-mirror-last-update-at= mirror.last_update_at.present? ? time_ago_with_tooltip(mirror.last_update_at) : _('Never') %td{ data: { qa_selector: 'mirror_last_update_at_cell' } }= mirror.last_update_at.present? ? time_ago_with_tooltip(mirror.last_update_at) : _('Never')
%td %td
- if mirror.disabled? - if mirror.disabled?
= render 'projects/mirrors/disabled_mirror_badge' = render 'projects/mirrors/disabled_mirror_badge'
......
---
title: Create a project for self-monitoring the GitLab instance
merge_request: 31389
author:
type: added
# frozen_string_literal: true
::Gitlab::DatabaseImporters::SelfMonitoring::Project::CreateService.new.execute!
# frozen_string_literal: true
::Gitlab::DatabaseImporters::SelfMonitoring::Project::CreateService.new.execute!
# frozen_string_literal: true
class AddGitlabInstanceAdministrationProject < ActiveRecord::Migration[5.2]
DOWNTIME = false
disable_ddl_transaction!
def up
BackgroundMigrationWorker.perform_async('AddGitlabInstanceAdministrationProject', [])
end
def down
ApplicationSetting.current_without_cache
&.instance_administration_project
&.owner
&.destroy!
end
end
...@@ -174,7 +174,7 @@ If we take a deeper look into the basic workflow, we can see ...@@ -174,7 +174,7 @@ If we take a deeper look into the basic workflow, we can see
the features available in GitLab at each stage of the DevOps the features available in GitLab at each stage of the DevOps
lifecycle, as shown on the illustration below. lifecycle, as shown on the illustration below.
![Deeper look into the basic CI/CD workflow](img/gitlab_workflow_example_extended_11_11.png) ![Deeper look into the basic CI/CD workflow](img/gitlab_workflow_example_extended_v12_3.png)
If you look at the image from the left to the right, If you look at the image from the left to the right,
you'll see some of the features available in GitLab you'll see some of the features available in GitLab
......
...@@ -3514,6 +3514,9 @@ msgstr "" ...@@ -3514,6 +3514,9 @@ msgstr ""
msgid "ClusterIntegration|Let's Encrypt" msgid "ClusterIntegration|Let's Encrypt"
msgstr "" msgstr ""
msgid "ClusterIntegration|Loading IAM Roles"
msgstr ""
msgid "ClusterIntegration|Machine type" msgid "ClusterIntegration|Machine type"
msgstr "" msgstr ""
...@@ -3523,6 +3526,9 @@ msgstr "" ...@@ -3523,6 +3526,9 @@ msgstr ""
msgid "ClusterIntegration|Manage your Kubernetes cluster by visiting %{link_gke}" msgid "ClusterIntegration|Manage your Kubernetes cluster by visiting %{link_gke}"
msgstr "" msgstr ""
msgid "ClusterIntegration|No IAM Roles found"
msgstr ""
msgid "ClusterIntegration|No machine types matched your search" msgid "ClusterIntegration|No machine types matched your search"
msgstr "" msgstr ""
...@@ -3589,9 +3595,15 @@ msgstr "" ...@@ -3589,9 +3595,15 @@ msgstr ""
msgid "ClusterIntegration|Request to begin uninstalling failed" msgid "ClusterIntegration|Request to begin uninstalling failed"
msgstr "" msgstr ""
msgid "ClusterIntegration|Role name"
msgstr ""
msgid "ClusterIntegration|Save changes" msgid "ClusterIntegration|Save changes"
msgstr "" msgstr ""
msgid "ClusterIntegration|Search IAM Roles"
msgstr ""
msgid "ClusterIntegration|Search machine types" msgid "ClusterIntegration|Search machine types"
msgstr "" msgstr ""
...@@ -3616,6 +3628,9 @@ msgstr "" ...@@ -3616,6 +3628,9 @@ msgstr ""
msgid "ClusterIntegration|Select project to choose zone" msgid "ClusterIntegration|Select project to choose zone"
msgstr "" msgstr ""
msgid "ClusterIntegration|Select the IAM Role to allow Amazon EKS and the Kubernetes control plane to manage AWS resources on your behalf. To use a new role name, first create one on %{startLink}Amazon Web Services%{endLink}."
msgstr ""
msgid "ClusterIntegration|Select zone" msgid "ClusterIntegration|Select zone"
msgstr "" msgstr ""
...@@ -3739,6 +3754,9 @@ msgstr "" ...@@ -3739,6 +3754,9 @@ msgstr ""
msgid "ClusterIntegration|sign up" msgid "ClusterIntegration|sign up"
msgstr "" msgstr ""
msgid "ClusterIntergation|Select role name"
msgstr ""
msgid "Code" msgid "Code"
msgstr "" msgstr ""
......
...@@ -13,8 +13,8 @@ module QA ...@@ -13,8 +13,8 @@ module QA
view 'app/views/projects/mirrors/_mirror_repos.html.haml' do view 'app/views/projects/mirrors/_mirror_repos.html.haml' do
element :mirror_repository_url_input element :mirror_repository_url_input
element :mirror_repository_button element :mirror_repository_button
element :mirror_repository_url element :mirror_repository_url_cell
element :mirror_last_update_at element :mirror_last_update_at_cell
element :mirrored_repository_row element :mirrored_repository_row
end end
...@@ -64,21 +64,21 @@ module QA ...@@ -64,21 +64,21 @@ module QA
wait(interval: 1) do wait(interval: 1) do
within_element_by_index(:mirrored_repository_row, row_index) do within_element_by_index(:mirrored_repository_row, row_index) do
last_update = find_element(:mirror_last_update_at, wait: 0) last_update = find_element(:mirror_last_update_at_cell, wait: 0)
last_update.has_text?('just now') || last_update.has_text?('seconds') last_update.has_text?('just now') || last_update.has_text?('seconds')
end end
end end
# Fail early if the page still shows that there has been no update # Fail early if the page still shows that there has been no update
within_element_by_index(:mirrored_repository_row, row_index) do within_element_by_index(:mirrored_repository_row, row_index) do
find_element(:mirror_last_update_at, wait: 0).assert_no_text('Never') find_element(:mirror_last_update_at_cell, wait: 0).assert_no_text('Never')
end end
end end
private private
def find_repository_row_index(target_url) def find_repository_row_index(target_url)
all_elements(:mirror_repository_url).index do |url| all_elements(:mirror_repository_url_cell).index do |url|
# The url might be a sanitized url but the target_url won't be so # The url might be a sanitized url but the target_url won't be so
# we compare just the paths instead of the full url # we compare just the paths instead of the full url
URI.parse(url.text).path == target_url.path URI.parse(url.text).path == target_url.path
......
...@@ -4,11 +4,12 @@ module QA ...@@ -4,11 +4,12 @@ module QA
module Resource module Resource
module Repository module Repository
class ProjectPush < Repository::Push class ProjectPush < Repository::Push
attr_accessor :project_name
attr_writer :wait_for_push attr_writer :wait_for_push
attribute :project do attribute :project do
Project.fabricate! do |resource| Project.fabricate! do |resource|
resource.name = 'project-with-code' resource.name = project_name
resource.description = 'Project with repository' resource.description = 'Project with repository'
end end
end end
...@@ -19,6 +20,7 @@ module QA ...@@ -19,6 +20,7 @@ module QA
@commit_message = "This is a test commit" @commit_message = "This is a test commit"
@branch_name = 'master' @branch_name = 'master'
@new_branch = true @new_branch = true
@project_name = 'project-with-code'
@wait_for_push = true @wait_for_push = true
end end
......
import { shallowMount } from '@vue/test-utils';
import ClusterFormDropdown from '~/create_cluster/eks_cluster/components/cluster_form_dropdown.vue';
import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.vue';
import DropdownSearchInput from '~/vue_shared/components/dropdown/dropdown_search_input.vue';
import DropdownHiddenInput from '~/vue_shared/components/dropdown/dropdown_hidden_input.vue';
describe('ClusterFormDropdown', () => {
let vm;
beforeEach(() => {
vm = shallowMount(ClusterFormDropdown);
});
afterEach(() => vm.destroy());
describe('when no item is selected', () => {
it('displays placeholder text', () => {
const placeholder = 'placeholder';
vm.setProps({ placeholder });
expect(vm.find(DropdownButton).props('toggleText')).toEqual(placeholder);
});
});
describe('when an item is selected', () => {
const selectedItem = { name: 'Name', value: 'value' };
beforeEach(() => {
vm.setData({ selectedItem });
});
it('displays selected item label', () => {
expect(vm.find(DropdownButton).props('toggleText')).toEqual(selectedItem.name);
});
it('sets selected value to dropdown hidden input', () => {
expect(vm.find(DropdownHiddenInput).props('value')).toEqual(selectedItem.value);
});
});
describe('when an item is selected and has a custom label property', () => {
it('displays selected item custom label', () => {
const labelProperty = 'customLabel';
const selectedItem = { [labelProperty]: 'Name' };
vm.setProps({ labelProperty });
vm.setData({ selectedItem });
expect(vm.find(DropdownButton).props('toggleText')).toEqual(selectedItem[labelProperty]);
});
});
describe('when loading', () => {
it('dropdown button isLoading', () => {
vm.setProps({ loading: true });
expect(vm.find(DropdownButton).props('isLoading')).toBe(true);
});
});
describe('when loading and loadingText is provided', () => {
it('uses loading text as toggle button text', () => {
const loadingText = 'loading text';
vm.setProps({ loading: true, loadingText });
expect(vm.find(DropdownButton).props('toggleText')).toEqual(loadingText);
});
});
describe('when disabled', () => {
it('dropdown button isDisabled', () => {
vm.setProps({ disabled: true });
expect(vm.find(DropdownButton).props('isDisabled')).toBe(true);
});
});
describe('when disabled and disabledText is provided', () => {
it('uses disabled text as toggle button text', () => {
const disabledText = 'disabled text';
vm.setProps({ disabled: true, disabledText });
expect(vm.find(DropdownButton).props('toggleText')).toBe(disabledText);
});
});
describe('when has errors', () => {
it('sets border-danger class selector to dropdown toggle', () => {
vm.setProps({ hasErrors: true });
expect(vm.find(DropdownButton).classes('border-danger')).toBe(true);
});
});
describe('when has errors and an error message', () => {
it('displays error message', () => {
const errorMessage = 'error message';
vm.setProps({ hasErrors: true, errorMessage });
expect(vm.find('.js-eks-dropdown-error-message').text()).toEqual(errorMessage);
});
});
describe('when no results are available', () => {
it('displays empty text', () => {
const emptyText = 'error message';
vm.setProps({ items: [], emptyText });
expect(vm.find('.js-empty-text').text()).toEqual(emptyText);
});
});
it('displays search field placeholder', () => {
const searchFieldPlaceholder = 'Placeholder';
vm.setProps({ searchFieldPlaceholder });
expect(vm.find(DropdownSearchInput).props('placeholderText')).toEqual(searchFieldPlaceholder);
});
it('it filters results by search query', () => {
const secondItem = { name: 'second item' };
const items = [{ name: 'first item' }, secondItem];
const searchQuery = 'second';
vm.setProps({ items });
vm.setData({ searchQuery });
expect(vm.findAll('.js-dropdown-item').length).toEqual(1);
expect(vm.find('.js-dropdown-item').text()).toEqual(secondItem.name);
});
});
import { shallowMount } from '@vue/test-utils';
import ClusterFormDropdown from '~/create_cluster/eks_cluster/components/cluster_form_dropdown.vue';
import RoleNameDropdown from '~/create_cluster/eks_cluster/components/role_name_dropdown.vue';
describe('RoleNameDropdown', () => {
let vm;
beforeEach(() => {
vm = shallowMount(RoleNameDropdown);
});
afterEach(() => vm.destroy());
it('renders a cluster-form-dropdown', () => {
expect(vm.find(ClusterFormDropdown).exists()).toBe(true);
});
it('sets roles to cluster-form-dropdown items property', () => {
const roles = [{ name: 'basic' }];
vm.setProps({ roles });
expect(vm.find(ClusterFormDropdown).props('items')).toEqual(roles);
});
it('sets a loading text', () => {
expect(vm.find(ClusterFormDropdown).props('loadingText')).toEqual('Loading IAM Roles');
});
it('sets a placeholder', () => {
expect(vm.find(ClusterFormDropdown).props('placeholder')).toEqual('Select role name');
});
it('sets an empty results text', () => {
expect(vm.find(ClusterFormDropdown).props('emptyText')).toEqual('No IAM Roles found');
});
it('sets a search field placeholder', () => {
expect(vm.find(ClusterFormDropdown).props('searchFieldPlaceholder')).toEqual(
'Search IAM Roles',
);
});
});
# frozen_string_literal: true
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20190801072937_add_gitlab_instance_administration_project.rb')
describe AddGitlabInstanceAdministrationProject, :migration do
let(:application_settings) { table(:application_settings) }
let(:users) { table(:users) }
let(:prometheus_settings) do
{
enable: true,
listen_address: 'localhost:9090'
}
end
before do
stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
stub_config(prometheus: prometheus_settings)
end
describe 'down' do
let!(:application_setting) { application_settings.create! }
let!(:user) { users.create!(admin: true, email: 'admin1@example.com', projects_limit: 10, state: :active) }
it 'deletes group and project' do
migrate!
expect(Project.count).to eq(1)
expect(Group.count).to eq(1)
schema_migrate_down!
expect(Project.count).to eq(0)
expect(Group.count).to eq(0)
end
end
end
...@@ -4630,13 +4630,6 @@ eslint-plugin-filenames@^1.3.2: ...@@ -4630,13 +4630,6 @@ eslint-plugin-filenames@^1.3.2:
lodash.snakecase "4.1.1" lodash.snakecase "4.1.1"
lodash.upperfirst "4.3.1" lodash.upperfirst "4.3.1"
eslint-plugin-html@5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-html/-/eslint-plugin-html-5.0.0.tgz#396e30a60dedee0122fe08f11d13c5ab22f20d32"
integrity sha512-f7p/7YQdgQUFVAX3nB4dnMQbrDeTalcA01PDhuvTLk0ZadCwM4Pb+639SRuqEf1zMkIxckLY+ckCr0hVP5zl6A==
dependencies:
htmlparser2 "^3.10.0"
eslint-plugin-import@^2.14.0, eslint-plugin-import@^2.16.0: eslint-plugin-import@^2.14.0, eslint-plugin-import@^2.16.0:
version "2.16.0" version "2.16.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.16.0.tgz#97ac3e75d0791c4fac0e15ef388510217be7f66f" resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.16.0.tgz#97ac3e75d0791c4fac0e15ef388510217be7f66f"
......
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