Commit 17f28c55 authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab-ce master

parents 4ea971db 0e6e924b
<script>
import { GlLoadingIcon } from '@gitlab/ui';
import { visitUrl } from '../../lib/utils/url_utility';
import tooltip from '../../vue_shared/directives/tooltip';
import identicon from '../../vue_shared/components/identicon.vue';
import eventHub from '../event_hub';
import { VISIBILITY_TYPE_ICON, GROUP_VISIBILITY_TYPE } from '../constants';
import itemCaret from './item_caret.vue';
import itemTypeIcon from './item_type_icon.vue';
import itemStats from './item_stats.vue';
import itemStatsValue from './item_stats_value.vue';
import itemActions from './item_actions.vue';
export default {
......@@ -14,10 +17,12 @@ export default {
tooltip,
},
components: {
GlLoadingIcon,
identicon,
itemCaret,
itemTypeIcon,
itemStats,
itemStatsValue,
itemActions,
},
props: {
......@@ -57,6 +62,12 @@ export default {
isGroup() {
return this.group.type === 'group';
},
visibilityIcon() {
return VISIBILITY_TYPE_ICON[this.group.visibility];
},
visibilityTooltip() {
return GROUP_VISIBILITY_TYPE[this.group.visibility];
},
},
methods: {
onClickRowGroup(e) {
......@@ -80,43 +91,61 @@ export default {
<li :id="groupDomId" :class="rowClass" class="group-row" @click.stop="onClickRowGroup">
<div
:class="{ 'project-row-contents': !isGroup }"
class="group-row-contents d-flex justify-content-end align-items-center"
class="group-row-contents d-flex align-items-center"
>
<div class="folder-toggle-wrap append-right-4 d-flex align-items-center">
<item-caret :is-group-open="group.isOpen" />
<item-type-icon :item-type="group.type" :is-group-open="group.isOpen" />
</div>
<gl-loading-icon
v-if="group.isChildrenLoading"
size="md"
class="d-none d-sm-inline-flex flex-shrink-0 append-right-10"
/>
<div
:class="{ 'content-loading': group.isChildrenLoading }"
class="avatar-container rect-avatar s24 d-none d-sm-flex"
:class="{ 'd-sm-flex': !group.isChildrenLoading }"
class="avatar-container rect-avatar s32 d-none flex-grow-0 flex-shrink-0 "
>
<a :href="group.relativePath" class="no-expand">
<img v-if="hasAvatar" :src="group.avatarUrl" class="avatar s24" />
<identicon v-else :entity-id="group.id" :entity-name="group.name" size-class="s24" />
<img v-if="hasAvatar" :src="group.avatarUrl" class="avatar s32" />
<identicon v-else :entity-id="group.id" :entity-name="group.name" size-class="s32" />
</a>
</div>
<div class="group-text flex-grow">
<div class="title namespace-title append-right-8">
<a
v-tooltip
:href="group.relativePath"
:title="group.fullName"
class="no-expand"
data-placement="bottom"
>{{
// ending bracket must be by closing tag to prevent
// link hover text-decoration from over-extending
group.name
}}</a
>
<span v-if="group.permission" class="user-access-role"> {{ group.permission }} </span>
<div class="group-text-container d-flex flex-fill align-items-center">
<div class="group-text flex-grow-1 flex-shrink-1">
<div class="d-flex align-items-center flex-wrap title namespace-title append-right-8">
<a
v-tooltip
:href="group.relativePath"
:title="group.fullName"
class="no-expand prepend-top-8 append-right-8"
data-placement="bottom"
>{{
// ending bracket must be by closing tag to prevent
// link hover text-decoration from over-extending
group.name
}}</a
>
<item-stats-value
:icon-name="visibilityIcon"
:title="visibilityTooltip"
css-class="item-visibility d-inline-flex align-items-center prepend-top-8 append-right-4"
/>
<span v-if="group.permission" class="user-access-role prepend-top-8">
{{ group.permission }}
</span>
</div>
<div v-if="group.description" class="description">
<span v-html="group.description"> </span>
</div>
</div>
<div v-if="group.description" class="description">
<span v-html="group.description"> </span>
<div
class="metadata align-items-md-center d-flex flex-grow-1 flex-shrink-0 flex-wrap justify-content-md-between"
>
<item-actions v-if="isGroup" :group="group" :parent-group="parentGroup" />
<item-stats :item="group" class="group-stats prepend-top-2 d-none d-md-flex" />
</div>
</div>
<item-stats :item="group" class="group-stats prepend-top-2" />
<item-actions v-if="isGroup" :group="group" :parent-group="parentGroup" />
</div>
<group-folder
v-if="group.isOpen && hasChildren"
......
......@@ -44,31 +44,31 @@ export default {
</script>
<template>
<div class="controls">
<div class="controls d-flex justify-content-end">
<a
v-if="group.canEdit"
v-if="group.canLeave"
v-tooltip
:href="group.editPath"
:title="editBtnTitle"
:aria-label="editBtnTitle"
:href="group.leavePath"
:title="leaveBtnTitle"
:aria-label="leaveBtnTitle"
data-container="body"
data-placement="bottom"
class="edit-group btn no-expand"
class="leave-group btn btn-xs no-expand"
@click.prevent="onLeaveGroup"
>
<icon name="settings" />
<icon name="leave" css-classes="position-top-0" />
</a>
<a
v-if="group.canLeave"
v-if="group.canEdit"
v-tooltip
:href="group.leavePath"
:title="leaveBtnTitle"
:aria-label="leaveBtnTitle"
:href="group.editPath"
:title="editBtnTitle"
:aria-label="editBtnTitle"
data-container="body"
data-placement="bottom"
class="leave-group btn no-expand"
@click.prevent="onLeaveGroup"
class="edit-group btn btn-xs no-expand"
>
<icon name="leave" />
<icon name="settings" css-classes="position-top-0" />
</a>
</div>
</template>
......@@ -21,5 +21,5 @@ export default {
</script>
<template>
<span class="folder-caret"> <icon :size="12" :name="iconClass" /> </span>
<span class="folder-caret append-right-4"> <icon :size="10" :name="iconClass" /> </span>
</template>
......@@ -48,7 +48,7 @@ export default {
:title="__('Subgroups')"
:value="item.subgroupCount"
css-class="number-subgroups"
icon-name="folder"
icon-name="folder-o"
/>
<item-stats-value
v-if="isGroup"
......@@ -70,12 +70,6 @@ export default {
css-class="project-stars"
icon-name="star"
/>
<item-stats-value
:icon-name="visibilityIcon"
:title="visibilityTooltip"
css-class="item-visibility"
tooltip-placement="left"
/>
<div v-if="isProject" class="last-updated">
<time-ago-tooltip :time="item.updatedAt" tooltip-placement="bottom" />
</div>
......
......@@ -20,7 +20,7 @@ export default {
computed: {
iconClass() {
if (this.itemType === ITEM_TYPE.GROUP) {
return this.isGroupOpen ? 'folder-open' : 'folder';
return this.isGroupOpen ? 'folder-open' : 'folder-o';
}
return 'bookmark';
},
......
......@@ -133,7 +133,6 @@ ul.content-list {
.description {
@include str-truncated;
color: $gl-text-color-secondary;
}
.controls {
......
......@@ -65,7 +65,7 @@
.stats {
float: right;
line-height: $list-text-height;
color: $gl-text-color;
color: $gl-text-color-secondary;
span {
margin-right: 15px;
......
......@@ -168,12 +168,6 @@
}
}
.groups-listing {
.group-list-tree .group-row:first-child {
border-top: 0;
}
}
.card {
.shared_runners_limit_under_quota {
color: $green-500;
......@@ -260,7 +254,6 @@ table.pipeline-project-metrics tr td {
color: $gl-text-color-secondary;
font-size: 12px;
line-height: 20px;
margin: -5px 3px;
padding: 0 $label-padding;
border: 1px solid $border-color;
border-radius: $label-border-radius;
......@@ -294,39 +287,6 @@ table.pipeline-project-metrics tr td {
}
.group-list-tree {
.avatar-container.content-loading {
position: relative;
> a,
> a .avatar {
height: 100%;
border-radius: 50%;
}
> a {
padding: 2px;
.avatar {
border: 2px solid $white-normal;
&.identicon {
line-height: 15px;
}
}
}
&::after {
content: '';
position: absolute;
height: 100%;
width: 100%;
background-color: transparent;
border: 2px outset $gl-gray-200;
border-radius: 50%;
animation: spin-avatar 3s infinite linear;
}
}
.folder-toggle-wrap {
font-size: 0;
flex-shrink: 0;
......@@ -339,13 +299,14 @@ table.pipeline-project-metrics tr td {
.folder-caret,
.item-type-icon {
display: inline-block;
color: $gl-text-color-secondary;
}
.folder-caret {
width: 15px;
width: $gl-font-size-large;
svg {
margin-bottom: 1px;
margin-bottom: 2px;
}
}
......@@ -420,7 +381,7 @@ table.pipeline-project-metrics tr td {
}
.group-row-contents {
padding: $gl-padding-top;
padding: $gl-padding;
&:hover {
border-color: $blue-200;
......@@ -428,10 +389,15 @@ table.pipeline-project-metrics tr td {
cursor: pointer;
}
.group-text-container,
.group-text {
min-width: 0; // allows for truncated text within flex children
}
.group-text {
flex-basis: 100%;
}
.avatar-container {
flex-shrink: 0;
......@@ -441,6 +407,21 @@ table.pipeline-project-metrics tr td {
}
}
.title {
margin-top: -$gl-padding-8; // negative margin required for flex-wrap
font-size: $gl-font-size-large;
}
.item-visibility {
color: $gl-text-color-secondary;
}
@include media-breakpoint-down(md) {
.title {
font-size: $gl-font-size;
}
}
&.has-more-items {
display: block;
padding: 20px 10px;
......@@ -477,17 +458,18 @@ table.pipeline-project-metrics tr td {
}
.controls {
flex-shrink: 0;
flex-basis: 90px;
> .btn {
margin: 0 0 0 $btn-margin-5;
margin: 0 $btn-side-margin 0 0;
color: $gl-text-color-secondary;
}
}
}
@include media-breakpoint-down(xs) {
.group-stats {
display: none;
.metadata {
@include media-breakpoint-up(md) {
flex-basis: 240px;
}
}
}
......
......@@ -889,7 +889,6 @@ pre.light-well {
@include basic-list-stats;
display: flex;
align-items: center;
color: $gl-text-color-secondary;
padding: $gl-padding 0;
@include media-breakpoint-up(lg) {
......@@ -952,10 +951,6 @@ pre.light-well {
.description {
line-height: 1.5;
@include media-breakpoint-up(md) {
color: $gl-text-color;
}
}
@include media-breakpoint-down(md) {
......
......@@ -47,6 +47,19 @@ class EnvironmentsFinder
end
# rubocop: enable CodeReuse/ActiveRecord
# This method will eventually take the place of `#execute` as an
# efficient way to get relevant environment entries.
# Currently, `#execute` method has a serious technical debt and
# we will likely rework on it in the future.
# See more https://gitlab.com/gitlab-org/gitlab-ce/issues/63381
def find
environments = project.environments
environments = by_name(environments)
environments = by_search(environments)
environments
end
private
def ref
......@@ -56,4 +69,20 @@ class EnvironmentsFinder
def commit
params[:commit]
end
def by_name(environments)
if params[:name].present?
environments.for_name(params[:name])
else
environments
end
end
def by_search(environments)
if params[:search].present?
environments.for_name_like(params[:search], limit: nil)
else
environments
end
end
end
- project = find_project_for_result_blob(projects, wiki_blob)
- wiki_blob = parse_search_result(wiki_blob)
- wiki_blob_link = project_wiki_path(project, Pathname.new(wiki_blob.filename).sub_ext(''))
- wiki_blob_link = project_wiki_path(project, wiki_blob.basename)
= render partial: 'search/results/blob_data', locals: { blob: wiki_blob, project: project, file_name: wiki_blob.filename, blob_link: wiki_blob_link }
---
title: Add `name` and `search` parameters to project environments API
merge_request: 29385
author: Lee Tickett
type: added
---
title: Improve group list UI
merge_request: 26542
author:
type: changed
---
title: Build correct basenames for title search results
merge_request: 29898
author:
type: fixed
......@@ -11,9 +11,11 @@ GET /projects/:id/environments
| Attribute | Type | Required | Description |
| --------- | ------- | -------- | --------------------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `name` | string | no | Return the environment with this name. Mutually exclusive with `search` |
| `search` | string | no | Return list of environments matching the search criteria. Mutually exclusive with `name` |
```bash
curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/1/environments
curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/1/environments?name=review%2Ffix-foo
```
Example response:
......
......@@ -18,11 +18,16 @@ module API
end
params do
use :pagination
optional :name, type: String, desc: 'Returns the environment with this name'
optional :search, type: String, desc: 'Returns list of environments matching the search criteria'
mutually_exclusive :name, :search, message: 'cannot be used together'
end
get ':id/environments' do
authorize! :read_environment, user_project
present paginate(user_project.environments), with: Entities::Environment, current_user: current_user
environments = ::EnvironmentsFinder.new(user_project, current_user, params).find
present paginate(environments), with: Entities::Environment, current_user: current_user
end
desc 'Creates a new environment' do
......
......@@ -93,7 +93,7 @@ module Gitlab
data = {
id: blob.id,
binary_filename: blob.path,
binary_basename: File.basename(blob.path, File.extname(blob.path)),
binary_basename: path_without_extension(blob.path),
ref: ref,
startline: 1,
binary_data: blob.data,
......@@ -111,6 +111,10 @@ module Gitlab
content_match.match(FILENAME_REGEXP) { |matches| matches[:filename] }
end
def path_without_extension(path)
Pathname.new(path).sub_ext('').to_s
end
def parsed_content
strong_memoize(:parsed_content) do
if content_match
......@@ -137,8 +141,7 @@ module Gitlab
filename = matches[:filename]
startline = matches[:startline]
startline = startline.to_i - index
extname = Regexp.escape(File.extname(filename))
basename = filename.sub(/#{extname}$/, '')
basename = path_without_extension(filename)
end
data << line.sub(prefix.to_s, '')
......
......@@ -3,7 +3,7 @@ require 'spec_helper'
describe 'User searches for wiki pages', :js do
let(:user) { create(:user) }
let(:project) { create(:project, :repository, :wiki_repo, namespace: user.namespace) }
let!(:wiki_page) { create(:wiki_page, wiki: project.wiki, attrs: { title: 'test_wiki', content: 'Some Wiki content' }) }
let!(:wiki_page) { create(:wiki_page, wiki: project.wiki, attrs: { title: 'directory/title', content: 'Some Wiki content' }) }
before do
project.add_maintainer(user)
......@@ -22,7 +22,7 @@ describe 'User searches for wiki pages', :js do
click_link(project.full_name)
end
fill_in('dashboard_search', with: 'content')
fill_in('dashboard_search', with: search_term)
find('.btn-search').click
page.within('.search-filter') do
......@@ -43,7 +43,7 @@ describe 'User searches for wiki pages', :js do
context 'when searching by title' do
it_behaves_like 'search wiki blobs' do
let(:search_term) { 'test_wiki' }
let(:search_term) { 'title' }
end
end
end
......@@ -156,6 +156,8 @@ describe('GroupItemComponent', () => {
describe('template', () => {
it('should render component template correctly', () => {
const visibilityIconEl = vm.$el.querySelector('.item-visibility');
expect(vm.$el.getAttribute('id')).toBe('group-55');
expect(vm.$el.classList.contains('group-row')).toBeTruthy();
......@@ -173,6 +175,11 @@ describe('GroupItemComponent', () => {
expect(vm.$el.querySelector('.title')).toBeDefined();
expect(vm.$el.querySelector('.title a.no-expand')).toBeDefined();
expect(visibilityIconEl).not.toBe(null);
expect(visibilityIconEl.dataset.originalTitle).toBe(vm.visibilityTooltip);
expect(visibilityIconEl.querySelectorAll('svg').length).toBeGreaterThan(0);
expect(vm.$el.querySelector('.access-type')).toBeDefined();
expect(vm.$el.querySelector('.description')).toBeDefined();
......
......@@ -108,18 +108,6 @@ describe('ItemStatsComponent', () => {
vm.$destroy();
});
it('renders item visibility icon and tooltip correctly', () => {
const vm = createComponent();
const visibilityIconEl = vm.$el.querySelector('.item-visibility');
expect(visibilityIconEl).not.toBe(null);
expect(visibilityIconEl.dataset.originalTitle).toBe(vm.visibilityTooltip);
expect(visibilityIconEl.querySelectorAll('svg').length).toBeGreaterThan(0);
vm.$destroy();
});
it('renders start count and last updated information for project item correctly', () => {
const item = Object.assign({}, mockParentGroupItem, {
type: ITEM_TYPE.PROJECT,
......
......@@ -153,76 +153,72 @@ describe Gitlab::Ci::Pipeline::Seed::Build do
end
end
context 'when keywords and pipeline source policy matches' do
possibilities = [%w[pushes push],
%w[web web],
%w[triggers trigger],
%w[schedules schedule],
%w[api api],
%w[external external]]
context 'when using only' do
possibilities.each do |keyword, source|
context "when using keyword `#{keyword}` and source `#{source}`" do
let(:pipeline) do
build(:ci_empty_pipeline, ref: 'deploy', tag: false, source: source)
end
context 'with source-keyword policy' do
using RSpec::Parameterized
let(:pipeline) { build(:ci_empty_pipeline, ref: 'deploy', tag: false, source: source) }
context 'matches' do
where(:keyword, :source) do
[
%w(pushes push),
%w(web web),
%w(triggers trigger),
%w(schedules schedule),
%w(api api),
%w(external external)
]
end
with_them do
context 'using an only policy' do
let(:attributes) { { name: 'rspec', only: { refs: [keyword] } } }
it { is_expected.to be_included }
end
end
end
context 'when using except' do
possibilities.each do |keyword, source|
context "when using keyword `#{keyword}` and source `#{source}`" do
let(:pipeline) do
build(:ci_empty_pipeline, ref: 'deploy', tag: false, source: source)
end
context 'using an except policy' do
let(:attributes) { { name: 'rspec', except: { refs: [keyword] } } }
it { is_expected.not_to be_included }
end
context 'using both only and except policies' do
let(:attributes) { { name: 'rspec', only: { refs: [keyword] }, except: { refs: [keyword] } } }
it { is_expected.not_to be_included }
end
end
end
end
context 'when keywords and pipeline source does not match' do
possibilities = [%w[pushes web],
%w[web push],
%w[triggers schedule],
%w[schedules external],
%w[api trigger],
%w[external api]]
context 'when using only' do
possibilities.each do |keyword, source|
context "when using keyword `#{keyword}` and source `#{source}`" do
let(:pipeline) do
build(:ci_empty_pipeline, ref: 'deploy', tag: false, source: source)
end
context 'non-matches' do
where(:keyword, :source) do
%w(web trigger schedule api external).map { |source| ['pushes', source] } +
%w(push trigger schedule api external).map { |source| ['web', source] } +
%w(push web schedule api external).map { |source| ['triggers', source] } +
%w(push web trigger api external).map { |source| ['schedules', source] } +
%w(push web trigger schedule external).map { |source| ['api', source] } +
%w(push web trigger schedule api).map { |source| ['external', source] }
end
with_them do
context 'using an only policy' do
let(:attributes) { { name: 'rspec', only: { refs: [keyword] } } }
it { is_expected.not_to be_included }
end
end
end
context 'when using except' do
possibilities.each do |keyword, source|
context "when using keyword `#{keyword}` and source `#{source}`" do
let(:pipeline) do
build(:ci_empty_pipeline, ref: 'deploy', tag: false, source: source)
end
context 'using an except policy' do
let(:attributes) { { name: 'rspec', except: { refs: [keyword] } } }
it { is_expected.to be_included }
end
context 'using both only and except policies' do
let(:attributes) { { name: 'rspec', only: { refs: [keyword] }, except: { refs: [keyword] } } }
it { is_expected.not_to be_included }
end
end
end
end
......
......@@ -3,14 +3,15 @@
require 'spec_helper'
describe Gitlab::Search::FoundBlob do
describe 'parsing results' do
let(:project) { create(:project, :public, :repository) }
let(:project) { create(:project, :public, :repository) }
describe 'parsing content results' do
let(:results) { project.repository.search_files_by_content('feature', 'master') }
let(:search_result) { results.first }
subject { described_class.new(content_match: search_result, project: project) }
it "returns a valid FoundBlob" do
it 'returns a valid FoundBlob' do
is_expected.to be_an described_class
expect(subject.id).to be_nil
expect(subject.path).to eq('CHANGELOG')
......@@ -21,13 +22,13 @@ describe Gitlab::Search::FoundBlob do
expect(subject.data.lines[2]).to eq(" - Feature: Replace teams with group membership\n")
end
it "doesn't parses content if not needed" do
it 'does not parse content if not needed' do
expect(subject).not_to receive(:parse_search_result)
expect(subject.project_id).to eq(project.id)
expect(subject.binary_filename).to eq('CHANGELOG')
end
it "parses content only once when needed" do
it 'parses content only once when needed' do
expect(subject).to receive(:parse_search_result).once.and_call_original
expect(subject.filename).to eq('CHANGELOG')
expect(subject.startline).to eq(188)
......@@ -119,7 +120,7 @@ describe Gitlab::Search::FoundBlob do
end
end
context "when filename has extension" do
context 'when filename has extension' do
let(:search_result) { "master:CONTRIBUTE.md\x005\x00- [Contribute to GitLab](#contribute-to-gitlab)\n" }
it { expect(subject.path).to eq('CONTRIBUTE.md') }
......@@ -127,7 +128,7 @@ describe Gitlab::Search::FoundBlob do
it { expect(subject.basename).to eq('CONTRIBUTE') }
end
context "when file under directory" do
context 'when file is under directory' do
let(:search_result) { "master:a/b/c.md\x005\x00a b c\n" }
it { expect(subject.path).to eq('a/b/c.md') }
......@@ -135,4 +136,28 @@ describe Gitlab::Search::FoundBlob do
it { expect(subject.basename).to eq('a/b/c') }
end
end
describe 'parsing title results' do
context 'when file is under directory' do
let(:path) { 'a/b/c.md' }
subject { described_class.new(blob_filename: path, project: project, ref: 'master') }
before do
allow(Gitlab::Git::Blob).to receive(:batch).and_return([
Gitlab::Git::Blob.new(path: path)
])
end
it { expect(subject.path).to eq('a/b/c.md') }
it { expect(subject.filename).to eq('a/b/c.md') }
it { expect(subject.basename).to eq('a/b/c') }
context 'when filename has multiple extensions' do
let(:path) { 'a/b/c.whatever.md' }
it { expect(subject.basename).to eq('a/b/c.whatever') }
end
end
end
end
......@@ -34,6 +34,47 @@ describe API::Environments do
expect(json_response.first['project'].keys).to contain_exactly(*project_data_keys)
expect(json_response.first).not_to have_key("last_deployment")
end
context 'when filtering' do
let!(:environment2) { create(:environment, project: project) }
it 'returns environment by name' do
get api("/projects/#{project.id}/environments?name=#{environment.name}", user)
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.size).to eq(1)
expect(json_response.first['name']).to eq(environment.name)
end
it 'returns no environment by non-existent name' do
get api("/projects/#{project.id}/environments?name=test", user)
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.size).to eq(0)
end
it 'returns environments by name_like' do
get api("/projects/#{project.id}/environments?search=envir", user)
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.size).to eq(2)
end
it 'returns no environment by non-existent name_like' do
get api("/projects/#{project.id}/environments?search=test", user)
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.size).to eq(0)
end
end
end
context 'as non member' do
......
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