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 {
paths.push(`search=${sanitized}`);
}
let parameterizedUrl = `?scope=all&utf8=%E2%9C%93&${paths.join('&')}`;
let parameterizedUrl = `?scope=all&${paths.join('&')}`;
if (this.anchor) {
parameterizedUrl += `#${this.anchor}`;
......
......@@ -113,7 +113,6 @@ export default {
}),
visibility: initFormField({
value: this.projectVisibility,
skipValidation: true,
}),
},
};
......@@ -326,7 +325,11 @@ export default {
/>
</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>
{{ s__('ForkProject|Visibility level') }}
<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:
"key": "OpenJDK",
"name": "OpenJDK"
},
{
"key": "OpenJDK-alpine",
"name": "OpenJDK-alpine"
},
{
"key": "PHP",
"name": "PHP"
......
<script>
import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
import { GlSingleStat } from '@gitlab/ui/dist/charts';
import Api from 'ee/api';
import MetricCard from '~/analytics/shared/components/metric_card.vue';
import createFlash from '~/flash';
import { __, s__ } from '~/locale';
export default {
name: 'GroupActivityCard',
components: {
MetricCard,
GlSkeletonLoading,
GlSingleStat,
},
inject: ['groupFullPath', 'groupName'],
data() {
......@@ -65,9 +67,25 @@ export default {
</script>
<template>
<metric-card
:title="s__('GroupActivityMetrics|Recent activity (last 90 days)')"
:metrics="metricsArray"
:is-loading="isLoading"
<div
class="gl-display-flex gl-flex-direction-column gl-md-flex-direction-row gl-mt-6 gl-mb-4 gl-align-items-flex-start"
>
<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>
......@@ -2,31 +2,33 @@
module AuditEvents
class BuildService
# Handle missing attributes
MissingAttributeError = Class.new(StandardError)
# @raise [MissingAttributeError] when required attributes are blank
#
# @return [BuildService]
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
@target = target
@ip_address = ip_address
@message = message
@message = build_message(message)
end
# Create an instance of AuditEvent
#
# @raise [MissingAttributeError] when required attributes are blank
#
# @return [AuditEvent]
def execute
raise MissingAttributeError if missing_attribute?
AuditEvent.new(payload)
end
private
def missing_attribute?
@author.blank? || @scope.blank? || @target.blank? || @message.blank?
def missing_attribute?(author, scope, target, ip_address, message)
author.blank? || scope.blank? || target.blank? || message.blank?
end
def payload
......@@ -34,7 +36,8 @@ module AuditEvents
base_payload.merge(
details: base_details_payload.merge(
ip_address: ip_address,
entity_path: @scope.full_path
entity_path: @scope.full_path,
custom_message: @message
),
ip_address: ip_address
)
......@@ -63,6 +66,18 @@ module AuditEvents
}
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
@ip_address.presence || @author.current_sign_in_ip
end
......
......@@ -23,6 +23,10 @@ module Gitlab
impersonator.name
end
def impersonated?
true
end
private
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 MockAdapter from 'axios-mock-adapter';
import GroupActivityCard from 'ee/analytics/group_analytics/components/group_activity_card.vue';
import Api from 'ee/api';
import waitForPromises from 'helpers/wait_for_promises';
import MetricCard from '~/analytics/shared/components/metric_card.vue';
import axios from '~/lib/utils/axios_utils';
const TEST_GROUP_ID = 'gitlab-org';
......@@ -44,20 +45,10 @@ describe('GroupActivity component', () => {
mock.restore();
});
const findMetricCard = () => wrapper.find(MetricCard);
const findAllSkeletonLoaders = () => wrapper.findAllComponents(GlSkeletonLoading);
const findAllSingleStats = () => wrapper.findAllComponents(GlSingleStat);
it('matches the snapshot', () => {
createComponent();
return wrapper.vm
.$nextTick()
.then(waitForPromises)
.then(() => {
expect(wrapper.element).toMatchSnapshot();
});
});
it('fetches MR and issue count and updates isLoading properly', () => {
it('fetches the metrics and updates isLoading properly', () => {
createComponent();
expect(wrapper.vm.isLoading).toBe(true);
......@@ -79,18 +70,39 @@ describe('GroupActivity component', () => {
});
});
it('passes the metrics array to the metric card', () => {
it('updates the loading state properly', () => {
createComponent();
expect(findAllSkeletonLoaders()).toHaveLength(3);
return wrapper.vm
.$nextTick()
.then(waitForPromises)
.then(() => {
expect(findMetricCard().props('metrics')).toEqual([
{ key: 'mergeRequests', value: 10, label: 'Merge Requests opened' },
{ key: 'issues', value: 20, label: 'Issues opened' },
{ key: 'newMembers', value: 30, label: 'Members added' },
]);
expect(findAllSkeletonLoaders()).toHaveLength(0);
});
});
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 @@
require 'spec_helper'
RSpec.describe AuditEvents::BuildService do
let(:author) { build(:author, current_sign_in_ip: '127.0.0.1') }
let(:scope) { build(:group) }
let(:target) { build(:project) }
let(:author) { build_stubbed(:author, current_sign_in_ip: '127.0.0.1') }
let(:scope) { build_stubbed(:group) }
let(:target) { build_stubbed(:project) }
let(:ip_address) { '192.168.8.8' }
let(:message) { 'Added an interesting field from project Gotham' }
......@@ -58,6 +58,29 @@ RSpec.describe AuditEvents::BuildService do
expect(event.ip_address).to eq(author.current_sign_in_ip)
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
context 'when not licensed' do
......@@ -86,31 +109,41 @@ RSpec.describe AuditEvents::BuildService do
expect(event.created_at).to eq(DateTime.current)
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
context 'when attributes are missing' do
context 'when author is missing' do
let(:author) { nil }
it { expect { event }.to raise_error(described_class::MissingAttributeError) }
it { expect { service }.to raise_error(described_class::MissingAttributeError) }
end
context 'when scope is missing' do
let(:scope) { nil }
it { expect { event }.to raise_error(described_class::MissingAttributeError) }
it { expect { service }.to raise_error(described_class::MissingAttributeError) }
end
context 'when target is missing' do
let(:target) { nil }
it { expect { event }.to raise_error(described_class::MissingAttributeError) }
it { expect { service }.to raise_error(described_class::MissingAttributeError) }
end
context 'when message is missing' do
let(:message) { nil }
it { expect { event }.to raise_error(described_class::MissingAttributeError) }
it { expect { service }.to raise_error(described_class::MissingAttributeError) }
end
end
end
......
......@@ -14341,6 +14341,9 @@ msgstr ""
msgid "ForkProject|Please select a namespace"
msgstr ""
msgid "ForkProject|Please select a visibility level"
msgstr ""
msgid "ForkProject|Private"
msgstr ""
......@@ -15745,13 +15748,16 @@ msgstr ""
msgid "GroupActivityMetrics|Issues opened"
msgstr ""
msgid "GroupActivityMetrics|Last 90 days"
msgstr ""
msgid "GroupActivityMetrics|Members added"
msgstr ""
msgid "GroupActivityMetrics|Merge Requests opened"
msgstr ""
msgid "GroupActivityMetrics|Recent activity (last 90 days)"
msgid "GroupActivityMetrics|Recent activity"
msgstr ""
msgid "GroupImport|Failed to import group."
......
......@@ -185,7 +185,7 @@ describe('Filtered Search Manager', () => {
});
describe('search', () => {
const defaultParams = '?scope=all&utf8=%E2%9C%93';
const defaultParams = '?scope=all';
const defaultState = '&state=opened';
it('should search with a single word', (done) => {
......
......@@ -302,7 +302,6 @@ describe('Issuables list component', () => {
my_reaction_emoji: 'airplane',
scope: 'all',
state: 'opened',
utf8: '',
weight: '0',
milestone: 'v3.0',
labels: 'Aquapod,Astro',
......@@ -312,7 +311,7 @@ describe('Issuables list component', () => {
describe('when page is not present in params', () => {
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(() => {
setUrl(query);
......@@ -356,7 +355,7 @@ describe('Issuables list component', () => {
describe('when page is present in the param', () => {
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(() => {
setUrl(query);
......
......@@ -261,7 +261,7 @@ describe('ForkForm component', () => {
});
describe('onSubmit', () => {
beforeEach(() => {
const setupComponent = (fields = {}) => {
jest.spyOn(urlUtility, 'redirectTo').mockImplementation();
mockGetRequest();
......@@ -271,9 +271,14 @@ describe('ForkForm component', () => {
namespaces: MOCK_NAMESPACES_RESPONSE,
form: {
state: true,
...fields,
},
},
);
};
beforeEach(() => {
setupComponent();
});
const selectedMockNamespaceIndex = 1;
......@@ -305,6 +310,23 @@ describe('ForkForm component', () => {
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', () => {
......
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