diff --git a/app/assets/javascripts/blob/balsamiq/balsamiq_viewer.js b/app/assets/javascripts/blob/balsamiq/balsamiq_viewer.js
index 75777b910ca..87c8568802e 100644
--- a/app/assets/javascripts/blob/balsamiq/balsamiq_viewer.js
+++ b/app/assets/javascripts/blob/balsamiq/balsamiq_viewer.js
@@ -1,5 +1,7 @@
import sqljs from 'sql.js';
import { template as _template } from 'underscore';
+import axios from '~/lib/utils/axios_utils';
+import { successCodes } from '~/lib/utils/http_status';
const PREVIEW_TEMPLATE = _template(`
@@ -16,30 +18,25 @@ class BalsamiqViewer {
}
loadFile(endpoint) {
- return new Promise((resolve, reject) => {
- const xhr = new XMLHttpRequest();
-
- xhr.open('GET', endpoint, true);
- xhr.responseType = 'arraybuffer';
- xhr.onload = loadEvent => this.fileLoaded(loadEvent, resolve, reject);
- xhr.onerror = reject;
-
- xhr.send();
- });
+ return axios
+ .get(endpoint, {
+ responseType: 'arraybuffer',
+ validateStatus(status) {
+ return status !== successCodes.OK;
+ },
+ })
+ .then(({ data }) => {
+ 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');
- this.initDatabase(loadEvent.target.response);
+ this.initDatabase(fileBuffer);
const previews = this.getPreviews();
previews.forEach(preview => {
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 801e9e7204c..7577112cb0e 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -877,11 +877,16 @@ pre.light-well {
flex-direction: column;
// Disable Flexbox for admin page
- &.admin-projects {
+ &.admin-projects,
+ &.group-settings-projects {
display: block;
.project-row {
display: block;
+
+ .description > p {
+ margin-bottom: 0;
+ }
}
}
diff --git a/app/presenters/project_presenter.rb b/app/presenters/project_presenter.rb
index 6c300cd8be1..6d370f6241c 100644
--- a/app/presenters/project_presenter.rb
+++ b/app/presenters/project_presenter.rb
@@ -51,7 +51,8 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
new_file_anchor_data,
readme_anchor_data,
changelog_anchor_data,
- contribution_guide_anchor_data
+ contribution_guide_anchor_data,
+ gitlab_ci_anchor_data
].compact.reject { |item| item.is_link }
end
diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml
index bb0d62a70c0..08e668e8623 100644
--- a/app/views/admin/projects/index.html.haml
+++ b/app/views/admin/projects/index.html.haml
@@ -1,8 +1,20 @@
-- page_title "Projects"
+- page_title _('Projects')
- params[:visibility_level] ||= []
.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
= render 'shared/projects/search_form', autofocus: true, admin_view: true
.dropdown
@@ -22,20 +34,4 @@
New Project
= 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'
diff --git a/app/views/clusters/clusters/show.html.haml b/app/views/clusters/clusters/show.html.haml
index 00fdd5e9562..cccba48624b 100644
--- a/app/views/clusters/clusters/show.html.haml
+++ b/app/views/clusters/clusters/show.html.haml
@@ -3,6 +3,7 @@
- breadcrumb_title @cluster.name
- page_title _('Kubernetes Cluster')
- manage_prometheus_path = edit_project_service_path(@cluster.project, 'prometheus') if @project
+- cluster_environments_path = clusterable.environments_cluster_path(@cluster)
- expanded = expanded_by_default?
@@ -16,7 +17,7 @@
install_jupyter_path: clusterable.install_applications_cluster_path(@cluster, :jupyter),
install_knative_path: clusterable.install_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',
has_rbac: has_rbac_enabled?(@cluster) ? 'true': 'false',
cluster_type: @cluster.cluster_type,
@@ -37,7 +38,7 @@
%h4= @cluster.name
= render 'banner'
- = render_if_exists 'clusters/clusters/group_cluster_environments', expanded: expanded
-
- - unless Gitlab.ee?
+ - if cluster_environments_path.present?
+ = render_if_exists 'clusters/clusters/group_cluster_environments', expanded: expanded
+ - else
= render 'configure', expanded: expanded
diff --git a/app/views/groups/projects.html.haml b/app/views/groups/projects.html.haml
index ba186875a86..8b01e54474a 100644
--- a/app/views/groups/projects.html.haml
+++ b/app/views/groups/projects.html.haml
@@ -8,21 +8,38 @@
.controls
= link_to new_project_path(namespace_id: @group.id), class: "btn btn-sm btn-success" do
New project
- %ul.content-list
+ %ul.projects-list.content-list.group-settings-projects
- @projects.each do |project|
- %li
- .list-item-name
- %span{ class: visibility_level_color(project.visibility_level) }
- = visibility_level_icon(project.visibility_level)
- %strong= link_to project.full_name, project
- .float-right
+ %li.project-row{ class: ('no-description' if project.description.blank?) }
+ .controls
+ = link_to _('Members'), project_project_members_path(project), id: "edit_#{dom_id(project)}", class: "btn"
+ = link_to _('Edit'), edit_project_path(project), id: "edit_#{dom_id(project)}", class: "btn"
+ = link_to _('Remove'), project, data: { confirm: remove_project_message(project)}, method: :delete, class: "btn btn-remove"
+
+ .stats
+ %span.badge.badge-pill
+ = storage_counter(project.statistics&.storage_size)
- if project.archived
%span.badge.badge-warning archived
- %span.badge.badge-pill
- = storage_counter(project.statistics.storage_size)
- = link_to 'Members', project_project_members_path(project), id: "edit_#{dom_id(project)}", class: "btn btn-sm"
- = link_to 'Edit', edit_project_path(project), id: "edit_#{dom_id(project)}", class: "btn btn-sm"
- = link_to 'Remove', project, data: { confirm: remove_project_message(project)}, method: :delete, class: "btn btn-sm btn-remove"
+
+ .title
+ = link_to(project_path(project)) do
+ .dash-project-avatar
+ .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?
.nothing-here-block This group has no projects yet
diff --git a/changelogs/unreleased/dz-fix-group-settings-projects-page.yml b/changelogs/unreleased/dz-fix-group-settings-projects-page.yml
new file mode 100644
index 00000000000..9d5b93f9a50
--- /dev/null
+++ b/changelogs/unreleased/dz-fix-group-settings-projects-page.yml
@@ -0,0 +1,5 @@
+---
+title: Improve UI for admin/projects and group/settings/projects pages
+merge_request: 17247
+author:
+type: changed
diff --git a/changelogs/unreleased/mc-feature-show-setup-cicd-empty-repo.yml b/changelogs/unreleased/mc-feature-show-setup-cicd-empty-repo.yml
new file mode 100644
index 00000000000..3165159fdab
--- /dev/null
+++ b/changelogs/unreleased/mc-feature-show-setup-cicd-empty-repo.yml
@@ -0,0 +1,5 @@
+---
+title: Show the "Set up CI/CD" prompt in empty repositories when applicable.
+merge_request: 17274
+author: Ben McCormick
+type: changed
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 56dcc15527b..4d0bdf40b28 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -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. |
| [`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. |
-| [`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. |
| [`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`. |
@@ -1996,9 +1996,11 @@ Possible values for `when` are:
- `missing_dependency_failure`: Retry if a dependency was missing.
- `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
build:
@@ -2129,7 +2131,7 @@ step-1:
stage: stage1
script:
- echo "Can be canceled"
-
+
step-2:
stage: stage2
script:
diff --git a/doc/development/ee_features.md b/doc/development/ee_features.md
index 6de6497c8c4..22dd9be073f 100644
--- a/doc/development/ee_features.md
+++ b/doc/development/ee_features.md
@@ -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
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)
to something that evaluates as `false`. The same works for running tests
(for example `IS_GITLAB_EE=0 yarn jest`).
diff --git a/spec/features/clusters/cluster_detail_page_spec.rb b/spec/features/clusters/cluster_detail_page_spec.rb
index 683c57a97f8..927862689c1 100644
--- a/spec/features/clusters/cluster_detail_page_spec.rb
+++ b/spec/features/clusters/cluster_detail_page_spec.rb
@@ -13,7 +13,7 @@ describe 'Clusterable > Show page' do
sign_in(current_user)
end
- shared_examples 'editing domain' do
+ shared_examples 'show page' do
before do
clusterable.add_maintainer(current_user)
end
@@ -53,6 +53,12 @@ describe 'Clusterable > Show page' do
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
shared_examples 'editing a GCP cluster' do
@@ -113,42 +119,30 @@ describe 'Clusterable > Show page' do
end
context 'when clusterable is a project' do
- it_behaves_like 'editing domain' do
- let(:clusterable) { create(:project) }
- let(:cluster) { create(:cluster, :provided_by_gcp, :project, projects: [clusterable]) }
- let(:cluster_path) { project_cluster_path(clusterable, cluster) }
- end
+ let(:clusterable) { create(:project) }
+ let(:cluster_path) { project_cluster_path(clusterable, cluster) }
+ let(:cluster) { create(:cluster, :provided_by_gcp, :project, projects: [clusterable]) }
- it_behaves_like 'editing a GCP cluster' do
- let(:clusterable) { create(:project) }
- let(:cluster) { create(:cluster, :provided_by_gcp, :project, projects: [clusterable]) }
- let(:cluster_path) { project_cluster_path(clusterable, cluster) }
- end
+ it_behaves_like 'show page'
+
+ it_behaves_like 'editing a GCP cluster'
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_path) { project_cluster_path(clusterable, cluster) }
end
end
context 'when clusterable is a group' do
- it_behaves_like 'editing domain' do
- let(:clusterable) { create(:group) }
- let(:cluster) { create(:cluster, :provided_by_gcp, :group, groups: [clusterable]) }
- let(:cluster_path) { group_cluster_path(clusterable, cluster) }
- end
+ let(:clusterable) { create(:group) }
+ let(:cluster_path) { group_cluster_path(clusterable, cluster) }
+ let(:cluster) { create(:cluster, :provided_by_gcp, :group, groups: [clusterable]) }
- it_behaves_like 'editing a GCP cluster' do
- let(:clusterable) { create(:group) }
- let(:cluster) { create(:cluster, :provided_by_gcp, :group, groups: [clusterable]) }
- let(:cluster_path) { group_cluster_path(clusterable, cluster) }
- end
+ it_behaves_like 'show page'
+
+ it_behaves_like 'editing a GCP cluster'
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_path) { group_cluster_path(clusterable, cluster) }
end
end
end
diff --git a/spec/javascripts/blob/balsamiq/balsamiq_viewer_spec.js b/spec/javascripts/blob/balsamiq/balsamiq_viewer_spec.js
index fd73fb4bfcc..d175c8ba853 100644
--- a/spec/javascripts/blob/balsamiq/balsamiq_viewer_spec.js
+++ b/spec/javascripts/blob/balsamiq/balsamiq_viewer_spec.js
@@ -1,8 +1,10 @@
import sqljs from 'sql.js';
+import axios from '~/lib/utils/axios_utils';
import BalsamiqViewer from '~/blob/balsamiq/balsamiq_viewer';
import ClassSpecHelper from '../../helpers/class_spec_helper';
describe('BalsamiqViewer', () => {
+ const mockArrayBuffer = new ArrayBuffer(10);
let balsamiqViewer;
let viewer;
@@ -19,44 +21,65 @@ describe('BalsamiqViewer', () => {
});
describe('loadFile', () => {
- let xhr;
- let loadFile;
+ let bv;
const endpoint = 'endpoint';
+ const requestSuccess = Promise.resolve({
+ data: mockArrayBuffer,
+ status: 200,
+ });
beforeEach(() => {
- xhr = jasmine.createSpyObj('xhr', ['open', 'send']);
-
- balsamiqViewer = jasmine.createSpyObj('balsamiqViewer', ['renderFile']);
-
- spyOn(window, 'XMLHttpRequest').and.returnValue(xhr);
-
- loadFile = BalsamiqViewer.prototype.loadFile.call(balsamiqViewer, endpoint);
+ viewer = {};
+ bv = new BalsamiqViewer(viewer);
});
- it('should call .open', () => {
- expect(xhr.open).toHaveBeenCalledWith('GET', endpoint, true);
+ it('should call `axios.get` on `endpoint` param with responseType set to `arraybuffer', () => {
+ spyOn(axios, 'get').and.returnValue(requestSuccess);
+ spyOn(bv, 'renderFile').and.stub();
+
+ bv.loadFile(endpoint);
+
+ expect(axios.get).toHaveBeenCalledWith(
+ endpoint,
+ jasmine.objectContaining({
+ responseType: 'arraybuffer',
+ }),
+ );
});
- it('should set .responseType', () => {
- expect(xhr.responseType).toBe('arraybuffer');
+ it('should call `renderFile` on request success', done => {
+ spyOn(axios, 'get').and.returnValue(requestSuccess);
+ spyOn(bv, 'renderFile').and.callFake(() => {});
+
+ bv.loadFile(endpoint)
+ .then(() => {
+ expect(bv.renderFile).toHaveBeenCalledWith(mockArrayBuffer);
+ })
+ .then(done)
+ .catch(done.fail);
});
- it('should call .send', () => {
- expect(xhr.send).toHaveBeenCalled();
- });
+ it('should not call `renderFile` on request failure', done => {
+ spyOn(axios, 'get').and.returnValue(Promise.reject());
+ spyOn(bv, 'renderFile');
- it('should return a promise', () => {
- expect(loadFile).toEqual(jasmine.any(Promise));
+ bv.loadFile(endpoint)
+ .then(() => {
+ done.fail('Expected loadFile to throw error!');
+ })
+ .catch(() => {
+ expect(bv.renderFile).not.toHaveBeenCalled();
+ })
+ .then(done)
+ .catch(done.fail);
});
});
describe('renderFile', () => {
let container;
- let loadEvent;
let previews;
beforeEach(() => {
- loadEvent = { target: { response: {} } };
viewer = jasmine.createSpyObj('viewer', ['appendChild']);
previews = [document.createElement('ul'), document.createElement('ul')];
@@ -73,11 +96,11 @@ describe('BalsamiqViewer', () => {
container = containerElement;
});
- BalsamiqViewer.prototype.renderFile.call(balsamiqViewer, loadEvent);
+ BalsamiqViewer.prototype.renderFile.call(balsamiqViewer, mockArrayBuffer);
});
it('should call .initDatabase', () => {
- expect(balsamiqViewer.initDatabase).toHaveBeenCalledWith(loadEvent.target.response);
+ expect(balsamiqViewer.initDatabase).toHaveBeenCalledWith(mockArrayBuffer);
});
it('should call .getPreviews', () => {
diff --git a/spec/presenters/project_presenter_spec.rb b/spec/presenters/project_presenter_spec.rb
index 5bf80f6e318..2a00548c2c3 100644
--- a/spec/presenters/project_presenter_spec.rb
+++ b/spec/presenters/project_presenter_spec.rb
@@ -430,4 +430,26 @@ describe ProjectPresenter do
)
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