Commit 8a8c6280 authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents 92fa6bd0 23551042
...@@ -801,7 +801,7 @@ export default class FilteredSearchManager { ...@@ -801,7 +801,7 @@ export default class FilteredSearchManager {
paths.push(`search=${sanitized}`); paths.push(`search=${sanitized}`);
} }
let parameterizedUrl = `?scope=all&utf8=%E2%9C%93&${paths.join('&')}`; let parameterizedUrl = `?scope=all&${paths.join('&')}`;
if (this.anchor) { if (this.anchor) {
parameterizedUrl += `#${this.anchor}`; parameterizedUrl += `#${this.anchor}`;
......
...@@ -113,7 +113,6 @@ export default { ...@@ -113,7 +113,6 @@ export default {
}), }),
visibility: initFormField({ visibility: initFormField({
value: this.projectVisibility, value: this.projectVisibility,
skipValidation: true,
}), }),
}, },
}; };
...@@ -326,7 +325,11 @@ export default { ...@@ -326,7 +325,11 @@ export default {
/> />
</gl-form-group> </gl-form-group>
<gl-form-group> <gl-form-group
v-validation:[form.showValidation]
:invalid-feedback="s__('ForkProject|Please select a visibility level')"
:state="form.fields.visibility.state"
>
<label> <label>
{{ s__('ForkProject|Visibility level') }} {{ s__('ForkProject|Visibility level') }}
<gl-link :href="visibilityHelpPath" target="_blank"> <gl-link :href="visibilityHelpPath" target="_blank">
......
# frozen_string_literal: true
# This file was introduced during upgrading Rails from 5.2 to 6.0.
# This file can be removed when `config.load_defaults 6.0` is introduced.
# Don't force requests from old versions of IE to be UTF-8 encoded.
Rails.application.config.action_view.default_enforce_utf8 = false
...@@ -74,10 +74,6 @@ Example response: ...@@ -74,10 +74,6 @@ Example response:
"key": "OpenJDK", "key": "OpenJDK",
"name": "OpenJDK" "name": "OpenJDK"
}, },
{
"key": "OpenJDK-alpine",
"name": "OpenJDK-alpine"
},
{ {
"key": "PHP", "key": "PHP",
"name": "PHP" "name": "PHP"
......
<script> <script>
import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
import { GlSingleStat } from '@gitlab/ui/dist/charts';
import Api from 'ee/api'; import Api from 'ee/api';
import MetricCard from '~/analytics/shared/components/metric_card.vue';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { __, s__ } from '~/locale'; import { __, s__ } from '~/locale';
export default { export default {
name: 'GroupActivityCard', name: 'GroupActivityCard',
components: { components: {
MetricCard, GlSkeletonLoading,
GlSingleStat,
}, },
inject: ['groupFullPath', 'groupName'], inject: ['groupFullPath', 'groupName'],
data() { data() {
...@@ -65,9 +67,25 @@ export default { ...@@ -65,9 +67,25 @@ export default {
</script> </script>
<template> <template>
<metric-card <div
:title="s__('GroupActivityMetrics|Recent activity (last 90 days)')" class="gl-display-flex gl-flex-direction-column gl-md-flex-direction-row gl-mt-6 gl-mb-4 gl-align-items-flex-start"
:metrics="metricsArray" >
:is-loading="isLoading" <div class="gl-display-flex gl-flex-direction-column gl-pr-9 gl-flex-shrink-0">
/> <span class="gl-font-weight-bold">{{ s__('GroupActivityMetrics|Recent activity') }}</span>
<span>{{ s__('GroupActivityMetrics|Last 90 days') }}</span>
</div>
<div
v-for="metric in metricsArray"
:key="metric.key"
class="gl-pr-9 gl-my-4 gl-md-mt-0 gl-md-mb-0"
>
<gl-skeleton-loading v-if="isLoading" />
<gl-single-stat
v-else
:value="`${metric.value}`"
:title="metric.label"
:should-animate="true"
/>
</div>
</div>
</template> </template>
...@@ -2,31 +2,33 @@ ...@@ -2,31 +2,33 @@
module AuditEvents module AuditEvents
class BuildService class BuildService
# Handle missing attributes
MissingAttributeError = Class.new(StandardError) MissingAttributeError = Class.new(StandardError)
# @raise [MissingAttributeError] when required attributes are blank
#
# @return [BuildService]
def initialize(author:, scope:, target:, ip_address:, message:) def initialize(author:, scope:, target:, ip_address:, message:)
@author = author raise MissingAttributeError if missing_attribute?(author, scope, target, ip_address, message)
@author = build_author(author)
@scope = scope @scope = scope
@target = target @target = target
@ip_address = ip_address @ip_address = ip_address
@message = message @message = build_message(message)
end end
# Create an instance of AuditEvent # Create an instance of AuditEvent
# #
# @raise [MissingAttributeError] when required attributes are blank
#
# @return [AuditEvent] # @return [AuditEvent]
def execute def execute
raise MissingAttributeError if missing_attribute?
AuditEvent.new(payload) AuditEvent.new(payload)
end end
private private
def missing_attribute? def missing_attribute?(author, scope, target, ip_address, message)
@author.blank? || @scope.blank? || @target.blank? || @message.blank? author.blank? || scope.blank? || target.blank? || message.blank?
end end
def payload def payload
...@@ -34,7 +36,8 @@ module AuditEvents ...@@ -34,7 +36,8 @@ module AuditEvents
base_payload.merge( base_payload.merge(
details: base_details_payload.merge( details: base_details_payload.merge(
ip_address: ip_address, ip_address: ip_address,
entity_path: @scope.full_path entity_path: @scope.full_path,
custom_message: @message
), ),
ip_address: ip_address ip_address: ip_address
) )
...@@ -63,6 +66,18 @@ module AuditEvents ...@@ -63,6 +66,18 @@ module AuditEvents
} }
end end
def build_author(author)
author.impersonated? ? ::Gitlab::Audit::ImpersonatedAuthor.new(author) : author
end
def build_message(message)
if License.feature_available?(:admin_audit_log) && @author.impersonated?
"#{message} (by #{@author.impersonated_by})"
else
message
end
end
def ip_address def ip_address
@ip_address.presence || @author.current_sign_in_ip @ip_address.presence || @author.current_sign_in_ip
end end
......
...@@ -23,6 +23,10 @@ module Gitlab ...@@ -23,6 +23,10 @@ module Gitlab
impersonator.name impersonator.name
end end
def impersonated?
true
end
private private
def impersonator def impersonator
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`GroupActivity component matches the snapshot 1`] = `
<div
class="gl-card gl-mb-5"
>
<div
class="gl-card-header"
>
<strong>
Recent activity (last 90 days)
</strong>
</div>
<div
class="gl-card-body"
>
<div
class="gl-display-flex"
>
<div
class="js-metric-card-item gl-flex-grow-1 gl-text-center"
>
<h3
class="gl-my-2"
>
10
</h3>
<p
class="text-secondary gl-font-sm gl-mb-2"
>
Merge Requests opened
<!---->
</p>
</div>
<div
class="js-metric-card-item gl-flex-grow-1 gl-text-center"
>
<h3
class="gl-my-2"
>
20
</h3>
<p
class="text-secondary gl-font-sm gl-mb-2"
>
Issues opened
<!---->
</p>
</div>
<div
class="js-metric-card-item gl-flex-grow-1 gl-text-center"
>
<h3
class="gl-my-2"
>
30
</h3>
<p
class="text-secondary gl-font-sm gl-mb-2"
>
Members added
<!---->
</p>
</div>
</div>
</div>
<!---->
</div>
`;
import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
import { GlSingleStat } from '@gitlab/ui/dist/charts';
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import GroupActivityCard from 'ee/analytics/group_analytics/components/group_activity_card.vue'; import GroupActivityCard from 'ee/analytics/group_analytics/components/group_activity_card.vue';
import Api from 'ee/api'; import Api from 'ee/api';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import MetricCard from '~/analytics/shared/components/metric_card.vue';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
const TEST_GROUP_ID = 'gitlab-org'; const TEST_GROUP_ID = 'gitlab-org';
...@@ -44,20 +45,10 @@ describe('GroupActivity component', () => { ...@@ -44,20 +45,10 @@ describe('GroupActivity component', () => {
mock.restore(); mock.restore();
}); });
const findMetricCard = () => wrapper.find(MetricCard); const findAllSkeletonLoaders = () => wrapper.findAllComponents(GlSkeletonLoading);
const findAllSingleStats = () => wrapper.findAllComponents(GlSingleStat);
it('matches the snapshot', () => { it('fetches the metrics and updates isLoading properly', () => {
createComponent();
return wrapper.vm
.$nextTick()
.then(waitForPromises)
.then(() => {
expect(wrapper.element).toMatchSnapshot();
});
});
it('fetches MR and issue count and updates isLoading properly', () => {
createComponent(); createComponent();
expect(wrapper.vm.isLoading).toBe(true); expect(wrapper.vm.isLoading).toBe(true);
...@@ -79,18 +70,39 @@ describe('GroupActivity component', () => { ...@@ -79,18 +70,39 @@ describe('GroupActivity component', () => {
}); });
}); });
it('passes the metrics array to the metric card', () => { it('updates the loading state properly', () => {
createComponent(); createComponent();
expect(findAllSkeletonLoaders()).toHaveLength(3);
return wrapper.vm return wrapper.vm
.$nextTick() .$nextTick()
.then(waitForPromises) .then(waitForPromises)
.then(() => { .then(() => {
expect(findMetricCard().props('metrics')).toEqual([ expect(findAllSkeletonLoaders()).toHaveLength(0);
{ key: 'mergeRequests', value: 10, label: 'Merge Requests opened' },
{ key: 'issues', value: 20, label: 'Issues opened' },
{ key: 'newMembers', value: 30, label: 'Members added' },
]);
}); });
}); });
describe('metrics', () => {
beforeEach(() => {
createComponent();
});
it.each`
index | value | title
${0} | ${10} | ${'Merge Requests opened'}
${1} | ${20} | ${'Issues opened'}
${2} | ${30} | ${'Members added'}
`('renders a GlSingleStat for "$title"', ({ index, value, title }) => {
const singleStat = findAllSingleStats().at(index);
return wrapper.vm
.$nextTick()
.then(waitForPromises)
.then(() => {
expect(singleStat.props('value')).toBe(`${value}`);
expect(singleStat.props('title')).toBe(title);
});
});
});
}); });
...@@ -3,9 +3,9 @@ ...@@ -3,9 +3,9 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe AuditEvents::BuildService do RSpec.describe AuditEvents::BuildService do
let(:author) { build(:author, current_sign_in_ip: '127.0.0.1') } let(:author) { build_stubbed(:author, current_sign_in_ip: '127.0.0.1') }
let(:scope) { build(:group) } let(:scope) { build_stubbed(:group) }
let(:target) { build(:project) } let(:target) { build_stubbed(:project) }
let(:ip_address) { '192.168.8.8' } let(:ip_address) { '192.168.8.8' }
let(:message) { 'Added an interesting field from project Gotham' } let(:message) { 'Added an interesting field from project Gotham' }
...@@ -58,6 +58,29 @@ RSpec.describe AuditEvents::BuildService do ...@@ -58,6 +58,29 @@ RSpec.describe AuditEvents::BuildService do
expect(event.ip_address).to eq(author.current_sign_in_ip) expect(event.ip_address).to eq(author.current_sign_in_ip)
end end
end end
context 'when author is impersonated' do
let(:impersonator) { build_stubbed(:user, name: 'Agent Donald', current_sign_in_ip: '8.8.8.8') }
let(:author) { build_stubbed(:author, impersonator: impersonator) }
it 'sets author to impersonated user', :aggregate_failures do
expect(event.author_id).to eq(author.id)
expect(event.author_name).to eq(author.name)
end
it 'includes impersonator name in message' do
expect(event.details[:custom_message])
.to eq('Added an interesting field from project Gotham (by Agent Donald)')
end
context 'when IP address is not provided' do
let(:ip_address) { nil }
it 'uses impersonator current_sign_in_ip' do
expect(event.ip_address).to eq(impersonator.current_sign_in_ip)
end
end
end
end end
context 'when not licensed' do context 'when not licensed' do
...@@ -86,31 +109,41 @@ RSpec.describe AuditEvents::BuildService do ...@@ -86,31 +109,41 @@ RSpec.describe AuditEvents::BuildService do
expect(event.created_at).to eq(DateTime.current) expect(event.created_at).to eq(DateTime.current)
end end
end end
context 'when author is impersonated' do
let(:impersonator) { build_stubbed(:user, name: 'Agent Donald', current_sign_in_ip: '8.8.8.8') }
let(:author) { build_stubbed(:author, impersonator: impersonator) }
it 'does not includes impersonator name in message' do
expect(event.details[:custom_message])
.to eq('Added an interesting field from project Gotham')
end
end
end end
context 'when attributes are missing' do context 'when attributes are missing' do
context 'when author is missing' do context 'when author is missing' do
let(:author) { nil } let(:author) { nil }
it { expect { event }.to raise_error(described_class::MissingAttributeError) } it { expect { service }.to raise_error(described_class::MissingAttributeError) }
end end
context 'when scope is missing' do context 'when scope is missing' do
let(:scope) { nil } let(:scope) { nil }
it { expect { event }.to raise_error(described_class::MissingAttributeError) } it { expect { service }.to raise_error(described_class::MissingAttributeError) }
end end
context 'when target is missing' do context 'when target is missing' do
let(:target) { nil } let(:target) { nil }
it { expect { event }.to raise_error(described_class::MissingAttributeError) } it { expect { service }.to raise_error(described_class::MissingAttributeError) }
end end
context 'when message is missing' do context 'when message is missing' do
let(:message) { nil } let(:message) { nil }
it { expect { event }.to raise_error(described_class::MissingAttributeError) } it { expect { service }.to raise_error(described_class::MissingAttributeError) }
end end
end end
end end
......
...@@ -14341,6 +14341,9 @@ msgstr "" ...@@ -14341,6 +14341,9 @@ msgstr ""
msgid "ForkProject|Please select a namespace" msgid "ForkProject|Please select a namespace"
msgstr "" msgstr ""
msgid "ForkProject|Please select a visibility level"
msgstr ""
msgid "ForkProject|Private" msgid "ForkProject|Private"
msgstr "" msgstr ""
...@@ -15745,13 +15748,16 @@ msgstr "" ...@@ -15745,13 +15748,16 @@ msgstr ""
msgid "GroupActivityMetrics|Issues opened" msgid "GroupActivityMetrics|Issues opened"
msgstr "" msgstr ""
msgid "GroupActivityMetrics|Last 90 days"
msgstr ""
msgid "GroupActivityMetrics|Members added" msgid "GroupActivityMetrics|Members added"
msgstr "" msgstr ""
msgid "GroupActivityMetrics|Merge Requests opened" msgid "GroupActivityMetrics|Merge Requests opened"
msgstr "" msgstr ""
msgid "GroupActivityMetrics|Recent activity (last 90 days)" msgid "GroupActivityMetrics|Recent activity"
msgstr "" msgstr ""
msgid "GroupImport|Failed to import group." msgid "GroupImport|Failed to import group."
......
...@@ -185,7 +185,7 @@ describe('Filtered Search Manager', () => { ...@@ -185,7 +185,7 @@ describe('Filtered Search Manager', () => {
}); });
describe('search', () => { describe('search', () => {
const defaultParams = '?scope=all&utf8=%E2%9C%93'; const defaultParams = '?scope=all';
const defaultState = '&state=opened'; const defaultState = '&state=opened';
it('should search with a single word', (done) => { it('should search with a single word', (done) => {
......
...@@ -302,7 +302,6 @@ describe('Issuables list component', () => { ...@@ -302,7 +302,6 @@ describe('Issuables list component', () => {
my_reaction_emoji: 'airplane', my_reaction_emoji: 'airplane',
scope: 'all', scope: 'all',
state: 'opened', state: 'opened',
utf8: '',
weight: '0', weight: '0',
milestone: 'v3.0', milestone: 'v3.0',
labels: 'Aquapod,Astro', labels: 'Aquapod,Astro',
...@@ -312,7 +311,7 @@ describe('Issuables list component', () => { ...@@ -312,7 +311,7 @@ describe('Issuables list component', () => {
describe('when page is not present in params', () => { describe('when page is not present in params', () => {
const query = const query =
'?assignee_username=root&author_username=root&confidential=yes&label_name%5B%5D=Aquapod&label_name%5B%5D=Astro&milestone_title=v3.0&my_reaction_emoji=airplane&scope=all&sort=priority&state=opened&utf8=%E2%9C%93&weight=0&not[label_name][]=Afterpod&not[milestone_title][]=13'; '?assignee_username=root&author_username=root&confidential=yes&label_name%5B%5D=Aquapod&label_name%5B%5D=Astro&milestone_title=v3.0&my_reaction_emoji=airplane&scope=all&sort=priority&state=opened&weight=0&not[label_name][]=Afterpod&not[milestone_title][]=13';
beforeEach(() => { beforeEach(() => {
setUrl(query); setUrl(query);
...@@ -356,7 +355,7 @@ describe('Issuables list component', () => { ...@@ -356,7 +355,7 @@ describe('Issuables list component', () => {
describe('when page is present in the param', () => { describe('when page is present in the param', () => {
const query = const query =
'?assignee_username=root&author_username=root&confidential=yes&label_name%5B%5D=Aquapod&label_name%5B%5D=Astro&milestone_title=v3.0&my_reaction_emoji=airplane&scope=all&sort=priority&state=opened&utf8=%E2%9C%93&weight=0&page=3'; '?assignee_username=root&author_username=root&confidential=yes&label_name%5B%5D=Aquapod&label_name%5B%5D=Astro&milestone_title=v3.0&my_reaction_emoji=airplane&scope=all&sort=priority&state=opened&weight=0&page=3';
beforeEach(() => { beforeEach(() => {
setUrl(query); setUrl(query);
......
...@@ -261,7 +261,7 @@ describe('ForkForm component', () => { ...@@ -261,7 +261,7 @@ describe('ForkForm component', () => {
}); });
describe('onSubmit', () => { describe('onSubmit', () => {
beforeEach(() => { const setupComponent = (fields = {}) => {
jest.spyOn(urlUtility, 'redirectTo').mockImplementation(); jest.spyOn(urlUtility, 'redirectTo').mockImplementation();
mockGetRequest(); mockGetRequest();
...@@ -271,9 +271,14 @@ describe('ForkForm component', () => { ...@@ -271,9 +271,14 @@ describe('ForkForm component', () => {
namespaces: MOCK_NAMESPACES_RESPONSE, namespaces: MOCK_NAMESPACES_RESPONSE,
form: { form: {
state: true, state: true,
...fields,
}, },
}, },
); );
};
beforeEach(() => {
setupComponent();
}); });
const selectedMockNamespaceIndex = 1; const selectedMockNamespaceIndex = 1;
...@@ -305,6 +310,23 @@ describe('ForkForm component', () => { ...@@ -305,6 +310,23 @@ describe('ForkForm component', () => {
expect(urlUtility.redirectTo).not.toHaveBeenCalled(); expect(urlUtility.redirectTo).not.toHaveBeenCalled();
}); });
it('does not make POST request if no visbility is checked', async () => {
jest.spyOn(axios, 'post');
setupComponent({
fields: {
visibility: {
value: null,
},
},
});
await submitForm();
expect(wrapper.find('[name="visibility"]:checked').exists()).toBe(false);
expect(axios.post).not.toHaveBeenCalled();
});
}); });
describe('with valid form', () => { describe('with valid form', () => {
......
FROM openjdk:8-alpine
COPY . /usr/src/myapp
WORKDIR /usr/src/myapp
RUN javac Main.java
CMD ["java", "Main"]
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