Commit 00124398 authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent 83d8c1d6
......@@ -8,6 +8,7 @@ import {
GlDropdownItem,
GlFormGroup,
GlModal,
GlSearchBoxByType,
GlModalDirective,
GlTooltipDirective,
} from '@gitlab/ui';
......@@ -15,6 +16,7 @@ import PanelType from 'ee_else_ce/monitoring/components/panel_type.vue';
import { s__ } from '~/locale';
import createFlash from '~/flash';
import Icon from '~/vue_shared/components/icon.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { getParameterValues, mergeUrlParams, redirectTo } from '~/lib/utils/url_utility';
import invalidUrl from '~/lib/utils/invalid_url';
......@@ -38,6 +40,7 @@ export default {
GlButton,
GlDropdown,
GlDropdownItem,
GlSearchBoxByType,
GlFormGroup,
GlModal,
......@@ -52,6 +55,7 @@ export default {
GlTooltip: GlTooltipDirective,
TrackEvent: TrackEventDirective,
},
mixins: [glFeatureFlagsMixin()],
props: {
externalDashboardUrl: {
type: String,
......@@ -198,13 +202,12 @@ export default {
'dashboard',
'emptyState',
'showEmptyState',
'environments',
'deploymentData',
'useDashboardEndpoint',
'allDashboards',
'additionalPanelTypesEnabled',
]),
...mapGetters('monitoringDashboard', ['getMetricStates']),
...mapGetters('monitoringDashboard', ['getMetricStates', 'filteredEnvironments']),
firstDashboard() {
return this.environmentsEndpoint.length > 0 && this.allDashboards.length > 0
? this.allDashboards[0]
......@@ -227,6 +230,9 @@ export default {
this.externalDashboardUrl.length
);
},
shouldRenderSearchableEnvironmentsDropdown() {
return this.glFeatures.searchableEnvironmentsDropdown;
},
},
created() {
this.setEndpoints({
......@@ -255,6 +261,7 @@ export default {
'setGettingStartedEmptyState',
'setEndpoints',
'setPanelGroupMetrics',
'setEnvironmentsSearchTerm',
]),
updatePanels(key, panels) {
this.setPanelGroupMetrics({
......@@ -296,6 +303,9 @@ export default {
setFormValidity(isValid) {
this.formIsValid = isValid;
},
debouncedEnvironmentsSearch: _.debounce(function environmentsSearchOnInput(searchTerm) {
this.setEnvironmentsSearchTerm(searchTerm);
}, 500),
submitCustomMetricsForm() {
this.$refs.customMetricsForm.submit();
},
......@@ -374,17 +384,36 @@ export default {
data-qa-selector="environments_dropdown"
class="mb-0 d-flex"
toggle-class="dropdown-menu-toggle"
menu-class="monitor-environment-dropdown-menu"
:text="currentEnvironmentName"
:disabled="environments.length === 0"
:disabled="filteredEnvironments.length === 0"
>
<gl-dropdown-item
v-for="environment in environments"
:key="environment.id"
:active="environment.name === currentEnvironmentName"
active-class="is-active"
:href="environment.metrics_path"
>{{ environment.name }}</gl-dropdown-item
>
<div class="d-flex flex-column overflow-hidden">
<gl-search-box-by-type
v-if="shouldRenderSearchableEnvironmentsDropdown"
ref="monitorEnvironmentsDropdownSearch"
class="m-2"
@input="debouncedEnvironmentsSearch"
/>
<div class="flex-fill overflow-auto">
<gl-dropdown-item
v-for="environment in filteredEnvironments"
:key="environment.id"
:active="environment.name === currentEnvironmentName"
active-class="is-active"
:href="environment.metrics_path"
>{{ environment.name }}</gl-dropdown-item
>
</div>
<div
v-if="shouldRenderSearchableEnvironmentsDropdown"
v-show="filteredEnvironments.length === 0"
ref="monitorEnvironmentsDropdownMsg"
class="text-secondary no-matches-message"
>
{{ s__('No matching results') }}
</div>
</div>
</gl-dropdown>
</gl-form-group>
......@@ -415,18 +444,16 @@ export default {
variant="default"
class="mr-2 mt-1 js-rearrange-button"
@click="toggleRearrangingPanels"
>{{ __('Arrange charts') }}</gl-button
>
{{ __('Arrange charts') }}
</gl-button>
<gl-button
v-if="addingMetricsAvailable"
ref="addMetricBtn"
v-gl-modal="$options.addMetric.modalId"
variant="outline-success"
class="mr-2 mt-1"
>{{ $options.addMetric.title }}</gl-button
>
{{ $options.addMetric.title }}
</gl-button>
<gl-modal
v-if="addingMetricsAvailable"
ref="addMetricModal"
......@@ -448,9 +475,8 @@ export default {
:disabled="!formIsValid"
variant="success"
@click="submitCustomMetricsForm"
>{{ __('Save changes') }}</gl-button
>
{{ __('Save changes') }}
</gl-button>
</div>
</gl-modal>
......@@ -458,9 +484,8 @@ export default {
v-if="selectedDashboard.can_edit"
class="mt-1 js-edit-link"
:href="selectedDashboard.project_blob_path"
>{{ __('Edit dashboard') }}</gl-button
>
{{ __('Edit dashboard') }}
</gl-button>
<gl-button
v-if="externalDashboardUrl.length"
......@@ -506,9 +531,9 @@ export default {
class="draggable-remove js-draggable-remove p-2 w-100 position-absolute d-flex justify-content-end"
@click="removePanel(groupData.key, groupData.panels, graphIndex)"
>
<a class="mx-2 p-2 draggable-remove-link" :aria-label="__('Remove')"
><icon name="close"
/></a>
<a class="mx-2 p-2 draggable-remove-link" :aria-label="__('Remove')">
<icon name="close" />
</a>
</div>
<panel-type
......
......@@ -30,6 +30,10 @@ export const setEndpoints = ({ commit }, endpoints) => {
commit(types.SET_ENDPOINTS, endpoints);
};
export const setEnvironmentsSearchTerm = ({ commit }, searchTerm) => {
commit(types.SET_ENVIRONMENTS_SEARCH_TERM, searchTerm);
};
export const setShowErrorBanner = ({ commit }, enabled) => {
commit(types.SET_SHOW_ERROR_BANNER, enabled);
};
......
......@@ -58,5 +58,20 @@ export const metricsWithData = state => groupKey => {
return res;
};
/**
* Filter environments by names.
*
* This is used in the environments dropdown with searchable input.
* Also, this searchable dropdown is behind `searchable_environments_dropdown`
* feature flag
*
* @param {Object} state
* @returns {Array} List of environments
*/
export const filteredEnvironments = state =>
state.environments.filter(env =>
env.name.toLowerCase().includes((state.environmentsSearchTerm || '').trim().toLowerCase()),
);
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
......@@ -21,3 +21,5 @@ export const SET_GETTING_STARTED_EMPTY_STATE = 'SET_GETTING_STARTED_EMPTY_STATE'
export const SET_NO_DATA_EMPTY_STATE = 'SET_NO_DATA_EMPTY_STATE';
export const SET_SHOW_ERROR_BANNER = 'SET_SHOW_ERROR_BANNER';
export const SET_PANEL_GROUP_METRICS = 'SET_PANEL_GROUP_METRICS';
export const SET_ENVIRONMENTS_SEARCH_TERM = 'SET_ENVIRONMENTS_SEARCH_TERM';
......@@ -196,4 +196,7 @@ export default {
const panelGroup = state.dashboard.panel_groups.find(pg => payload.key === pg.key);
panelGroup.panels = payload.panels;
},
[types.SET_ENVIRONMENTS_SEARCH_TERM](state, searchTerm) {
state.environmentsSearchTerm = searchTerm;
},
};
......@@ -15,6 +15,7 @@ export default () => ({
deploymentData: [],
environments: [],
environmentsSearchTerm: '',
allDashboards: [],
currentDashboard: null,
projectPath: null,
......
......@@ -12,6 +12,7 @@
max-width: 900px;
&.navless-container {
padding: 35px $gl-padding;
// overriding .devise-layout-html.navless-container to support the sticky footer
// without having a header on size xs
@include media-breakpoint-down(xs) {
......@@ -24,6 +25,7 @@
.signup-heading h2 {
font-weight: $gl-font-weight-bold;
padding: 0 $gl-padding;
font-size: $gl-font-size-28;
@include media-breakpoint-down(md) {
font-size: $gl-font-size-large;
......@@ -49,4 +51,35 @@
color: $red-700;
}
}
.omniauth-divider {
&::before,
&::after {
content: '';
flex: 1;
border-bottom: 1px solid $gray-dark;
margin: $gl-padding-24 0;
}
&::before {
margin-right: $gl-padding;
}
&::after {
margin-left: $gl-padding;
}
}
.omniauth-btn {
width: 48%;
@include media-breakpoint-down(md) {
width: 100%;
}
img {
width: $default-icon-size;
height: $default-icon-size;
}
}
}
......@@ -58,6 +58,18 @@
.custom-time-range-form-group > label {
padding-bottom: $gl-padding;
}
.monitor-environment-dropdown-menu {
&.show {
display: flex;
flex-direction: column;
overflow: hidden;
}
.no-matches-message {
padding: $gl-padding-8 $gl-padding-12;
}
}
}
.prometheus-panel {
......
......@@ -14,6 +14,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
before_action :expire_etag_cache, only: [:index], unless: -> { request.format.json? }
before_action only: [:metrics, :additional_metrics, :metrics_dashboard] do
push_frontend_feature_flag(:prometheus_computed_alerts)
push_frontend_feature_flag(:searchable_environments_dropdown)
end
before_action do
push_frontend_feature_flag(:auto_stop_environments)
......
......@@ -89,7 +89,17 @@ module AuthHelper
def enabled_button_based_providers
disabled_providers = Gitlab::CurrentSettings.disabled_oauth_sign_in_sources || []
button_based_providers.map(&:to_s) - disabled_providers
providers = button_based_providers.map(&:to_s) - disabled_providers
providers.sort_by do |provider|
case provider
when 'google_oauth2'
0
when 'github'
1
else
2
end
end
end
def button_based_providers_enabled?
......
......@@ -394,6 +394,11 @@ class User < ApplicationRecord
Gitlab::CurrentSettings.minimum_password_length..Devise.password_length.max
end
# Generate a random password that conforms to the current password length settings
def random_password
Devise.friendly_token(password_length.max)
end
# Devise method overridden to allow sign in with email or username
def find_for_database_authentication(warden_conditions)
conditions = warden_conditions.dup
......
......@@ -38,13 +38,9 @@ module Metrics
# Determines whether users should be able to view
# dashboards at all.
def allowed?
if params[:environment]
Ability.allowed?(current_user, :read_environment, project)
elsif params[:cluster]
true # Authorization handled at controller level
else
false
end
return false unless params[:environment]
Ability.allowed?(current_user, :read_environment, project)
end
# Returns a new dashboard Hash, supplemented with DB info
......
......@@ -23,7 +23,7 @@ module Users
@reset_token = user.generate_reset_token if params[:reset_password]
if user_params[:force_random_password]
random_password = Devise.friendly_token.first(User.password_length.min)
random_password = User.random_password
user.password = user.password_confirmation = random_password
end
end
......
......@@ -41,3 +41,5 @@
= recaptcha_tags
.submit-container.mt-3
= f.submit _("Register"), class: "btn-register btn btn-block btn-success mb-0 p-2", data: { qa_selector: 'new_user_register_button' }
- if omniauth_enabled? && button_based_providers_enabled?
= render 'devise/shared/experimental_separate_sign_up_flow_omniauth_box'
.omniauth-divider.d-flex.align-items-center.text-center
= _("or")
%label.label-bold.d-block
= _("Create an account using:")
- providers = enabled_button_based_providers
.d-flex.justify-content-between.flex-wrap
- providers.each do |provider|
- has_icon = provider_has_icon?(provider)
= link_to omniauth_authorize_path(:user, provider), method: :post, class: "btn d-flex align-items-center omniauth-btn text-left oauth-login mb-2 p-2 #{qa_class_for_provider(provider)}", id: "oauth-login-#{provider}" do
- if has_icon
= provider_image_tag(provider)
%span.ml-2
= label_for_provider(provider)
......@@ -9,10 +9,10 @@
= render "layouts/broadcast"
.content
= render "layouts/flash"
.row.mb-3
.row.mb-6
.col-sm-8.offset-sm-2.col-md-6.offset-md-3.new-session-forms-container
= render_if_exists 'layouts/devise_help_text'
.text-center.signup-heading.mt-3.mb-3
.text-center.signup-heading.mb-3
= image_tag(image_url('logo.svg'), class: 'gitlab-logo', alt: 'GitLab Logo')
- if content_for?(:page_title)
%h2= yield :page_title
......
---
title: Fixes random passwords generated not conforming to minimum_password_length setting
merge_request: 23387
author:
type: fixed
---
title: Reorder signup omniauth options
merge_request: 23082
author:
type: changed
......@@ -34,7 +34,7 @@ module Gitlab
end
def password
@password ||= Gitlab::Utils.force_utf8(Devise.friendly_token[0, 8].downcase)
@password ||= Gitlab::Utils.force_utf8(::User.random_password.downcase)
end
def location
......
......@@ -5361,6 +5361,9 @@ msgstr ""
msgid "Create a personal access token on your account to pull or push via %{protocol}."
msgstr ""
msgid "Create an account using:"
msgstr ""
msgid "Create an issue"
msgstr ""
......@@ -22721,6 +22724,9 @@ msgstr ""
msgid "opened %{timeAgo}"
msgstr ""
msgid "or"
msgstr ""
msgid "out of %d total test"
msgid_plural "out of %d total tests"
msgstr[0] ""
......
......@@ -28,6 +28,15 @@ describe('Dashboard', () => {
let wrapper;
let mock;
const findEnvironmentsDropdown = () => wrapper.find({ ref: 'monitorEnvironmentsDropdown' });
const findAllEnvironmentsDropdownItems = () => findEnvironmentsDropdown().findAll(GlDropdownItem);
const setSearchTerm = searchTerm => {
wrapper.vm.$store.commit(
`monitoringDashboard/${types.SET_ENVIRONMENTS_SEARCH_TERM}`,
searchTerm,
);
};
const createShallowWrapper = (props = {}, options = {}) => {
wrapper = shallowMount(Dashboard, {
localVue,
......@@ -52,9 +61,6 @@ describe('Dashboard', () => {
});
};
const findEnvironmentsDropdown = () => wrapper.find({ ref: 'monitorEnvironmentsDropdown' });
const findAllEnvironmentsDropdownItems = () => findEnvironmentsDropdown().findAll(GlDropdownItem);
beforeEach(() => {
store = createStore();
mock = new MockAdapter(axios);
......@@ -155,12 +161,9 @@ describe('Dashboard', () => {
wrapper.vm
.$nextTick()
.then(() => {
const environmentDropdownItems = findAllEnvironmentsDropdownItems();
expect(findAllEnvironmentsDropdownItems().length).toEqual(environmentData.length);
expect(wrapper.vm.environments.length).toEqual(environmentData.length);
expect(environmentDropdownItems.length).toEqual(wrapper.vm.environments.length);
environmentDropdownItems.wrappers.forEach((itemWrapper, index) => {
findAllEnvironmentsDropdownItems().wrappers.forEach((itemWrapper, index) => {
const anchorEl = itemWrapper.find('a');
if (anchorEl.exists() && environmentData[index].metrics_path) {
const href = anchorEl.attributes('href');
......@@ -248,6 +251,70 @@ describe('Dashboard', () => {
});
});
describe('searchable environments dropdown', () => {
beforeEach(() => {
createMountedWrapper(
{ hasMetrics: true },
{
attachToDocument: true,
stubs: ['graph-group', 'panel-type'],
provide: {
glFeatures: { searchableEnvironmentsDropdown: true },
},
},
);
setupComponentStore(wrapper);
return wrapper.vm.$nextTick();
});
afterEach(() => {
wrapper.destroy();
});
it('renders a search input', () => {
expect(wrapper.find({ ref: 'monitorEnvironmentsDropdownSearch' }).exists()).toBe(true);
});
it('renders dropdown items', () => {
findAllEnvironmentsDropdownItems().wrappers.forEach((itemWrapper, index) => {
const anchorEl = itemWrapper.find('a');
if (anchorEl.exists()) {
expect(anchorEl.text()).toBe(environmentData[index].name);
}
});
});
it('filters rendered dropdown items', () => {
const searchTerm = 'production';
const resultEnvs = environmentData.filter(({ name }) => name.indexOf(searchTerm) !== -1);
setSearchTerm(searchTerm);
return wrapper.vm.$nextTick(() => {
expect(findAllEnvironmentsDropdownItems().length).toEqual(resultEnvs.length);
});
});
it('does not filter dropdown items if search term is empty string', () => {
const searchTerm = '';
setSearchTerm(searchTerm);
return wrapper.vm.$nextTick(() => {
expect(findAllEnvironmentsDropdownItems().length).toEqual(environmentData.length);
});
});
it("shows error message if search term doesn't match", () => {
const searchTerm = 'does-not-exist';
setSearchTerm(searchTerm);
return wrapper.vm.$nextTick(() => {
expect(wrapper.find({ ref: 'monitorEnvironmentsDropdownMsg' }).isVisible()).toBe(true);
});
});
});
describe('drag and drop function', () => {
const findDraggables = () => wrapper.findAll(VueDraggable);
const findEnabledDraggables = () => findDraggables().filter(f => !f.attributes('disabled'));
......
......@@ -331,6 +331,14 @@ export const mockedQueryResultPayloadCoresTotal = {
],
};
const extraEnvironmentData = new Array(15).fill(null).map((_, idx) => ({
id: 136 + idx,
name: `no-deployment/noop-branch-${idx}`,
state: 'available',
created_at: '2018-07-04T18:39:41.702Z',
updated_at: '2018-07-04T18:44:54.010Z',
}));
export const environmentData = [
{
id: 34,
......@@ -368,14 +376,7 @@ export const environmentData = [
id: 128,
},
},
{
id: 36,
name: 'no-deployment/noop-branch',
state: 'available',
created_at: '2018-07-04T18:39:41.702Z',
updated_at: '2018-07-04T18:44:54.010Z',
},
];
].concat(extraEnvironmentData);
export const metricsDashboardResponse = {
dashboard: {
......
......@@ -3,6 +3,7 @@ import mutations from '~/monitoring/stores/mutations';
import * as types from '~/monitoring/stores/mutation_types';
import { metricStates } from '~/monitoring/constants';
import {
environmentData,
metricsDashboardPayload,
mockedEmptyResult,
mockedQueryResultPayload,
......@@ -214,4 +215,58 @@ describe('Monitoring store Getters', () => {
});
});
});
describe('filteredEnvironments', () => {
let state;
const setupState = (initState = {}) => {
state = {
...state,
...initState,
};
};
beforeAll(() => {
setupState({
environments: environmentData,
});
});
afterAll(() => {
state = null;
});
[
{
input: '',
output: 17,
},
{
input: ' ',
output: 17,
},
{
input: null,
output: 17,
},
{
input: 'does-not-exist',
output: 0,
},
{
input: 'noop-branch-',
output: 15,
},
{
input: 'noop-branch-9',
output: 1,
},
].forEach(({ input, output }) => {
it(`filteredEnvironments returns ${output} items for ${input}`, () => {
setupState({
environmentsSearchTerm: input,
});
expect(getters.filteredEnvironments(state).length).toBe(output);
});
});
});
});
......@@ -73,12 +73,17 @@ describe AuthHelper do
describe 'enabled_button_based_providers' do
before do
allow(helper).to receive(:auth_providers) { [:twitter, :github] }
allow(helper).to receive(:auth_providers) { [:twitter, :github, :google_oauth2] }
end
context 'all providers are enabled to sign in' do
it 'returns all the enabled providers from settings' do
expect(helper.enabled_button_based_providers).to include('twitter', 'github')
expect(helper.enabled_button_based_providers).to include('twitter', 'github', 'google_oauth2')
end
it 'puts google and github in the beginning' do
expect(helper.enabled_button_based_providers.first).to eq('google_oauth2')
expect(helper.enabled_button_based_providers.second).to eq('github')
end
end
......
......@@ -139,6 +139,18 @@ describe Gitlab::Auth::LDAP::User do
expect(gl_user).to be_confirmed
end
end
context 'when the current minimum password length is different from the default minimum password length' do
before do
stub_application_setting minimum_password_length: 21
end
it 'creates the user' do
ldap_user.save
expect(gl_user).to be_persisted
end
end
end
describe 'updating email' do
......
......@@ -86,6 +86,20 @@ describe Gitlab::Auth::OAuth::User do
end
end
context 'when the current minimum password length is different from the default minimum password length' do
before do
stub_application_setting minimum_password_length: 21
end
it 'creates the user' do
stub_omniauth_config(allow_single_sign_on: [provider])
oauth_user.save
expect(gl_user).to be_persisted
end
end
it 'marks user as having password_automatically_set' do
stub_omniauth_config(allow_single_sign_on: [provider], external_providers: [provider])
......
......@@ -325,6 +325,18 @@ describe Gitlab::Auth::Saml::User do
expect(gl_user).to be_confirmed
end
end
context 'when the current minimum password length is different from the default minimum password length' do
before do
stub_application_setting minimum_password_length: 21
end
it 'creates the user' do
saml_user.save
expect(gl_user).to be_persisted
end
end
end
describe 'blocking' do
......
......@@ -508,6 +508,20 @@ describe User, :do_not_mock_admin_mode do
end
end
describe '.random_password' do
let(:random_password) { described_class.random_password }
before do
expect(User).to receive(:password_length).and_return(88..128)
end
context 'length' do
it 'conforms to the current password length settings' do
expect(random_password.length).to eq(128)
end
end
end
describe '.password_length' do
let(:password_length) { described_class.password_length }
......
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