提交 a6578252 编写于 作者: G GitLab Bot

Add latest changes from gitlab-org/gitlab@master

上级 5459ed73
此差异已折叠。
......@@ -214,7 +214,7 @@ gem 're2', '~> 1.2.0'
gem 'version_sorter', '~> 2.2.4'
# Export Ruby Regex to Javascript
gem 'js_regex', '~> 3.1'
gem 'js_regex', '~> 3.4'
# User agent parsing
gem 'device_detector'
......
......@@ -149,7 +149,7 @@ GEM
activemodel (>= 4.0.0)
activesupport (>= 4.0.0)
mime-types (>= 1.16)
character_set (1.1.2)
character_set (1.4.0)
charlock_holmes (0.7.6)
childprocess (3.0.0)
chunky_png (1.3.5)
......@@ -562,9 +562,9 @@ GEM
multipart-post
oauth (~> 0.5, >= 0.5.0)
jmespath (1.4.0)
js_regex (3.1.1)
character_set (~> 1.1)
regexp_parser (~> 1.1)
js_regex (3.4.0)
character_set (~> 1.4)
regexp_parser (~> 1.5)
regexp_property_values (~> 0.3)
json (2.3.0)
json-jwt (1.11.0)
......@@ -889,7 +889,7 @@ GEM
redis-store (1.8.1)
redis (>= 4, < 5)
regexp_parser (1.5.1)
regexp_property_values (0.3.4)
regexp_property_values (0.3.5)
representable (3.0.4)
declarative (< 0.1.0)
declarative-option (< 0.2.0)
......@@ -1291,7 +1291,7 @@ DEPENDENCIES
icalendar
invisible_captcha (~> 0.12.1)
jira-ruby (~> 2.0.0)
js_regex (~> 3.1)
js_regex (~> 3.4)
json (~> 2.3.0)
json-schema (~> 2.8.0)
jwt (~> 2.1.0)
......
......@@ -40,9 +40,6 @@ export default class BranchGraph {
}
prepareData(days, commits) {
let c = 0;
let j = 0;
let len = 0;
this.days = days;
this.commits = commits;
this.collectParents();
......@@ -53,38 +50,33 @@ export default class BranchGraph {
this.r = Raphael(this.element.get(0), cw, ch);
this.top = this.r.set();
this.barHeight = Math.max(this.graphHeight, this.unitTime * this.days.length + 320);
const ref = this.commits;
for (j = 0, len = ref.length; j < len; j += 1) {
c = ref[j];
if (c.id in this.parents) {
c.isParent = true;
this.commits = this.commits.reduce((acc, commit) => {
const updatedCommit = commit;
if (commit.id in this.parents) {
updatedCommit.isParent = true;
}
this.preparedCommits[c.id] = c;
this.markCommit(c);
}
acc.push(updatedCommit);
this.preparedCommits[commit.id] = commit;
this.markCommit(commit);
return acc;
}, []);
return this.collectColors();
}
collectParents() {
let j = 0;
let l = 0;
let len = 0;
let len1 = 0;
const ref = this.commits;
const results = [];
for (j = 0, len = ref.length; j < len; j += 1) {
const c = ref[j];
ref.forEach(c => {
this.mtime = Math.max(this.mtime, c.time);
this.mspace = Math.max(this.mspace, c.space);
const ref1 = c.parents;
const results1 = [];
for (l = 0, len1 = ref1.length; l < len1; l += 1) {
const p = ref1[l];
ref1.forEach(p => {
this.parents[p[0]] = true;
results1.push((this.mspace = Math.max(this.mspace, p[1])));
}
});
results.push(results1);
}
});
return results;
}
......@@ -114,7 +106,6 @@ export default class BranchGraph {
fill: '#444',
});
const ref = this.days;
for (mm = 0, len = ref.length; mm < len; mm += 1) {
const day = ref[mm];
if (cuday !== day[0] || cumonth !== day[1]) {
......@@ -295,7 +286,6 @@ export default class BranchGraph {
const { r } = this;
const ref = commit.parents;
const results = [];
for (i = 0, len = ref.length; i < len; i += 1) {
const parent = ref[i];
const parentCommit = this.preparedCommits[parent[0]];
......
<script>
import { GlEmptyState, GlLink, GlSprintf } from '@gitlab/ui';
import { mapState } from 'vuex';
export default {
props: {
clustersPath: {
type: String,
required: true,
},
helpPath: {
type: String,
required: true,
},
components: {
GlEmptyState,
GlLink,
GlSprintf,
},
computed: {
...mapState(['clustersPath', 'emptyImagePath', 'helpPath']),
},
};
</script>
<template>
<div class="row empty-state js-empty-state">
<div class="col-12">
<div class="text-content">
<h4 class="state-title text-center">
{{ s__('Serverless|Getting started with serverless') }}
</h4>
<p class="state-description">
{{
s__(`Serverless| In order to start using functions as a service,
you must first install Knative on your Kubernetes cluster.`)
}}
<a :href="helpPath"> {{ __('More information') }} </a>
</p>
<div class="text-center">
<a :href="clustersPath" class="btn btn-success">
{{ s__('Serverless|Install Knative') }}
</a>
</div>
</div>
</div>
</div>
<gl-empty-state
:svg-path="emptyImagePath"
:title="s__('Serverless|Getting started with serverless')"
:primary-button-link="clustersPath"
:primary-button-text="s__('Serverless|Install Knative')"
>
<template #description>
<gl-sprintf
:message="
s__(
'Serverless|In order to start using functions as a service, you must first install Knative on your Kubernetes cluster. %{linkStart}More information%{linkEnd}',
)
"
>
<template #link="{ content }">
<gl-link :href="helpPath">{{ content }}</gl-link>
</template>
</gl-sprintf>
</template>
</gl-empty-state>
</template>
......@@ -23,14 +23,6 @@ export default {
required: false,
default: false,
},
clustersPath: {
type: String,
required: true,
},
helpPath: {
type: String,
required: true,
},
},
data() {
return {
......@@ -96,8 +88,6 @@ export default {
<area-chart v-if="hasPrometheusData" :graph-data="graphData" :container-width="elWidth" />
<missing-prometheus
v-if="!hasPrometheus || hasPrometheusMissingData"
:help-path="helpPath"
:clusters-path="clustersPath"
:missing-data="hasPrometheusMissingData"
/>
</section>
......
<script>
import { mapState, mapActions, mapGetters } from 'vuex';
import { GlLoadingIcon } from '@gitlab/ui';
import { GlLink, GlLoadingIcon } from '@gitlab/ui';
import { sprintf, s__ } from '~/locale';
import EnvironmentRow from './environment_row.vue';
import EmptyState from './empty_state.vue';
......@@ -10,24 +10,11 @@ export default {
components: {
EnvironmentRow,
EmptyState,
GlLink,
GlLoadingIcon,
},
props: {
clustersPath: {
type: String,
required: true,
},
helpPath: {
type: String,
required: true,
},
statusPath: {
type: String,
required: true,
},
},
computed: {
...mapState(['installed', 'isLoading', 'hasFunctionData']),
...mapState(['installed', 'isLoading', 'hasFunctionData', 'helpPath', 'statusPath']),
...mapGetters(['getFunctions']),
checkingInstalled() {
......@@ -118,14 +105,14 @@ export default {
}}
</p>
<div class="text-center">
<a :href="helpPath" class="btn btn-success">
{{ s__('Serverless|Learn more about Serverless') }}
</a>
<gl-link :href="helpPath" class="btn btn-success">{{
s__('Serverless|Learn more about Serverless')
}}</gl-link>
</div>
</div>
</div>
</div>
<empty-state v-else :clusters-path="clustersPath" :help-path="helpPath" />
<empty-state v-else />
</section>
</template>
<script>
import { GlDeprecatedButton, GlLink } from '@gitlab/ui';
import { mapState } from 'vuex';
import { s__ } from '../../locale';
export default {
......@@ -8,20 +9,13 @@ export default {
GlLink,
},
props: {
clustersPath: {
type: String,
required: true,
},
helpPath: {
type: String,
required: true,
},
missingData: {
type: Boolean,
required: true,
},
},
computed: {
...mapState(['clustersPath', 'helpPath']),
missingStateClass() {
return this.missingData ? 'missing-prometheus-state' : 'empty-prometheus-state';
},
......
......@@ -6,6 +6,9 @@ import { createStore } from './store';
export default class Serverless {
constructor() {
if (document.querySelector('.js-serverless-function-details-page') != null) {
const entryPointData = document.querySelector('.js-serverless-function-details-page').dataset;
const store = createStore(entryPointData);
const {
serviceName,
serviceDescription,
......@@ -15,9 +18,7 @@ export default class Serverless {
servicePodcount,
serviceMetricsUrl,
prometheus,
clustersPath,
helpPath,
} = document.querySelector('.js-serverless-function-details-page').dataset;
} = entryPointData;
const el = document.querySelector('#js-serverless-function-details');
const service = {
......@@ -32,35 +33,26 @@ export default class Serverless {
this.functionDetails = new Vue({
el,
store: createStore(),
store,
render(createElement) {
return createElement(FunctionDetails, {
props: {
func: service,
hasPrometheus: prometheus !== undefined,
clustersPath,
helpPath,
},
});
},
});
} else {
const { statusPath, clustersPath, helpPath } = document.querySelector(
'.js-serverless-functions-page',
).dataset;
const entryPointData = document.querySelector('.js-serverless-functions-page').dataset;
const store = createStore(entryPointData);
const el = document.querySelector('#js-serverless-functions');
this.functions = new Vue({
el,
store: createStore(),
store,
render(createElement) {
return createElement(Functions, {
props: {
clustersPath,
helpPath,
statusPath,
},
});
return createElement(Functions);
},
});
}
......
......@@ -7,12 +7,12 @@ import createState from './state';
Vue.use(Vuex);
export const createStore = () =>
export const createStore = (entryPointData = {}) =>
new Vuex.Store({
actions,
getters,
mutations,
state: createState(),
state: createState(entryPointData),
});
export default createStore();
export default createStore;
export default () => ({
export default (
initialState = { clustersPath: null, helpPath: null, emptyImagePath: null, statusPath: null },
) => ({
clustersPath: initialState.clustersPath,
error: null,
helpPath: initialState.helpPath,
installed: 'checking',
isLoading: true,
// functions
functions: [],
hasFunctionData: true,
statusPath: initialState.statusPath,
// function_details
hasPrometheus: true,
hasPrometheusData: false,
graphData: {},
// empty_state
emptyImagePath: initialState.emptyImagePath,
});
......@@ -7,7 +7,8 @@
.serverless-functions-page.js-serverless-functions-page{ data: { status_path: status_path,
installed: @installed,
clusters_path: clusters_path,
help_path: help_page_path('user/project/clusters/serverless/index') } }
help_path: help_page_path('user/project/clusters/serverless/index'),
empty_image_path: image_path('illustrations/empty-state/empty-serverless-lg.svg') } }
%div{ class: [('limit-container-width' unless fluid_layout)] }
.js-serverless-survey-banner{ data: { user_name: current_user.name, user_email: current_user.email } }
......@@ -15,5 +16,5 @@
.js-serverless-functions-notice
.flash-container
.top-area.adjust.d-flex.justify-content-center
.top-area.adjust.d-flex.justify-content-center.gl-border-none
.serverless-functions-table#js-serverless-functions
---
title: Add serverless empty state illustration
merge_request: 36762
author:
type: changed
---
title: Use ES6 methods instead of `for` loops
merge_request: 37324
author: allenlai18
type: other
---
title: Add external column to custom emoji table
merge_request: 37346
author: Rajendra Kadam
type: added
# frozen_string_literal: true
class AddExternalToCustomEmoji < ActiveRecord::Migration[6.0]
DOWNTIME = false
def change
add_column :custom_emoji, :external, :boolean, default: true, null: false
end
end
......@@ -10918,6 +10918,7 @@ CREATE TABLE public.custom_emoji (
updated_at timestamp with time zone NOT NULL,
name text NOT NULL,
file text NOT NULL,
external boolean DEFAULT true NOT NULL,
CONSTRAINT check_8c586dd507 CHECK ((char_length(name) <= 36)),
CONSTRAINT check_dd5d60f1fb CHECK ((char_length(file) <= 255))
);
......@@ -23998,5 +23999,6 @@ COPY "schema_migrations" (version) FROM STDIN;
20200718040100
20200718040200
20200718040300
20200720154123
\.
---
stage: Release
group: Release Management
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Deployment safety
Deployment jobs can be more sensitive than other jobs in a pipeline,
......
---
type: reference
stage: Release
group: Release Management
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# CI/CD Environment Variables
......
......@@ -324,8 +324,9 @@ As in other list types, click the trash icon to remove a list.
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/11403) in GitLab 12.7
You can set Work In Progress (WIP) limits per issues list. When a limit is set, the list's header
shows the number of issues in the list and the soft limit of issues.
You can set a Work In Progress (WIP) limit for each issue list on an issue board. When a limit is
set, the list's header shows the number of issues in the list and the soft limit of issues.
You cannot set a WIP limit on the default lists (**Open** and **Closed**).
Examples:
......
......@@ -47,6 +47,7 @@ module Gitlab
push_frontend_feature_flag(:monaco_ci, default_enabled: false)
push_frontend_feature_flag(:snippets_edit_vue, default_enabled: false)
push_frontend_feature_flag(:webperf_experiment, default_enabled: false)
push_frontend_feature_flag(:snippets_binary_blob, default_enabled: false)
end
# Exposes the state of a feature flag to the frontend code.
......
......@@ -21387,9 +21387,6 @@ msgstr ""
msgid "ServerlessURL|Copy URL"
msgstr ""
msgid "Serverless| In order to start using functions as a service, you must first install Knative on your Kubernetes cluster."
msgstr ""
msgid "Serverless|Getting started with serverless"
msgstr ""
......@@ -21399,6 +21396,9 @@ msgstr ""
msgid "Serverless|If you believe none of these apply, please check back later as the function data may be in the process of becoming available."
msgstr ""
msgid "Serverless|In order to start using functions as a service, you must first install Knative on your Kubernetes cluster. %{linkStart}More information%{linkEnd}"
msgstr ""
msgid "Serverless|Install Knative"
msgstr ""
......
......@@ -104,6 +104,14 @@ RSpec.describe 'User uses header search field', :js do
let(:scope_name) { 'All GitLab' }
end
it 'displays search options' do
page.within('.search-input-wrap') do
fill_in('search', with: 'test')
end
expect(page).to have_selector(scoped_search_link('test'))
end
context 'when searching through the search field' do
before do
create(:issue, project: project, title: 'project issue')
......@@ -122,9 +130,41 @@ RSpec.describe 'User uses header search field', :js do
end
context 'when user is in a project scope' do
include_examples 'search field examples' do
let(:url) { project_path(project) }
let(:scope_name) { project.name }
context 'and it belongs to a group' do
let(:group) { create(:group) }
let(:project) { create(:project, namespace: group) }
include_examples 'search field examples' do
let(:url) { project_path(project) }
let(:scope_name) { project.name }
end
it 'displays search options' do
page.within('.search-input-wrap') do
fill_in('search', with: 'test')
end
expect(page).to have_selector(scoped_search_link('test'))
expect(page).to have_selector(scoped_search_link('test', group_id: group.id))
expect(page).to have_selector(scoped_search_link('test', project_id: project.id, group_id: group.id))
end
end
context 'and it belongs to a user' do
include_examples 'search field examples' do
let(:url) { project_path(project) }
let(:scope_name) { project.name }
end
it 'displays search options' do
page.within('.search-input-wrap') do
fill_in('search', with: 'test')
end
expect(page).to have_selector(scoped_search_link('test'))
expect(page).not_to have_selector(scoped_search_link('test', group_id: project.namespace_id))
expect(page).to have_selector(scoped_search_link('test', project_id: project.id))
end
end
end
......@@ -140,6 +180,16 @@ RSpec.describe 'User uses header search field', :js do
let(:url) { group_path(group) }
let(:scope_name) { group.name }
end
it 'displays search options' do
page.within('.search-input-wrap') do
fill_in('search', with: 'test')
end
expect(page).to have_selector(scoped_search_link('test'))
expect(page).to have_selector(scoped_search_link('test', group_id: group.id))
expect(page).not_to have_selector(scoped_search_link('test', project_id: project.id))
end
end
context 'when user is in a subgroup scope' do
......@@ -156,5 +206,25 @@ RSpec.describe 'User uses header search field', :js do
let(:url) { group_path(subgroup) }
let(:scope_name) { subgroup.name }
end
it 'displays search options' do
page.within('.search-input-wrap') do
fill_in('search', with: 'test')
end
expect(page).to have_selector(scoped_search_link('test'))
expect(page).to have_selector(scoped_search_link('test', group_id: subgroup.id))
expect(page).not_to have_selector(scoped_search_link('test', project_id: project.id))
end
end
def scoped_search_link(term, project_id: nil, group_id: nil)
# search_path will accept group_id and project_id but the order does not match
# what is expected in the href, so the variable must be built manually
href = search_path(search: term)
href.concat("&project_id=#{project_id}") if project_id
href.concat("&group_id=#{group_id}") if group_id
".dropdown a[href='#{href}']"
end
end
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`EmptyStateComponent should render content 1`] = `
"<section class=\\"row empty-state text-center\\">
<div class=\\"col-12\\">
<div class=\\"svg-250 svg-content\\"><img src=\\"/image.svg\\" alt=\\"Getting started with serverless\\" class=\\"gl-max-w-full\\"></div>
</div>
<div class=\\"col-12\\">
<div class=\\"text-content gl-mx-auto gl-my-0 gl-p-5\\">
<h1 class=\\"h4\\">Getting started with serverless</h1>
<p>In order to start using functions as a service, you must first install Knative on your Kubernetes cluster. <gl-link-stub href=\\"/help\\">More information</gl-link-stub>
</p>
<div>
<gl-button-stub category=\\"tertiary\\" variant=\\"success\\" size=\\"medium\\" icon=\\"\\" href=\\"/clusters\\">Install Knative</gl-button-stub>
<!---->
</div>
</div>
</div>
</section>"
`;
import { createStore } from '~/serverless/store';
import { GlEmptyState, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import EmptyStateComponent from '~/serverless/components/empty_state.vue';
describe('EmptyStateComponent', () => {
let wrapper;
beforeEach(() => {
const store = createStore({
clustersPath: '/clusters',
helpPath: '/help',
emptyImagePath: '/image.svg',
});
wrapper = shallowMount(EmptyStateComponent, { store, stubs: { GlEmptyState, GlSprintf } });
});
afterEach(() => {
wrapper.destroy();
});
it('should render content', () => {
expect(wrapper.html()).toMatchSnapshot();
});
});
......@@ -13,7 +13,7 @@ describe('functionDetailsComponent', () => {
localVue = createLocalVue();
localVue.use(Vuex);
store = createStore();
store = createStore({ clustersPath: '/clusters', helpPath: '/help' });
});
afterEach(() => {
......@@ -38,8 +38,6 @@ describe('functionDetailsComponent', () => {
propsData: {
func: serviceStub,
hasPrometheus: false,
clustersPath: '/clusters',
helpPath: '/help',
},
});
......@@ -65,8 +63,6 @@ describe('functionDetailsComponent', () => {
propsData: {
func: serviceStub,
hasPrometheus: false,
clustersPath: '/clusters',
helpPath: '/help',
},
});
......@@ -82,8 +78,6 @@ describe('functionDetailsComponent', () => {
propsData: {
func: serviceStub,
hasPrometheus: false,
clustersPath: '/clusters',
helpPath: '/help',
},
});
......@@ -99,8 +93,6 @@ describe('functionDetailsComponent', () => {
propsData: {
func: serviceStub,
hasPrometheus: false,
clustersPath: '/clusters',
helpPath: '/help',
},
});
......
......@@ -25,55 +25,31 @@ describe('functionsComponent', () => {
localVue = createLocalVue();
localVue.use(Vuex);
store = createStore();
store = createStore({});
});
afterEach(() => {
component.vm.$destroy();
component.destroy();
axiosMock.restore();
});
it('should render empty state when Knative is not installed', () => {
store.dispatch('receiveFunctionsSuccess', { knative_installed: false });
component = shallowMount(functionsComponent, {
localVue,
store,
propsData: {
clustersPath: '',
helpPath: '',
statusPath: '',
},
});
component = shallowMount(functionsComponent, { localVue, store });
expect(component.find(EmptyState).exists()).toBe(true);
});
it('should render a loading component', () => {
store.dispatch('requestFunctionsLoading');
component = shallowMount(functionsComponent, {
localVue,
store,
propsData: {
clustersPath: '',
helpPath: '',
statusPath: '',
},
});
component = shallowMount(functionsComponent, { localVue, store });
expect(component.find(GlLoadingIcon).exists()).toBe(true);
});
it('should render empty state when there is no function data', () => {
store.dispatch('receiveFunctionsNoDataSuccess', { knative_installed: true });
component = shallowMount(functionsComponent, {
localVue,
store,
propsData: {
clustersPath: '',
helpPath: '',
statusPath: '',
},
});
component = shallowMount(functionsComponent, { localVue, store });
expect(
component.vm.$el
......@@ -91,30 +67,17 @@ describe('functionsComponent', () => {
...mockServerlessFunctions,
knative_installed: 'checking',
});
component = shallowMount(functionsComponent, {
localVue,
store,
propsData: {
clustersPath: '',
helpPath: '',
statusPath: '',
},
});
component = shallowMount(functionsComponent, { localVue, store });
expect(component.find('.js-functions-wrapper').exists()).toBe(true);
expect(component.find('.js-functions-loader').exists()).toBe(true);
});
it('should render the functions list', () => {
component = shallowMount(functionsComponent, {
localVue,
store,
propsData: {
clustersPath: 'clustersPath',
helpPath: 'helpPath',
statusPath,
},
});
store = createStore({ clustersPath: 'clustersPath', helpPath: 'helpPath', statusPath });
component = shallowMount(functionsComponent, { localVue, store });
component.vm.$store.dispatch('receiveFunctionsSuccess', mockServerlessFunctions);
......
import { createStore } from '~/serverless/store';
import { GlDeprecatedButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import missingPrometheusComponent from '~/serverless/components/missing_prometheus.vue';
const createComponent = missingData =>
shallowMount(missingPrometheusComponent, {
propsData: {
clustersPath: '/clusters',
helpPath: '/help',
missingData,
},
});
describe('missingPrometheusComponent', () => {
let wrapper;
const createComponent = missingData => {
const store = createStore({ clustersPath: '/clusters', helpPath: '/help' });
wrapper = shallowMount(missingPrometheusComponent, { store, propsData: { missingData } });
};
afterEach(() => {
wrapper.destroy();
});
it('should render missing prometheus message', () => {
wrapper = createComponent(false);
createComponent(false);
const { vm } = wrapper;
expect(vm.$el.querySelector('.state-description').innerHTML.trim()).toContain(
......@@ -30,7 +28,7 @@ describe('missingPrometheusComponent', () => {
});
it('should render no prometheus data message', () => {
wrapper = createComponent(true);
createComponent(true);
const { vm } = wrapper;
expect(vm.$el.querySelector('.state-description').innerHTML.trim()).toContain(
......
......@@ -8,6 +8,7 @@ RSpec.describe CustomEmoji do
it { is_expected.to have_db_column(:file) }
it { is_expected.to validate_length_of(:name).is_at_most(36) }
it { is_expected.to validate_presence_of(:name) }
it { is_expected.to have_db_column(:external) }
end
describe 'exclusion of duplicated emoji' do
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册