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