Skip to content

Commit 598b0e9

Browse files
author
GitLab Bot
committed
Add latest changes from gitlab-org/gitlab@master
1 parent 0a692e1 commit 598b0e9

File tree

14 files changed

+509
-203
lines changed

14 files changed

+509
-203
lines changed

app/assets/javascripts/analytics/shared/components/projects_dropdown_filter.vue

Lines changed: 81 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,5 @@
11
<script>
2-
import {
3-
GlIcon,
4-
GlLoadingIcon,
5-
GlAvatar,
6-
GlDropdown,
7-
GlDropdownSectionHeader,
8-
GlDropdownItem,
9-
GlSearchBoxByType,
10-
GlTruncate,
11-
} from '@gitlab/ui';
2+
import { GlButton, GlIcon, GlAvatar, GlCollapsibleListbox, GlTruncate } from '@gitlab/ui';
123
import { debounce } from 'lodash';
134
import { filterBySearchTerm } from '~/analytics/shared/utils';
145
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
@@ -18,17 +9,15 @@ import { n__, s__, __ } from '~/locale';
189
import getProjects from '../graphql/projects.query.graphql';
1910
2011
const sortByProjectName = (projects = []) => projects.sort((a, b) => a.name.localeCompare(b.name));
12+
const mapItemToListboxFormat = (item) => ({ ...item, value: item.id, text: item.name });
2113
2214
export default {
2315
name: 'ProjectsDropdownFilter',
2416
components: {
17+
GlButton,
2518
GlIcon,
26-
GlLoadingIcon,
2719
GlAvatar,
28-
GlDropdown,
29-
GlDropdownSectionHeader,
30-
GlDropdownItem,
31-
GlSearchBoxByType,
20+
GlCollapsibleListbox,
3221
GlTruncate,
3322
},
3423
props: {
@@ -94,6 +83,9 @@ export default {
9483
selectedProjectIds() {
9584
return this.selectedProjects.map((p) => p.id);
9685
},
86+
selectedListBoxItems() {
87+
return this.multiSelect ? this.selectedProjectIds : this.selectedProjectIds[0];
88+
},
9789
hasSelectedProjects() {
9890
return Boolean(this.selectedProjects.length);
9991
},
@@ -110,6 +102,28 @@ export default {
110102
unselectedItems() {
111103
return this.availableProjects.filter(({ id }) => !this.selectedProjectIds.includes(id));
112104
},
105+
selectedGroupOptions() {
106+
return this.selectedItems.map(mapItemToListboxFormat);
107+
},
108+
unSelectedGroupOptions() {
109+
return this.unselectedItems.map(mapItemToListboxFormat);
110+
},
111+
listBoxItems() {
112+
if (this.selectedGroupOptions.length === 0) {
113+
return this.unSelectedGroupOptions;
114+
}
115+
116+
return [
117+
{
118+
text: __('Selected'),
119+
options: this.selectedGroupOptions,
120+
},
121+
{
122+
text: __('Unselected'),
123+
options: this.unSelectedGroupOptions,
124+
},
125+
];
126+
},
113127
},
114128
watch: {
115129
searchTerm() {
@@ -129,32 +143,29 @@ export default {
129143
search: debounce(function debouncedSearch() {
130144
this.fetchData();
131145
}, DEFAULT_DEBOUNCE_AND_THROTTLE_MS),
132-
getSelectedProjects(selectedProject, isSelected) {
133-
return isSelected
134-
? this.selectedProjects.concat([selectedProject])
135-
: this.selectedProjects.filter((project) => project.id !== selectedProject.id);
136-
},
137146
singleSelectedProject(selectedObj, isMarking) {
138147
return isMarking ? [selectedObj] : [];
139148
},
140-
setSelectedProjects(project) {
149+
setSelectedProjects(payload) {
141150
this.selectedProjects = this.multiSelect
142-
? this.getSelectedProjects(project, !this.isProjectSelected(project))
143-
: this.singleSelectedProject(project, !this.isProjectSelected(project));
151+
? payload
152+
: this.singleSelectedProject(payload, !this.isProjectSelected(payload));
144153
},
145-
onClick(project) {
154+
onClick(projectId) {
155+
const project = this.availableProjects.find(({ id }) => id === projectId);
146156
this.setSelectedProjects(project);
147157
this.handleUpdatedSelectedProjects();
148158
},
149-
onMultiSelectClick(project) {
150-
this.setSelectedProjects(project);
159+
onMultiSelectClick(projectIds) {
160+
const projects = this.availableProjects.filter(({ id }) => projectIds.includes(id));
161+
this.setSelectedProjects(projects);
151162
this.isDirty = true;
152163
},
153-
onSelected(project) {
164+
onSelected(payload) {
154165
if (this.multiSelect) {
155-
this.onMultiSelectClick(project);
166+
this.onMultiSelectClick(payload);
156167
} else {
157-
this.onClick(project);
168+
this.onClick(payload);
158169
}
159170
},
160171
onHide() {
@@ -201,97 +212,65 @@ export default {
201212
getEntityId(project) {
202213
return getIdFromGraphQLId(project.id);
203214
},
215+
setSearchTerm(val) {
216+
this.searchTerm = val;
217+
},
204218
},
205219
AVATAR_SHAPE_OPTION_RECT,
206220
};
207221
</script>
208222
<template>
209-
<gl-dropdown
223+
<gl-collapsible-listbox
210224
ref="projectsDropdown"
211-
class="dropdown dropdown-projects"
212225
toggle-class="gl-shadow-none gl-mb-0"
226+
:header-text="__('Projects')"
227+
:items="listBoxItems"
228+
:reset-button-label="__('Clear All')"
213229
:loading="loadingDefaultProjects"
214-
:show-clear-all="hasSelectedProjects"
215-
show-highlighted-items-title
216-
highlighted-items-title-class="gl-p-3"
217-
block
218-
@clear-all.stop="onClearAll"
219-
@hide="onHide"
230+
:multiple="multiSelect"
231+
:no-results-text="__('No matching results')"
232+
:selected="selectedListBoxItems"
233+
:searching="loading"
234+
searchable
235+
@hidden="onHide"
236+
@reset="onClearAll"
237+
@search="setSearchTerm"
238+
@select="onSelected"
220239
>
221-
<template #button-content>
222-
<gl-loading-icon v-if="loadingDefaultProjects" class="gl-mr-2 gl-flex-shrink-0" />
223-
<gl-avatar
224-
v-if="isOnlyOneProjectSelected"
225-
:src="selectedProjects[0].avatarUrl"
226-
:entity-id="getEntityId(selectedProjects[0])"
227-
:entity-name="selectedProjects[0].name"
228-
:size="16"
229-
:shape="$options.AVATAR_SHAPE_OPTION_RECT"
230-
:alt="selectedProjects[0].name"
231-
class="gl-display-inline-flex gl-vertical-align-middle gl-mr-2 gl-flex-shrink-0"
232-
/>
233-
<gl-truncate :text="selectedProjectsLabel" class="gl-min-w-0 gl-flex-grow-1" />
234-
<gl-icon class="gl-ml-2 gl-flex-shrink-0" name="chevron-down" />
235-
</template>
236-
<template #header>
237-
<gl-dropdown-section-header>{{ __('Projects') }}</gl-dropdown-section-header>
238-
<gl-search-box-by-type v-model.trim="searchTerm" :placeholder="__('Search')" />
239-
</template>
240-
<template #highlighted-items>
241-
<gl-dropdown-item
242-
v-for="project in selectedItems"
243-
:key="project.id"
244-
is-check-item
245-
:is-checked="isProjectSelected(project)"
246-
@click.native.capture.stop="onSelected(project)"
247-
>
248-
<div class="gl-display-flex">
249-
<gl-avatar
250-
class="gl-mr-2 gl-vertical-align-middle"
251-
:alt="project.name"
252-
:size="16"
253-
:entity-id="getEntityId(project)"
254-
:entity-name="project.name"
255-
:src="project.avatarUrl"
256-
:shape="$options.AVATAR_SHAPE_OPTION_RECT"
257-
/>
258-
<div>
259-
<div data-testid="project-name">{{ project.name }}</div>
260-
<div class="gl-text-gray-500" data-testid="project-full-path">
261-
{{ project.fullPath }}
262-
</div>
263-
</div>
264-
</div>
265-
</gl-dropdown-item>
240+
<template #toggle>
241+
<gl-button class="dropdown-projects">
242+
<gl-avatar
243+
v-if="isOnlyOneProjectSelected"
244+
:src="selectedProjects[0].avatarUrl"
245+
:entity-id="getEntityId(selectedProjects[0])"
246+
:entity-name="selectedProjects[0].name"
247+
:size="16"
248+
:shape="$options.AVATAR_SHAPE_OPTION_RECT"
249+
:alt="selectedProjects[0].name"
250+
class="gl-display-inline-flex gl-vertical-align-middle gl-mr-2 gl-flex-shrink-0"
251+
/>
252+
<gl-truncate :text="selectedProjectsLabel" class="gl-min-w-0 gl-flex-grow-1" />
253+
<gl-icon class="gl-ml-2 gl-flex-shrink-0" name="chevron-down" />
254+
</gl-button>
266255
</template>
267-
<gl-dropdown-item
268-
v-for="project in unselectedItems"
269-
:key="project.id"
270-
@click.native.capture.stop="onSelected(project)"
271-
>
256+
<template #list-item="{ item }">
272257
<div class="gl-display-flex">
273258
<gl-avatar
274-
class="gl-mr-2 vertical-align-middle"
275-
:alt="project.name"
259+
class="gl-mr-2 gl-vertical-align-middle"
260+
:alt="item.name"
276261
:size="16"
277-
:entity-id="getEntityId(project)"
278-
:entity-name="project.name"
279-
:src="project.avatarUrl"
262+
:entity-id="getEntityId(item)"
263+
:entity-name="item.name"
264+
:src="item.avatarUrl"
280265
:shape="$options.AVATAR_SHAPE_OPTION_RECT"
281266
/>
282267
<div>
283-
<div data-testid="project-name" data-qa-selector="project_name">{{ project.name }}</div>
268+
<div data-testid="project-name" data-qa-selector="project_name">{{ item.name }}</div>
284269
<div class="gl-text-gray-500" data-testid="project-full-path">
285-
{{ project.fullPath }}
270+
{{ item.fullPath }}
286271
</div>
287272
</div>
288273
</div>
289-
</gl-dropdown-item>
290-
<gl-dropdown-item v-show="noResultsAvailable" class="gl-pointer-events-none text-secondary">{{
291-
__('No matching results')
292-
}}</gl-dropdown-item>
293-
<gl-dropdown-item v-if="loading">
294-
<gl-loading-icon size="lg" />
295-
</gl-dropdown-item>
296-
</gl-dropdown>
274+
</template>
275+
</gl-collapsible-listbox>
297276
</template>
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# frozen_string_literal: true
2+
3+
module Mutations
4+
module Environments
5+
class Create < ::Mutations::BaseMutation
6+
graphql_name 'EnvironmentCreate'
7+
description 'Create an environment.'
8+
9+
include FindsProject
10+
11+
authorize :create_environment
12+
13+
argument :project_path,
14+
GraphQL::Types::ID,
15+
required: true,
16+
description: 'Full path of the project.'
17+
18+
argument :name,
19+
GraphQL::Types::String,
20+
required: true,
21+
description: 'Name of the environment.'
22+
23+
argument :external_url,
24+
GraphQL::Types::String,
25+
required: false,
26+
description: 'External URL of the environment.'
27+
28+
argument :tier,
29+
Types::DeploymentTierEnum,
30+
required: false,
31+
description: 'Tier of the environment.'
32+
33+
field :environment,
34+
Types::EnvironmentType,
35+
null: true,
36+
description: 'Created environment.'
37+
38+
def resolve(project_path:, **kwargs)
39+
project = authorized_find!(project_path)
40+
41+
response = ::Environments::CreateService.new(project, current_user, kwargs).execute
42+
43+
if response.success?
44+
{ environment: response.payload[:environment], errors: [] }
45+
else
46+
{ environment: response.payload[:environment], errors: response.errors }
47+
end
48+
end
49+
end
50+
end
51+
end

app/graphql/types/mutation_type.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ class MutationType < BaseObject
5252
mount_mutation Mutations::DependencyProxy::ImageTtlGroupPolicy::Update
5353
mount_mutation Mutations::DependencyProxy::GroupSettings::Update
5454
mount_mutation Mutations::Environments::CanaryIngress::Update
55+
mount_mutation Mutations::Environments::Create
5556
mount_mutation Mutations::Environments::Delete
5657
mount_mutation Mutations::Environments::Stop
5758
mount_mutation Mutations::Environments::Update
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# frozen_string_literal: true
2+
3+
module Environments
4+
class CreateService < BaseService
5+
def execute
6+
unless can?(current_user, :create_environment, project)
7+
return ServiceResponse.error(
8+
message: _('Unauthorized to create an environment'),
9+
payload: { environment: nil }
10+
)
11+
end
12+
13+
environment = project.environments.create(**params)
14+
15+
if environment.persisted?
16+
ServiceResponse.success(payload: { environment: environment })
17+
else
18+
ServiceResponse.error(
19+
message: environment.errors.full_messages,
20+
payload: { environment: nil }
21+
)
22+
end
23+
end
24+
end
25+
end

app/views/layouts/fullscreen.html.haml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
= render "layouts/broadcast"
1818
= yield :flash_message
1919
= render "layouts/flash", flash_container_no_margin: true
20-
.content-wrapper{ id: "content-body", class: "d-flex flex-column align-items-stretch" }
20+
.content-wrapper{ id: "content-body", class: "d-flex flex-column align-items-stretch gl-p-0" }
2121
= yield
2222
- unless minimal
2323
= render "layouts/nav/top_nav_responsive", class: "gl-flex-grow-1 gl-overflow-y-auto"
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# frozen_string_literal: true
2+
3+
class IndexSystemNoteMetadataOnIdForRelateAndUnrelateActions < Gitlab::Database::Migration[2.1]
4+
INDEX_NAME = 'tmp_index_for_backfilling_resource_link_events'
5+
CONDITION = "action='relate_to_parent' OR action='unrelate_from_parent'"
6+
7+
disable_ddl_transaction!
8+
9+
def up
10+
# Temporary index to be removed in https://gitlab.com/gitlab-org/gitlab/-/issues/408797
11+
add_concurrent_index :system_note_metadata, :id,
12+
where: CONDITION,
13+
name: INDEX_NAME
14+
end
15+
16+
def down
17+
remove_concurrent_index_by_name :system_note_metadata, INDEX_NAME
18+
end
19+
end

db/schema_migrations/20230426030342

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
54267e34c6978456efca6c784c4e4ea7541dac98a704095a9766809725021bb8

db/structure.sql

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33164,6 +33164,8 @@ CREATE INDEX tmp_index_container_repos_on_non_migrated ON container_repositories
3316433164

3316533165
CREATE INDEX tmp_index_container_repositories_on_id_migration_state ON container_repositories USING btree (id, migration_state);
3316633166

33167+
CREATE INDEX tmp_index_for_backfilling_resource_link_events ON system_note_metadata USING btree (id) WHERE (((action)::text = 'relate_to_parent'::text) OR ((action)::text = 'unrelate_from_parent'::text));
33168+
3316733169
CREATE INDEX tmp_index_for_null_member_namespace_id ON members USING btree (member_namespace_id) WHERE (member_namespace_id IS NULL);
3316833170

3316933171
CREATE INDEX tmp_index_for_project_namespace_id_migration_on_routes ON routes USING btree (id) WHERE ((namespace_id IS NULL) AND ((source_type)::text = 'Project'::text));

0 commit comments

Comments
 (0)