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

Add latest changes from gitlab-org/gitlab@master

parent 7e208091
import sqljs from 'sql.js'; import sqljs from 'sql.js';
import { template as _template } from 'underscore'; import { template as _template } from 'underscore';
import axios from '~/lib/utils/axios_utils';
import { successCodes } from '~/lib/utils/http_status';
const PREVIEW_TEMPLATE = _template(` const PREVIEW_TEMPLATE = _template(`
<div class="card"> <div class="card">
...@@ -16,30 +18,25 @@ class BalsamiqViewer { ...@@ -16,30 +18,25 @@ class BalsamiqViewer {
} }
loadFile(endpoint) { loadFile(endpoint) {
return new Promise((resolve, reject) => { return axios
const xhr = new XMLHttpRequest(); .get(endpoint, {
responseType: 'arraybuffer',
xhr.open('GET', endpoint, true); validateStatus(status) {
xhr.responseType = 'arraybuffer'; return status !== successCodes.OK;
xhr.onload = loadEvent => this.fileLoaded(loadEvent, resolve, reject); },
xhr.onerror = reject; })
.then(({ data }) => {
xhr.send(); this.renderFile(data);
}); })
} .catch(e => {
throw new Error(e);
fileLoaded(loadEvent, resolve, reject) { });
if (loadEvent.target.status !== 200) return reject();
this.renderFile(loadEvent);
return resolve();
} }
renderFile(loadEvent) { renderFile(fileBuffer) {
const container = document.createElement('ul'); const container = document.createElement('ul');
this.initDatabase(loadEvent.target.response); this.initDatabase(fileBuffer);
const previews = this.getPreviews(); const previews = this.getPreviews();
previews.forEach(preview => { previews.forEach(preview => {
......
...@@ -877,11 +877,16 @@ pre.light-well { ...@@ -877,11 +877,16 @@ pre.light-well {
flex-direction: column; flex-direction: column;
// Disable Flexbox for admin page // Disable Flexbox for admin page
&.admin-projects { &.admin-projects,
&.group-settings-projects {
display: block; display: block;
.project-row { .project-row {
display: block; display: block;
.description > p {
margin-bottom: 0;
}
} }
} }
......
...@@ -51,7 +51,8 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated ...@@ -51,7 +51,8 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
new_file_anchor_data, new_file_anchor_data,
readme_anchor_data, readme_anchor_data,
changelog_anchor_data, changelog_anchor_data,
contribution_guide_anchor_data contribution_guide_anchor_data,
gitlab_ci_anchor_data
].compact.reject { |item| item.is_link } ].compact.reject { |item| item.is_link }
end end
......
- page_title "Projects" - page_title _('Projects')
- params[:visibility_level] ||= [] - params[:visibility_level] ||= []
.top-area.scrolling-tabs-container.inner-page-scroll-tabs .top-area.scrolling-tabs-container.inner-page-scroll-tabs
.prepend-top-default %ul.nav-links.nav.nav-tabs
- opts = params[:visibility_level].present? ? {} : { page: admin_projects_path }
= nav_link(opts) do
= link_to _('All'), admin_projects_path
= nav_link(html_options: { class: active_when(params[:visibility_level] == Gitlab::VisibilityLevel::PRIVATE.to_s) }) do
= link_to _('Private'), admin_projects_path(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
= nav_link(html_options: { class: active_when(params[:visibility_level] == Gitlab::VisibilityLevel::INTERNAL.to_s) }) do
= link_to _('Internal'), admin_projects_path(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
= nav_link(html_options: { class: active_when(params[:visibility_level] == Gitlab::VisibilityLevel::PUBLIC.to_s) }) do
= link_to _('Public'), admin_projects_path(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
.nav-controls
.search-holder .search-holder
= render 'shared/projects/search_form', autofocus: true, admin_view: true = render 'shared/projects/search_form', autofocus: true, admin_view: true
.dropdown .dropdown
...@@ -22,20 +34,4 @@ ...@@ -22,20 +34,4 @@
New Project New Project
= button_tag "Search", class: "btn btn-primary btn-search hide" = button_tag "Search", class: "btn btn-primary btn-search hide"
%ul.nav-links.nav.nav-tabs
- opts = params[:visibility_level].present? ? {} : { page: admin_projects_path }
= nav_link(opts) do
= link_to admin_projects_path do
All
= nav_link(html_options: { class: active_when(params[:visibility_level] == Gitlab::VisibilityLevel::PRIVATE.to_s) }) do
= link_to admin_projects_path(visibility_level: Gitlab::VisibilityLevel::PRIVATE) do
Private
= nav_link(html_options: { class: active_when(params[:visibility_level] == Gitlab::VisibilityLevel::INTERNAL.to_s) }) do
= link_to admin_projects_path(visibility_level: Gitlab::VisibilityLevel::INTERNAL) do
Internal
= nav_link(html_options: { class: active_when(params[:visibility_level] == Gitlab::VisibilityLevel::PUBLIC.to_s) }) do
= link_to admin_projects_path(visibility_level: Gitlab::VisibilityLevel::PUBLIC) do
Public
= render 'projects' = render 'projects'
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
- breadcrumb_title @cluster.name - breadcrumb_title @cluster.name
- page_title _('Kubernetes Cluster') - page_title _('Kubernetes Cluster')
- manage_prometheus_path = edit_project_service_path(@cluster.project, 'prometheus') if @project - manage_prometheus_path = edit_project_service_path(@cluster.project, 'prometheus') if @project
- cluster_environments_path = clusterable.environments_cluster_path(@cluster)
- expanded = expanded_by_default? - expanded = expanded_by_default?
...@@ -16,7 +17,7 @@ ...@@ -16,7 +17,7 @@
install_jupyter_path: clusterable.install_applications_cluster_path(@cluster, :jupyter), install_jupyter_path: clusterable.install_applications_cluster_path(@cluster, :jupyter),
install_knative_path: clusterable.install_applications_cluster_path(@cluster, :knative), install_knative_path: clusterable.install_applications_cluster_path(@cluster, :knative),
update_knative_path: clusterable.update_applications_cluster_path(@cluster, :knative), update_knative_path: clusterable.update_applications_cluster_path(@cluster, :knative),
cluster_environments_path: clusterable.environments_cluster_path(@cluster), cluster_environments_path: cluster_environments_path,
toggle_status: @cluster.enabled? ? 'true': 'false', toggle_status: @cluster.enabled? ? 'true': 'false',
has_rbac: has_rbac_enabled?(@cluster) ? 'true': 'false', has_rbac: has_rbac_enabled?(@cluster) ? 'true': 'false',
cluster_type: @cluster.cluster_type, cluster_type: @cluster.cluster_type,
...@@ -37,7 +38,7 @@ ...@@ -37,7 +38,7 @@
%h4= @cluster.name %h4= @cluster.name
= render 'banner' = render 'banner'
= render_if_exists 'clusters/clusters/group_cluster_environments', expanded: expanded - if cluster_environments_path.present?
= render_if_exists 'clusters/clusters/group_cluster_environments', expanded: expanded
- unless Gitlab.ee? - else
= render 'configure', expanded: expanded = render 'configure', expanded: expanded
...@@ -8,21 +8,38 @@ ...@@ -8,21 +8,38 @@
.controls .controls
= link_to new_project_path(namespace_id: @group.id), class: "btn btn-sm btn-success" do = link_to new_project_path(namespace_id: @group.id), class: "btn btn-sm btn-success" do
New project New project
%ul.content-list %ul.projects-list.content-list.group-settings-projects
- @projects.each do |project| - @projects.each do |project|
%li %li.project-row{ class: ('no-description' if project.description.blank?) }
.list-item-name .controls
%span{ class: visibility_level_color(project.visibility_level) } = link_to _('Members'), project_project_members_path(project), id: "edit_#{dom_id(project)}", class: "btn"
= visibility_level_icon(project.visibility_level) = link_to _('Edit'), edit_project_path(project), id: "edit_#{dom_id(project)}", class: "btn"
%strong= link_to project.full_name, project = link_to _('Remove'), project, data: { confirm: remove_project_message(project)}, method: :delete, class: "btn btn-remove"
.float-right
.stats
%span.badge.badge-pill
= storage_counter(project.statistics&.storage_size)
- if project.archived - if project.archived
%span.badge.badge-warning archived %span.badge.badge-warning archived
%span.badge.badge-pill
= storage_counter(project.statistics.storage_size) .title
= link_to 'Members', project_project_members_path(project), id: "edit_#{dom_id(project)}", class: "btn btn-sm" = link_to(project_path(project)) do
= link_to 'Edit', edit_project_path(project), id: "edit_#{dom_id(project)}", class: "btn btn-sm" .dash-project-avatar
= link_to 'Remove', project, data: { confirm: remove_project_message(project)}, method: :delete, class: "btn btn-sm btn-remove" .avatar-container.rect-avatar.s40
= project_icon(project, alt: '', class: 'avatar project-avatar s40', width: 40, height: 40)
%span.project-full-name
%span.namespace-name
- if project.namespace
= project.namespace.human_name
\/
%span.project-name
= project.name
%span{ class: visibility_level_color(project.visibility_level) }
= visibility_level_icon(project.visibility_level)
- if project.description.present?
.description
= markdown_field(project, :description)
- if @projects.blank? - if @projects.blank?
.nothing-here-block This group has no projects yet .nothing-here-block This group has no projects yet
......
---
title: Improve UI for admin/projects and group/settings/projects pages
merge_request: 17247
author:
type: changed
---
title: Show the "Set up CI/CD" prompt in empty repositories when applicable.
merge_request: 17274
author: Ben McCormick
type: changed
...@@ -110,7 +110,7 @@ The following table lists available parameters for jobs: ...@@ -110,7 +110,7 @@ The following table lists available parameters for jobs:
| [`dependencies`](#dependencies) | Other jobs that a job depends on so that you can pass artifacts between them. | | [`dependencies`](#dependencies) | Other jobs that a job depends on so that you can pass artifacts between them. |
| [`coverage`](#coverage) | Code coverage settings for a given job. | | [`coverage`](#coverage) | Code coverage settings for a given job. |
| [`retry`](#retry) | When and how many times a job can be auto-retried in case of a failure. | | [`retry`](#retry) | When and how many times a job can be auto-retried in case of a failure. |
| [`timeout`](#timeout) | Define a custom timeout that would take precedence over the project-wide one. | | [`timeout`](#timeout) | Define a custom job-level timeout that takes precedence over the project-wide setting. |
| [`parallel`](#parallel) | How many instances of a job should be run in parallel. | | [`parallel`](#parallel) | How many instances of a job should be run in parallel. |
| [`trigger`](#trigger-premium) | Defines a downstream pipeline trigger. | | [`trigger`](#trigger-premium) | Defines a downstream pipeline trigger. |
| [`include`](#include) | Allows this job to include external YAML files. Also available: `include:local`, `include:file`, `include:template`, and `include:remote`. | | [`include`](#include) | Allows this job to include external YAML files. Also available: `include:local`, `include:file`, `include:template`, and `include:remote`. |
...@@ -1996,9 +1996,11 @@ Possible values for `when` are: ...@@ -1996,9 +1996,11 @@ Possible values for `when` are:
- `missing_dependency_failure`: Retry if a dependency was missing. - `missing_dependency_failure`: Retry if a dependency was missing.
- `runner_unsupported`: Retry if the runner was unsupported. - `runner_unsupported`: Retry if the runner was unsupported.
### timeout ### `timeout`
`timeout` allows you to configure a timeout for a specific job: > [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/14887) in GitLab 12.3.
`timeout` allows you to configure a timeout for a specific job. For example:
```yaml ```yaml
build: build:
...@@ -2129,7 +2131,7 @@ step-1: ...@@ -2129,7 +2131,7 @@ step-1:
stage: stage1 stage: stage1
script: script:
- echo "Can be canceled" - echo "Can be canceled"
step-2: step-2:
stage: stage2 stage: stage2
script: script:
......
...@@ -19,7 +19,7 @@ CE specs should remain untouched as much as possible and extra specs ...@@ -19,7 +19,7 @@ CE specs should remain untouched as much as possible and extra specs
should be added for EE. Licensed features can be stubbed using the should be added for EE. Licensed features can be stubbed using the
spec helper `stub_licensed_features` in `EE::LicenseHelpers`. spec helper `stub_licensed_features` in `EE::LicenseHelpers`.
You can force Webpack to act as CE by either deleting the `ee/` directory or by You can force GitLab to act as CE by either deleting the `ee/` directory or by
setting the [`IS_GITLAB_EE` environment variable](https://gitlab.com/gitlab-org/gitlab/blob/master/config/helpers/is_ee_env.js) setting the [`IS_GITLAB_EE` environment variable](https://gitlab.com/gitlab-org/gitlab/blob/master/config/helpers/is_ee_env.js)
to something that evaluates as `false`. The same works for running tests to something that evaluates as `false`. The same works for running tests
(for example `IS_GITLAB_EE=0 yarn jest`). (for example `IS_GITLAB_EE=0 yarn jest`).
......
...@@ -13,7 +13,7 @@ describe 'Clusterable > Show page' do ...@@ -13,7 +13,7 @@ describe 'Clusterable > Show page' do
sign_in(current_user) sign_in(current_user)
end end
shared_examples 'editing domain' do shared_examples 'show page' do
before do before do
clusterable.add_maintainer(current_user) clusterable.add_maintainer(current_user)
end end
...@@ -53,6 +53,12 @@ describe 'Clusterable > Show page' do ...@@ -53,6 +53,12 @@ describe 'Clusterable > Show page' do
end end
end end
end end
it 'does not show the environments tab' do
visit cluster_path
expect(page).not_to have_selector('.js-cluster-nav-environments', text: 'Environments')
end
end end
shared_examples 'editing a GCP cluster' do shared_examples 'editing a GCP cluster' do
...@@ -113,42 +119,30 @@ describe 'Clusterable > Show page' do ...@@ -113,42 +119,30 @@ describe 'Clusterable > Show page' do
end end
context 'when clusterable is a project' do context 'when clusterable is a project' do
it_behaves_like 'editing domain' do let(:clusterable) { create(:project) }
let(:clusterable) { create(:project) } let(:cluster_path) { project_cluster_path(clusterable, cluster) }
let(:cluster) { create(:cluster, :provided_by_gcp, :project, projects: [clusterable]) } let(:cluster) { create(:cluster, :provided_by_gcp, :project, projects: [clusterable]) }
let(:cluster_path) { project_cluster_path(clusterable, cluster) }
end
it_behaves_like 'editing a GCP cluster' do it_behaves_like 'show page'
let(:clusterable) { create(:project) }
let(:cluster) { create(:cluster, :provided_by_gcp, :project, projects: [clusterable]) } it_behaves_like 'editing a GCP cluster'
let(:cluster_path) { project_cluster_path(clusterable, cluster) }
end
it_behaves_like 'editing a user-provided cluster' do it_behaves_like 'editing a user-provided cluster' do
let(:clusterable) { create(:project) }
let(:cluster) { create(:cluster, :provided_by_user, :project, projects: [clusterable]) } let(:cluster) { create(:cluster, :provided_by_user, :project, projects: [clusterable]) }
let(:cluster_path) { project_cluster_path(clusterable, cluster) }
end end
end end
context 'when clusterable is a group' do context 'when clusterable is a group' do
it_behaves_like 'editing domain' do let(:clusterable) { create(:group) }
let(:clusterable) { create(:group) } let(:cluster_path) { group_cluster_path(clusterable, cluster) }
let(:cluster) { create(:cluster, :provided_by_gcp, :group, groups: [clusterable]) } let(:cluster) { create(:cluster, :provided_by_gcp, :group, groups: [clusterable]) }
let(:cluster_path) { group_cluster_path(clusterable, cluster) }
end
it_behaves_like 'editing a GCP cluster' do it_behaves_like 'show page'
let(:clusterable) { create(:group) }
let(:cluster) { create(:cluster, :provided_by_gcp, :group, groups: [clusterable]) } it_behaves_like 'editing a GCP cluster'
let(:cluster_path) { group_cluster_path(clusterable, cluster) }
end
it_behaves_like 'editing a user-provided cluster' do it_behaves_like 'editing a user-provided cluster' do
let(:clusterable) { create(:group) }
let(:cluster) { create(:cluster, :provided_by_user, :group, groups: [clusterable]) } let(:cluster) { create(:cluster, :provided_by_user, :group, groups: [clusterable]) }
let(:cluster_path) { group_cluster_path(clusterable, cluster) }
end end
end end
end end
import sqljs from 'sql.js'; import sqljs from 'sql.js';
import axios from '~/lib/utils/axios_utils';
import BalsamiqViewer from '~/blob/balsamiq/balsamiq_viewer'; import BalsamiqViewer from '~/blob/balsamiq/balsamiq_viewer';
import ClassSpecHelper from '../../helpers/class_spec_helper'; import ClassSpecHelper from '../../helpers/class_spec_helper';
describe('BalsamiqViewer', () => { describe('BalsamiqViewer', () => {
const mockArrayBuffer = new ArrayBuffer(10);
let balsamiqViewer; let balsamiqViewer;
let viewer; let viewer;
...@@ -19,44 +21,65 @@ describe('BalsamiqViewer', () => { ...@@ -19,44 +21,65 @@ describe('BalsamiqViewer', () => {
}); });
describe('loadFile', () => { describe('loadFile', () => {
let xhr; let bv;
let loadFile;
const endpoint = 'endpoint'; const endpoint = 'endpoint';
const requestSuccess = Promise.resolve({
data: mockArrayBuffer,
status: 200,
});
beforeEach(() => { beforeEach(() => {
xhr = jasmine.createSpyObj('xhr', ['open', 'send']); viewer = {};
bv = new BalsamiqViewer(viewer);
});
balsamiqViewer = jasmine.createSpyObj('balsamiqViewer', ['renderFile']); it('should call `axios.get` on `endpoint` param with responseType set to `arraybuffer', () => {
spyOn(axios, 'get').and.returnValue(requestSuccess);
spyOn(bv, 'renderFile').and.stub();
spyOn(window, 'XMLHttpRequest').and.returnValue(xhr); bv.loadFile(endpoint);
loadFile = BalsamiqViewer.prototype.loadFile.call(balsamiqViewer, endpoint); expect(axios.get).toHaveBeenCalledWith(
endpoint,
jasmine.objectContaining({
responseType: 'arraybuffer',
}),
);
}); });
it('should call .open', () => { it('should call `renderFile` on request success', done => {
expect(xhr.open).toHaveBeenCalledWith('GET', endpoint, true); spyOn(axios, 'get').and.returnValue(requestSuccess);
}); spyOn(bv, 'renderFile').and.callFake(() => {});
it('should set .responseType', () => { bv.loadFile(endpoint)
expect(xhr.responseType).toBe('arraybuffer'); .then(() => {
expect(bv.renderFile).toHaveBeenCalledWith(mockArrayBuffer);
})
.then(done)
.catch(done.fail);
}); });
it('should call .send', () => { it('should not call `renderFile` on request failure', done => {
expect(xhr.send).toHaveBeenCalled(); spyOn(axios, 'get').and.returnValue(Promise.reject());
}); spyOn(bv, 'renderFile');
it('should return a promise', () => { bv.loadFile(endpoint)
expect(loadFile).toEqual(jasmine.any(Promise)); .then(() => {
done.fail('Expected loadFile to throw error!');
})
.catch(() => {
expect(bv.renderFile).not.toHaveBeenCalled();
})
.then(done)
.catch(done.fail);
}); });
}); });
describe('renderFile', () => { describe('renderFile', () => {
let container; let container;
let loadEvent;
let previews; let previews;
beforeEach(() => { beforeEach(() => {
loadEvent = { target: { response: {} } };
viewer = jasmine.createSpyObj('viewer', ['appendChild']); viewer = jasmine.createSpyObj('viewer', ['appendChild']);
previews = [document.createElement('ul'), document.createElement('ul')]; previews = [document.createElement('ul'), document.createElement('ul')];
...@@ -73,11 +96,11 @@ describe('BalsamiqViewer', () => { ...@@ -73,11 +96,11 @@ describe('BalsamiqViewer', () => {
container = containerElement; container = containerElement;
}); });
BalsamiqViewer.prototype.renderFile.call(balsamiqViewer, loadEvent); BalsamiqViewer.prototype.renderFile.call(balsamiqViewer, mockArrayBuffer);
}); });
it('should call .initDatabase', () => { it('should call .initDatabase', () => {
expect(balsamiqViewer.initDatabase).toHaveBeenCalledWith(loadEvent.target.response); expect(balsamiqViewer.initDatabase).toHaveBeenCalledWith(mockArrayBuffer);
}); });
it('should call .getPreviews', () => { it('should call .getPreviews', () => {
......
...@@ -430,4 +430,26 @@ describe ProjectPresenter do ...@@ -430,4 +430,26 @@ describe ProjectPresenter do
) )
end end
end end
describe '#empty_repo_statistics_buttons' do
let(:project) { create(:project, :repository) }
let(:presenter) { described_class.new(project, current_user: user) }
subject(:empty_repo_statistics_buttons) { presenter.empty_repo_statistics_buttons }
before do
project.add_developer(user)
allow(project).to receive(:auto_devops_enabled?).and_return(false)
end
it 'orders the items correctly in an empty project' do
expect(empty_repo_statistics_buttons.map(&:label)).to start_with(
a_string_including('New'),
a_string_including('README'),
a_string_including('CHANGELOG'),
a_string_including('CONTRIBUTING'),
a_string_including('CI/CD')
)
end
end
end end
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