Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-06-17 13:07:47 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-06-17 13:07:47 +0300
commitd670c3006e6e44901bce0d53cc4768d1d80ffa92 (patch)
tree8f65743c232e5b76850c4cc264ba15e1185815ff /app
parenta5f4bba440d7f9ea47046a0a561d49adf0a1e6d4 (diff)
Add latest changes from gitlab-org/gitlab@14-0-stable-ee
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/content_editor/components/top_toolbar.vue9
-rw-r--r--app/assets/javascripts/content_editor/extensions/strike.js9
-rw-r--r--app/assets/javascripts/content_editor/services/create_content_editor.js2
-rw-r--r--app/assets/javascripts/environments/components/deploy_board.vue4
-rw-r--r--app/assets/javascripts/issuable_list/components/issuable_item.vue13
-rw-r--r--app/assets/javascripts/issuable_list/components/issuable_list_root.vue32
-rw-r--r--app/assets/javascripts/issues_list/components/issue_card_time_info.vue7
-rw-r--r--app/assets/javascripts/issues_list/components/issues_list_app.vue132
-rw-r--r--app/assets/javascripts/issues_list/constants.js5
-rw-r--r--app/assets/javascripts/issues_list/index.js13
-rw-r--r--app/assets/javascripts/issues_list/queries/get_issues.query.graphql45
-rw-r--r--app/assets/javascripts/issues_list/queries/issue.fragment.graphql51
-rw-r--r--app/assets/javascripts/jira_connect/index.js2
-rw-r--r--app/assets/javascripts/performance_bar/index.js2
-rw-r--r--app/assets/javascripts/runner/components/runner_manual_setup_help.vue42
-rw-r--r--app/assets/javascripts/runner/components/runner_registration_token_reset.vue83
-rw-r--r--app/assets/javascripts/runner/graphql/runners_registration_token_reset.mutation.graphql6
-rw-r--r--app/assets/javascripts/runner/runner_list/runner_list_app.vue7
-rw-r--r--app/assets/javascripts/sentry/index.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_action_button.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_actions.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue7
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue35
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue7
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue44
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue83
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue24
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value_collapsed.vue55
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/create_label.mutation.graphql15
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue5
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/actions.js26
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/mutation_types.js4
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/mutations.js11
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/state.js1
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/components/security_report_download_dropdown.vue2
-rw-r--r--app/assets/javascripts/webpack.js3
-rw-r--r--app/assets/stylesheets/framework/contextual_sidebar_refactoring/contextual_sidebar_variant.scss54
-rw-r--r--app/assets/stylesheets/startup/startup-dark.scss114
-rw-r--r--app/assets/stylesheets/startup/startup-general.scss18
-rw-r--r--app/controllers/admin/application_settings_controller.rb9
-rw-r--r--app/controllers/admin/cohorts_controller.rb23
-rw-r--r--app/controllers/admin/users_controller.rb24
-rw-r--r--app/controllers/confirmations_controller.rb2
-rw-r--r--app/controllers/projects/environments_controller.rb4
-rw-r--r--app/controllers/projects/merge_requests/content_controller.rb11
-rw-r--r--app/helpers/issues_helper.rb1
-rw-r--r--app/models/ability.rb67
-rw-r--r--app/models/analytics/cycle_analytics/project_level.rb1
-rw-r--r--app/models/ci/pipeline.rb2
-rw-r--r--app/models/clusters/platforms/kubernetes.rb6
-rw-r--r--app/models/container_expiration_policy.rb10
-rw-r--r--app/models/integration.rb4
-rw-r--r--app/models/issue.rb1
-rw-r--r--app/models/merge_request.rb3
-rw-r--r--app/models/packages/package.rb2
-rw-r--r--app/models/project.rb27
-rw-r--r--app/models/remote_mirror.rb3
-rw-r--r--app/models/user.rb15
-rw-r--r--app/policies/base_policy.rb6
-rw-r--r--app/policies/concerns/policy_actor.rb6
-rw-r--r--app/policies/global_policy.rb2
-rw-r--r--app/serializers/merge_request_poll_widget_entity.rb1
-rw-r--r--app/serializers/merge_request_widget_entity.rb2
-rw-r--r--app/services/environments/canary_ingress/update_service.rb4
-rw-r--r--app/services/merge_requests/refresh_service.rb10
-rw-r--r--app/services/packages/helm/process_file_service.rb97
-rw-r--r--app/services/projects/update_remote_mirror_service.rb8
-rw-r--r--app/services/users/update_service.rb1
-rw-r--r--app/views/admin/cohorts/_cohorts.html.haml (renamed from app/views/admin/users/_cohorts.html.haml)0
-rw-r--r--app/views/admin/cohorts/_cohorts_table.html.haml (renamed from app/views/admin/users/_cohorts_table.html.haml)0
-rw-r--r--app/views/admin/cohorts/index.html.haml (renamed from app/views/admin/users/cohorts.html.haml)2
-rw-r--r--app/views/admin/runners/show.html.haml14
-rw-r--r--app/views/admin/users/_tabs.html.haml2
-rw-r--r--app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml16
-rw-r--r--app/views/layouts/nav/sidebar/_admin.html.haml6
-rw-r--r--app/views/projects/merge_requests/show.html.haml2
-rw-r--r--app/workers/container_expiration_policy_worker.rb8
-rw-r--r--app/workers/web_hook_worker.rb2
79 files changed, 1028 insertions, 360 deletions
diff --git a/app/assets/javascripts/content_editor/components/top_toolbar.vue b/app/assets/javascripts/content_editor/components/top_toolbar.vue
index 07fdd3147e2..d3363ce092b 100644
--- a/app/assets/javascripts/content_editor/components/top_toolbar.vue
+++ b/app/assets/javascripts/content_editor/components/top_toolbar.vue
@@ -64,6 +64,15 @@ export default {
@execute="trackToolbarControlExecution"
/>
<toolbar-button
+ data-testid="strike"
+ content-type="strike"
+ icon-name="strikethrough"
+ editor-command="toggleStrike"
+ :label="__('Strikethrough')"
+ :tiptap-editor="contentEditor.tiptapEditor"
+ @execute="trackToolbarControlExecution"
+ />
+ <toolbar-button
data-testid="code"
content-type="code"
icon-name="code"
diff --git a/app/assets/javascripts/content_editor/extensions/strike.js b/app/assets/javascripts/content_editor/extensions/strike.js
new file mode 100644
index 00000000000..6f228e00994
--- /dev/null
+++ b/app/assets/javascripts/content_editor/extensions/strike.js
@@ -0,0 +1,9 @@
+import { Strike } from '@tiptap/extension-strike';
+
+export const tiptapExtension = Strike;
+export const serializer = {
+ open: '~~',
+ close: '~~',
+ mixable: true,
+ expelEnclosingWhitespace: true,
+};
diff --git a/app/assets/javascripts/content_editor/services/create_content_editor.js b/app/assets/javascripts/content_editor/services/create_content_editor.js
index df45287e6cb..8a54da6f57d 100644
--- a/app/assets/javascripts/content_editor/services/create_content_editor.js
+++ b/app/assets/javascripts/content_editor/services/create_content_editor.js
@@ -19,6 +19,7 @@ import * as Link from '../extensions/link';
import * as ListItem from '../extensions/list_item';
import * as OrderedList from '../extensions/ordered_list';
import * as Paragraph from '../extensions/paragraph';
+import * as Strike from '../extensions/strike';
import * as Text from '../extensions/text';
import buildSerializerConfig from './build_serializer_config';
import { ContentEditor } from './content_editor';
@@ -44,6 +45,7 @@ const builtInContentEditorExtensions = [
ListItem,
OrderedList,
Paragraph,
+ Strike,
Text,
];
diff --git a/app/assets/javascripts/environments/components/deploy_board.vue b/app/assets/javascripts/environments/components/deploy_board.vue
index f9c4660036b..217cea051b7 100644
--- a/app/assets/javascripts/environments/components/deploy_board.vue
+++ b/app/assets/javascripts/environments/components/deploy_board.vue
@@ -66,9 +66,7 @@ export default {
return this.isEmpty;
},
canRenderCanaryWeight() {
- return (
- this.glFeatures.canaryIngressWeightControl && !isEmpty(this.deployBoardData.canary_ingress)
- );
+ return !isEmpty(this.deployBoardData.canary_ingress);
},
instanceCount() {
const { instances } = this.deployBoardData;
diff --git a/app/assets/javascripts/issuable_list/components/issuable_item.vue b/app/assets/javascripts/issuable_list/components/issuable_item.vue
index 348dc054f57..20d1dce3905 100644
--- a/app/assets/javascripts/issuable_list/components/issuable_item.vue
+++ b/app/assets/javascripts/issuable_list/components/issuable_item.vue
@@ -50,6 +50,9 @@ export default {
},
},
computed: {
+ issuableId() {
+ return getIdFromGraphQLId(this.issuable.id);
+ },
createdInPastDay() {
const createdSecondsAgo = differenceInSeconds(new Date(this.issuable.createdAt), new Date());
return createdSecondsAgo < SECONDS_IN_DAY;
@@ -61,7 +64,7 @@ export default {
return this.issuable.gitlabWebUrl || this.issuable.webUrl;
},
authorId() {
- return getIdFromGraphQLId(`${this.author.id}`);
+ return getIdFromGraphQLId(this.author.id);
},
isIssuableUrlExternal() {
return isExternal(this.webUrl);
@@ -70,10 +73,10 @@ export default {
return this.issuable.labels?.nodes || this.issuable.labels || [];
},
labelIdsString() {
- return JSON.stringify(this.labels.map((label) => label.id));
+ return JSON.stringify(this.labels.map((label) => getIdFromGraphQLId(label.id)));
},
assignees() {
- return this.issuable.assignees || [];
+ return this.issuable.assignees?.nodes || this.issuable.assignees || [];
},
createdAt() {
return sprintf(__('created %{timeAgo}'), {
@@ -157,7 +160,7 @@ export default {
<template>
<li
- :id="`issuable_${issuable.id}`"
+ :id="`issuable_${issuableId}`"
class="issue gl-px-5!"
:class="{ closed: issuable.closedAt, today: createdInPastDay }"
:data-labels="labelIdsString"
@@ -167,7 +170,7 @@ export default {
<gl-form-checkbox
class="gl-mr-0"
:checked="checked"
- :data-id="issuable.id"
+ :data-id="issuableId"
@input="$emit('checked-input', $event)"
>
<span class="gl-sr-only">{{ issuable.title }}</span>
diff --git a/app/assets/javascripts/issuable_list/components/issuable_list_root.vue b/app/assets/javascripts/issuable_list/components/issuable_list_root.vue
index 45584205be0..a19c76cfe3f 100644
--- a/app/assets/javascripts/issuable_list/components/issuable_list_root.vue
+++ b/app/assets/javascripts/issuable_list/components/issuable_list_root.vue
@@ -1,7 +1,7 @@
<script>
-import { GlSkeletonLoading, GlPagination } from '@gitlab/ui';
+import { GlKeysetPagination, GlSkeletonLoading, GlPagination } from '@gitlab/ui';
import { uniqueId } from 'lodash';
-
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { updateHistory, setUrlParams } from '~/lib/utils/url_utility';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
@@ -19,6 +19,7 @@ export default {
tag: 'ul',
},
components: {
+ GlKeysetPagination,
GlSkeletonLoading,
IssuableTabs,
FilteredSearchBar,
@@ -140,6 +141,21 @@ export default {
required: false,
default: false,
},
+ useKeysetPagination: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ hasNextPage: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ hasPreviousPage: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -211,7 +227,7 @@ export default {
},
methods: {
issuableId(issuable) {
- return issuable.id || issuable.iid || uniqueId();
+ return getIdFromGraphQLId(issuable.id) || issuable.iid || uniqueId();
},
issuableChecked(issuable) {
return this.checkedIssuables[this.issuableId(issuable)]?.checked;
@@ -315,8 +331,16 @@ export default {
<slot v-else name="empty-state"></slot>
</template>
+ <div v-if="showPaginationControls && useKeysetPagination" class="gl-text-center gl-mt-3">
+ <gl-keyset-pagination
+ :has-next-page="hasNextPage"
+ :has-previous-page="hasPreviousPage"
+ @next="$emit('next-page')"
+ @prev="$emit('previous-page')"
+ />
+ </div>
<gl-pagination
- v-if="showPaginationControls"
+ v-else-if="showPaginationControls"
:per-page="defaultPageSize"
:total-items="totalItems"
:value="currentPage"
diff --git a/app/assets/javascripts/issues_list/components/issue_card_time_info.vue b/app/assets/javascripts/issues_list/components/issue_card_time_info.vue
index 8d00d337bac..70d73aca925 100644
--- a/app/assets/javascripts/issues_list/components/issue_card_time_info.vue
+++ b/app/assets/javascripts/issues_list/components/issue_card_time_info.vue
@@ -42,6 +42,9 @@ export default {
}
return __('Milestone');
},
+ milestoneLink() {
+ return this.issue.milestone.webPath || this.issue.milestone.webUrl;
+ },
dueDate() {
return this.issue.dueDate && dateInWords(new Date(this.issue.dueDate), true);
},
@@ -49,7 +52,7 @@ export default {
return isInPast(new Date(this.issue.dueDate));
},
timeEstimate() {
- return this.issue.timeStats?.humanTimeEstimate;
+ return this.issue.humanTimeEstimate || this.issue.timeStats?.humanTimeEstimate;
},
showHealthStatus() {
return this.hasIssuableHealthStatusFeature && this.issue.healthStatus;
@@ -85,7 +88,7 @@ export default {
class="issuable-milestone gl-display-none gl-sm-display-inline-block! gl-mr-3"
data-testid="issuable-milestone"
>
- <gl-link v-gl-tooltip :href="issue.milestone.webUrl" :title="milestoneDate">
+ <gl-link v-gl-tooltip :href="milestoneLink" :title="milestoneDate">
<gl-icon name="clock" />
{{ issue.milestone.title }}
</gl-link>
diff --git a/app/assets/javascripts/issues_list/components/issues_list_app.vue b/app/assets/javascripts/issues_list/components/issues_list_app.vue
index d5cab77f26c..dbf7717b248 100644
--- a/app/assets/javascripts/issues_list/components/issues_list_app.vue
+++ b/app/assets/javascripts/issues_list/components/issues_list_app.vue
@@ -9,7 +9,7 @@ import {
GlTooltipDirective,
} from '@gitlab/ui';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
-import { toNumber } from 'lodash';
+import getIssuesQuery from 'ee_else_ce/issues_list/queries/get_issues.query.graphql';
import createFlash from '~/flash';
import CsvImportExportButtons from '~/issuable/components/csv_import_export_buttons.vue';
import IssuableByEmail from '~/issuable/components/issuable_by_email.vue';
@@ -17,13 +17,12 @@ import IssuableList from '~/issuable_list/components/issuable_list_root.vue';
import { IssuableListTabs, IssuableStates } from '~/issuable_list/constants';
import {
API_PARAM,
- apiSortParams,
CREATED_DESC,
i18n,
+ initialPageParams,
MAX_LIST_SIZE,
PAGE_SIZE,
PARAM_DUE_DATE,
- PARAM_PAGE,
PARAM_SORT,
PARAM_STATE,
RELATIVE_POSITION_DESC,
@@ -49,7 +48,8 @@ import {
getSortOptions,
} from '~/issues_list/utils';
import axios from '~/lib/utils/axios_utils';
-import { convertObjectPropsToCamelCase, getParameterByName } from '~/lib/utils/common_utils';
+import { getParameterByName } from '~/lib/utils/common_utils';
+import { scrollUp } from '~/lib/utils/scroll_utils';
import {
DEFAULT_NONE_ANY,
OPERATOR_IS_ONLY,
@@ -107,9 +107,6 @@ export default {
emptyStateSvgPath: {
default: '',
},
- endpoint: {
- default: '',
- },
exportCsvPath: {
default: '',
},
@@ -173,15 +170,43 @@ export default {
dueDateFilter: getDueDateValue(getParameterByName(PARAM_DUE_DATE)),
exportCsvPathWithQuery: this.getExportCsvPathWithQuery(),
filterTokens: getFilterTokens(window.location.search),
- isLoading: false,
issues: [],
- page: toNumber(getParameterByName(PARAM_PAGE)) || 1,
+ pageInfo: {},
+ pageParams: initialPageParams,
showBulkEditSidebar: false,
sortKey: getSortKey(getParameterByName(PARAM_SORT)) || defaultSortKey,
state: state || IssuableStates.Opened,
totalIssues: 0,
};
},
+ apollo: {
+ issues: {
+ query: getIssuesQuery,
+ variables() {
+ return {
+ projectPath: this.projectPath,
+ search: this.searchQuery,
+ sort: this.sortKey,
+ state: this.state,
+ ...this.pageParams,
+ ...this.apiFilterParams,
+ };
+ },
+ update: ({ project }) => project.issues.nodes,
+ result({ data }) {
+ this.pageInfo = data.project.issues.pageInfo;
+ this.totalIssues = data.project.issues.count;
+ this.exportCsvPathWithQuery = this.getExportCsvPathWithQuery();
+ },
+ error(error) {
+ createFlash({ message: this.$options.i18n.errorFetchingIssues, captureError: true, error });
+ },
+ skip() {
+ return !this.hasProjectIssues;
+ },
+ debounce: 200,
+ },
+ },
computed: {
hasSearch() {
return this.searchQuery || Object.keys(this.urlFilterParams).length;
@@ -348,7 +373,6 @@ export default {
return {
due_date: this.dueDateFilter,
- page: this.page,
search: this.searchQuery,
state: this.state,
...urlSortParams[this.sortKey],
@@ -361,7 +385,6 @@ export default {
},
mounted() {
eventHub.$on('issuables:toggleBulkEdit', this.toggleBulkEditSidebar);
- this.fetchIssues();
},
beforeDestroy() {
eventHub.$off('issuables:toggleBulkEdit', this.toggleBulkEditSidebar);
@@ -406,54 +429,11 @@ export default {
fetchUsers(search) {
return axios.get(this.autocompleteUsersPath, { params: { search } });
},
- fetchIssues() {
- if (!this.hasProjectIssues) {
- return undefined;
- }
-
- this.isLoading = true;
-
- const filterParams = {
- ...this.apiFilterParams,
- };
-
- if (filterParams.epic_id) {
- filterParams.epic_id = filterParams.epic_id.split('::&').pop();
- } else if (filterParams['not[epic_id]']) {
- filterParams['not[epic_id]'] = filterParams['not[epic_id]'].split('::&').pop();
- }
-
- return axios
- .get(this.endpoint, {
- params: {
- due_date: this.dueDateFilter,
- page: this.page,
- per_page: PAGE_SIZE,
- search: this.searchQuery,
- state: this.state,
- with_labels_details: true,
- ...apiSortParams[this.sortKey],
- ...filterParams,
- },
- })
- .then(({ data, headers }) => {
- this.page = Number(headers['x-page']);
- this.totalIssues = Number(headers['x-total']);
- this.issues = data.map((issue) => convertObjectPropsToCamelCase(issue, { deep: true }));
- this.exportCsvPathWithQuery = this.getExportCsvPathWithQuery();
- })
- .catch(() => {
- createFlash({ message: this.$options.i18n.errorFetchingIssues });
- })
- .finally(() => {
- this.isLoading = false;
- });
- },
getExportCsvPathWithQuery() {
return `${this.exportCsvPath}${window.location.search}`;
},
getStatus(issue) {
- if (issue.closedAt && issue.movedToId) {
+ if (issue.closedAt && issue.moved) {
return this.$options.i18n.closedMoved;
}
if (issue.closedAt) {
@@ -484,18 +464,26 @@ export default {
},
handleClickTab(state) {
if (this.state !== state) {
- this.page = 1;
+ this.pageParams = initialPageParams;
}
this.state = state;
- this.fetchIssues();
},
handleFilter(filter) {
this.filterTokens = filter;
- this.fetchIssues();
},
- handlePageChange(page) {
- this.page = page;
- this.fetchIssues();
+ handleNextPage() {
+ this.pageParams = {
+ afterCursor: this.pageInfo.endCursor,
+ firstPageSize: PAGE_SIZE,
+ };
+ scrollUp();
+ },
+ handlePreviousPage() {
+ this.pageParams = {
+ beforeCursor: this.pageInfo.startCursor,
+ lastPageSize: PAGE_SIZE,
+ };
+ scrollUp();
},
handleReorder({ newIndex, oldIndex }) {
const issueToMove = this.issues[oldIndex];
@@ -530,9 +518,11 @@ export default {
createFlash({ message: this.$options.i18n.reorderError });
});
},
- handleSort(value) {
- this.sortKey = value;
- this.fetchIssues();
+ handleSort(sortKey) {
+ if (this.sortKey !== sortKey) {
+ this.pageParams = initialPageParams;
+ }
+ this.sortKey = sortKey;
},
toggleBulkEditSidebar(showBulkEditSidebar) {
this.showBulkEditSidebar = showBulkEditSidebar;
@@ -556,18 +546,18 @@ export default {
:tabs="$options.IssuableListTabs"
:current-tab="state"
:tab-counts="tabCounts"
- :issuables-loading="isLoading"
+ :issuables-loading="$apollo.queries.issues.loading"
:is-manual-ordering="isManualOrdering"
:show-bulk-edit-sidebar="showBulkEditSidebar"
:show-pagination-controls="showPaginationControls"
- :total-items="totalIssues"
- :current-page="page"
- :previous-page="page - 1"
- :next-page="page + 1"
+ :use-keyset-pagination="true"
+ :has-next-page="pageInfo.hasNextPage"
+ :has-previous-page="pageInfo.hasPreviousPage"
:url-params="urlParams"
@click-tab="handleClickTab"
@filter="handleFilter"
- @page-change="handlePageChange"
+ @next-page="handleNextPage"
+ @previous-page="handlePreviousPage"
@reorder="handleReorder"
@sort="handleSort"
@update-legacy-bulk-edit="handleUpdateLegacyBulkEdit"
@@ -646,7 +636,7 @@ export default {
</li>
<blocking-issues-count
class="gl-display-none gl-sm-display-block"
- :blocking-issues-count="issuable.blockingIssuesCount"
+ :blocking-issues-count="issuable.blockedByCount"
:is-list-item="true"
/>
</template>
diff --git a/app/assets/javascripts/issues_list/constants.js b/app/assets/javascripts/issues_list/constants.js
index 06e140d6420..76006f9081d 100644
--- a/app/assets/javascripts/issues_list/constants.js
+++ b/app/assets/javascripts/issues_list/constants.js
@@ -101,10 +101,13 @@ export const i18n = {
export const JIRA_IMPORT_SUCCESS_ALERT_HIDE_MAP_KEY = 'jira-import-success-alert-hide-map';
export const PARAM_DUE_DATE = 'due_date';
-export const PARAM_PAGE = 'page';
export const PARAM_SORT = 'sort';
export const PARAM_STATE = 'state';
+export const initialPageParams = {
+ firstPageSize: PAGE_SIZE,
+};
+
export const DUE_DATE_NONE = '0';
export const DUE_DATE_ANY = '';
export const DUE_DATE_OVERDUE = 'overdue';
diff --git a/app/assets/javascripts/issues_list/index.js b/app/assets/javascripts/issues_list/index.js
index d0c9462a3d7..97b9a9a115d 100644
--- a/app/assets/javascripts/issues_list/index.js
+++ b/app/assets/javascripts/issues_list/index.js
@@ -73,6 +73,13 @@ export function mountIssuesListApp() {
return false;
}
+ Vue.use(VueApollo);
+
+ const defaultClient = createDefaultClient({}, { assumeImmutableResults: true });
+ const apolloProvider = new VueApollo({
+ defaultClient,
+ });
+
const {
autocompleteAwardEmojisPath,
autocompleteUsersPath,
@@ -83,7 +90,6 @@ export function mountIssuesListApp() {
email,
emailsHelpPagePath,
emptyStateSvgPath,
- endpoint,
exportCsvPath,
groupEpicsPath,
hasBlockedIssuesFeature,
@@ -113,16 +119,13 @@ export function mountIssuesListApp() {
return new Vue({
el,
- // Currently does not use Vue Apollo, but need to provide {} for now until the
- // issue is fixed upstream in https://github.com/vuejs/vue-apollo/pull/1153
- apolloProvider: {},
+ apolloProvider,
provide: {
autocompleteAwardEmojisPath,
autocompleteUsersPath,
calendarPath,
canBulkUpdate: parseBoolean(canBulkUpdate),
emptyStateSvgPath,
- endpoint,
groupEpicsPath,
hasBlockedIssuesFeature: parseBoolean(hasBlockedIssuesFeature),
hasIssuableHealthStatusFeature: parseBoolean(hasIssuableHealthStatusFeature),
diff --git a/app/assets/javascripts/issues_list/queries/get_issues.query.graphql b/app/assets/javascripts/issues_list/queries/get_issues.query.graphql
new file mode 100644
index 00000000000..afd53084ca0
--- /dev/null
+++ b/app/assets/javascripts/issues_list/queries/get_issues.query.graphql
@@ -0,0 +1,45 @@
+#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
+#import "./issue.fragment.graphql"
+
+query getProjectIssues(
+ $projectPath: ID!
+ $search: String
+ $sort: IssueSort
+ $state: IssuableState
+ $assigneeId: String
+ $assigneeUsernames: [String!]
+ $authorUsername: String
+ $labelName: [String]
+ $milestoneTitle: [String]
+ $not: NegatedIssueFilterInput
+ $beforeCursor: String
+ $afterCursor: String
+ $firstPageSize: Int
+ $lastPageSize: Int
+) {
+ project(fullPath: $projectPath) {
+ issues(
+ search: $search
+ sort: $sort
+ state: $state
+ assigneeId: $assigneeId
+ assigneeUsernames: $assigneeUsernames
+ authorUsername: $authorUsername
+ labelName: $labelName
+ milestoneTitle: $milestoneTitle
+ not: $not
+ before: $beforeCursor
+ after: $afterCursor
+ first: $firstPageSize
+ last: $lastPageSize
+ ) {
+ count
+ pageInfo {
+ ...PageInfo
+ }
+ nodes {
+ ...IssueFragment
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/issues_list/queries/issue.fragment.graphql b/app/assets/javascripts/issues_list/queries/issue.fragment.graphql
new file mode 100644
index 00000000000..de30d8b4bf6
--- /dev/null
+++ b/app/assets/javascripts/issues_list/queries/issue.fragment.graphql
@@ -0,0 +1,51 @@
+fragment IssueFragment on Issue {
+ id
+ iid
+ closedAt
+ confidential
+ createdAt
+ downvotes
+ dueDate
+ humanTimeEstimate
+ moved
+ title
+ updatedAt
+ upvotes
+ userDiscussionsCount
+ webUrl
+ assignees {
+ nodes {
+ id
+ avatarUrl
+ name
+ username
+ webUrl
+ }
+ }
+ author {
+ id
+ avatarUrl
+ name
+ username
+ webUrl
+ }
+ labels {
+ nodes {
+ id
+ color
+ title
+ description
+ }
+ }
+ milestone {
+ id
+ dueDate
+ startDate
+ webPath
+ title
+ }
+ taskCompletionStatus {
+ completedCount
+ count
+ }
+}
diff --git a/app/assets/javascripts/jira_connect/index.js b/app/assets/javascripts/jira_connect/index.js
index dc8bb3b0c77..bc0d21c6c9a 100644
--- a/app/assets/javascripts/jira_connect/index.js
+++ b/app/assets/javascripts/jira_connect/index.js
@@ -1,3 +1,5 @@
+import '../webpack';
+
import setConfigs from '@gitlab/ui/dist/config';
import Vue from 'vue';
import { getLocation, sizeToParent } from '~/jira_connect/utils';
diff --git a/app/assets/javascripts/performance_bar/index.js b/app/assets/javascripts/performance_bar/index.js
index d8aab25a6a8..66e999ca43b 100644
--- a/app/assets/javascripts/performance_bar/index.js
+++ b/app/assets/javascripts/performance_bar/index.js
@@ -1,3 +1,5 @@
+import '../webpack';
+
import Vue from 'vue';
import axios from '~/lib/utils/axios_utils';
import { numberToHumanSize } from '~/lib/utils/number_utils';
diff --git a/app/assets/javascripts/runner/components/runner_manual_setup_help.vue b/app/assets/javascripts/runner/components/runner_manual_setup_help.vue
index 4755977b051..426d377c92b 100644
--- a/app/assets/javascripts/runner/components/runner_manual_setup_help.vue
+++ b/app/assets/javascripts/runner/components/runner_manual_setup_help.vue
@@ -1,8 +1,10 @@
<script>
import { GlLink, GlSprintf, GlTooltipDirective } from '@gitlab/ui';
-import { __ } from '~/locale';
+import { s__ } from '~/locale';
+import RunnerRegistrationTokenReset from '~/runner/components/runner_registration_token_reset.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import RunnerInstructions from '~/vue_shared/components/runner_instructions/runner_instructions.vue';
+import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '../constants';
export default {
components: {
@@ -10,6 +12,7 @@ export default {
GlSprintf,
ClipboardButton,
RunnerInstructions,
+ RunnerRegistrationTokenReset,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -24,16 +27,40 @@ export default {
type: String,
required: true,
},
- typeName: {
+ type: {
type: String,
- required: false,
- default: __('shared'),
+ required: true,
+ validator(type) {
+ return [INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE].includes(type);
+ },
},
},
+ data() {
+ return {
+ currentRegistrationToken: this.registrationToken,
+ };
+ },
computed: {
rootUrl() {
return gon.gitlab_url || '';
},
+ typeName() {
+ switch (this.type) {
+ case INSTANCE_TYPE:
+ return s__('Runners|shared');
+ case GROUP_TYPE:
+ return s__('Runners|group');
+ case PROJECT_TYPE:
+ return s__('Runners|specific');
+ default:
+ return '';
+ }
+ },
+ },
+ methods: {
+ onTokenReset(token) {
+ this.currentRegistrationToken = token;
+ },
},
};
</script>
@@ -65,12 +92,13 @@ export default {
{{ __('And this registration token:') }}
<br />
- <code data-testid="registration-token">{{ registrationToken }}</code>
- <clipboard-button :title="__('Copy token')" :text="registrationToken" />
+ <code data-testid="registration-token">{{ currentRegistrationToken }}</code>
+ <clipboard-button :title="__('Copy token')" :text="currentRegistrationToken" />
</li>
</ol>
- <!-- TODO Implement reset token functionality -->
+ <runner-registration-token-reset :type="type" @tokenReset="onTokenReset" />
+
<runner-instructions />
</div>
</template>
diff --git a/app/assets/javascripts/runner/components/runner_registration_token_reset.vue b/app/assets/javascripts/runner/components/runner_registration_token_reset.vue
new file mode 100644
index 00000000000..b03574264d9
--- /dev/null
+++ b/app/assets/javascripts/runner/components/runner_registration_token_reset.vue
@@ -0,0 +1,83 @@
+<script>
+import { GlButton } from '@gitlab/ui';
+import createFlash, { FLASH_TYPES } from '~/flash';
+import { __, s__ } from '~/locale';
+import runnersRegistrationTokenResetMutation from '~/runner/graphql/runners_registration_token_reset.mutation.graphql';
+import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '../constants';
+
+export default {
+ components: {
+ GlButton,
+ },
+ props: {
+ type: {
+ type: String,
+ required: true,
+ validator(type) {
+ return [INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE].includes(type);
+ },
+ },
+ },
+ data() {
+ return {
+ loading: false,
+ };
+ },
+ computed: {},
+ methods: {
+ async resetToken() {
+ // TODO Replace confirmation with gl-modal
+ // See: https://gitlab.com/gitlab-org/gitlab/-/issues/333810
+ // eslint-disable-next-line no-alert
+ if (!window.confirm(__('Are you sure you want to reset the registration token?'))) {
+ return;
+ }
+
+ this.loading = true;
+ try {
+ const {
+ data: {
+ runnersRegistrationTokenReset: { token, errors },
+ },
+ } = await this.$apollo.mutate({
+ mutation: runnersRegistrationTokenResetMutation,
+ variables: {
+ // TODO Currently INTANCE_TYPE only is supported
+ // In future iterations this component will support
+ // other registration token types.
+ // See: https://gitlab.com/gitlab-org/gitlab/-/issues/19819
+ input: {
+ type: this.type,
+ },
+ },
+ });
+ if (errors && errors.length) {
+ this.onError(new Error(errors[0]));
+ return;
+ }
+ this.onSuccess(token);
+ } catch (e) {
+ this.onError(e);
+ } finally {
+ this.loading = false;
+ }
+ },
+ onError(error) {
+ const { message } = error;
+ createFlash({ message });
+ },
+ onSuccess(token) {
+ createFlash({
+ message: s__('Runners|New registration token generated!'),
+ type: FLASH_TYPES.SUCCESS,
+ });
+ this.$emit('tokenReset', token);
+ },
+ },
+};
+</script>
+<template>
+ <gl-button :loading="loading" @click="resetToken">
+ {{ __('Reset registration token') }}
+ </gl-button>
+</template>
diff --git a/app/assets/javascripts/runner/graphql/runners_registration_token_reset.mutation.graphql b/app/assets/javascripts/runner/graphql/runners_registration_token_reset.mutation.graphql
new file mode 100644
index 00000000000..9c2797732ad
--- /dev/null
+++ b/app/assets/javascripts/runner/graphql/runners_registration_token_reset.mutation.graphql
@@ -0,0 +1,6 @@
+mutation runnersRegistrationTokenReset($input: RunnersRegistrationTokenResetInput!) {
+ runnersRegistrationTokenReset(input: $input) {
+ token
+ errors
+ }
+}
diff --git a/app/assets/javascripts/runner/runner_list/runner_list_app.vue b/app/assets/javascripts/runner/runner_list/runner_list_app.vue
index b4eacb911a2..7f3a980ccca 100644
--- a/app/assets/javascripts/runner/runner_list/runner_list_app.vue
+++ b/app/assets/javascripts/runner/runner_list/runner_list_app.vue
@@ -7,6 +7,7 @@ import RunnerList from '../components/runner_list.vue';
import RunnerManualSetupHelp from '../components/runner_manual_setup_help.vue';
import RunnerPagination from '../components/runner_pagination.vue';
import RunnerTypeHelp from '../components/runner_type_help.vue';
+import { INSTANCE_TYPE } from '../constants';
import getRunnersQuery from '../graphql/get_runners.query.graphql';
import {
fromUrlQueryToSearch,
@@ -97,6 +98,7 @@ export default {
});
},
},
+ INSTANCE_TYPE,
};
</script>
<template>
@@ -106,7 +108,10 @@ export default {
<runner-type-help />
</div>
<div class="col-sm-6">
- <runner-manual-setup-help :registration-token="registrationToken" />
+ <runner-manual-setup-help
+ :registration-token="registrationToken"
+ :type="$options.INSTANCE_TYPE"
+ />
</div>
</div>
diff --git a/app/assets/javascripts/sentry/index.js b/app/assets/javascripts/sentry/index.js
index 06e4e0aa507..a875ef84088 100644
--- a/app/assets/javascripts/sentry/index.js
+++ b/app/assets/javascripts/sentry/index.js
@@ -1,3 +1,5 @@
+import '../webpack';
+
import SentryConfig from './sentry_config';
const index = function index() {
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_action_button.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_action_button.vue
index b25c0cc0d96..bdd46d6a656 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_action_button.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_action_button.vue
@@ -71,7 +71,7 @@ export default {
:aria-label="buttonTitle"
:loading="isLoading"
:disabled="isActionInProgress"
- :class="`inline gl-ml-2 ${containerClasses}`"
+ :class="`inline gl-ml-3 ${containerClasses}`"
:icon="icon"
@click="$emit('click')"
>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_actions.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_actions.vue
index 671f9cb8e74..7e587663c26 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_actions.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_actions.vue
@@ -151,7 +151,7 @@ export default {
</script>
<template>
- <div>
+ <div class="gl-display-inline-flex">
<deployment-action-button
v-if="canBeManuallyDeployed"
:action-in-progress="actionInProgress"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue
index a5d165ebd49..459bee8023f 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue
@@ -71,13 +71,13 @@ export default {
};
</script>
<template>
- <span>
+ <span class="gl-display-inline-flex">
<gl-button-group v-if="shouldRenderDropdown" size="small">
<review-app-link
:display="appButtonText"
:link="deploymentExternalUrl"
size="small"
- css-class="deploy-link js-deploy-url inline"
+ css-class="deploy-link js-deploy-url inline gl-ml-3"
/>
<gl-dropdown toggle-class="gl-px-2!" size="small" class="js-mr-wigdet-deployment-dropdown">
<template #button-content>
@@ -112,7 +112,7 @@ export default {
:display="appButtonText"
:link="deploymentExternalUrl"
size="small"
- css-class="js-deploy-url deploy-link btn btn-default btn-sm inline"
+ css-class="js-deploy-url deploy-link btn btn-default btn-sm inline gl-ml-3"
/>
<visual-review-app-link
v-if="showVisualReviewApp"
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue
index 2e7b3e149b2..3b261f5ac25 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue
@@ -71,9 +71,9 @@ export default {
<template>
<base-token
- :token-config="config"
- :token-value="value"
- :token-active="active"
+ :config="config"
+ :value="value"
+ :active="active"
:tokens-list-loading="loading"
:token-values="authors"
:fn-active-token-value="getActiveAuthor"
@@ -81,6 +81,7 @@ export default {
:preloaded-token-values="preloadedAuthors"
:recent-token-values-storage-key="config.recentTokenValuesStorageKey"
@fetch-token-values="fetchAuthorBySearchTerm"
+ v-on="$listeners"
>
<template #view="{ viewTokenProps: { inputValue, activeTokenValue } }">
<gl-avatar
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue
index fb6b9e4bc0d..bda6b340871 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue
@@ -19,29 +19,34 @@ export default {
GlLoadingIcon,
},
props: {
- tokenConfig: {
+ config: {
type: Object,
required: true,
},
- tokenValue: {
+ value: {
type: Object,
required: true,
},
- tokenActive: {
+ active: {
type: Boolean,
required: true,
},
tokensListLoading: {
type: Boolean,
- required: true,
+ required: false,
+ default: false,
},
tokenValues: {
type: Array,
- required: true,
+ required: false,
+ default: () => [],
},
fnActiveTokenValue: {
type: Function,
- required: true,
+ required: false,
+ default: (tokenValues, currentTokenValue) => {
+ return tokenValues.find(({ value }) => value === currentTokenValue);
+ },
},
defaultTokenValues: {
type: Array,
@@ -90,9 +95,9 @@ export default {
},
currentTokenValue() {
if (this.fnCurrentTokenValue) {
- return this.fnCurrentTokenValue(this.tokenValue.data);
+ return this.fnCurrentTokenValue(this.value.data);
}
- return this.tokenValue.data.toLowerCase();
+ return this.value.data.toLowerCase();
},
activeTokenValue() {
return this.fnActiveTokenValue(this.tokenValues, this.currentTokenValue);
@@ -113,11 +118,11 @@ export default {
},
},
watch: {
- tokenActive: {
+ active: {
immediate: true,
handler(newValue) {
if (!newValue && !this.tokenValues.length) {
- this.$emit('fetch-token-values', this.tokenValue.data);
+ this.$emit('fetch-token-values', this.value.data);
}
},
},
@@ -148,9 +153,11 @@ export default {
<template>
<gl-filtered-search-token
- :config="tokenConfig"
- v-bind="{ ...this.$parent.$props, ...this.$parent.$attrs }"
- v-on="this.$parent.$listeners"
+ :config="config"
+ :value="value"
+ :active="active"
+ v-bind="$attrs"
+ v-on="$listeners"
@input="handleInput"
@select="handleTokenValueSelected(activeTokenValue)"
>
@@ -177,7 +184,7 @@ export default {
<gl-dropdown-divider />
</template>
<slot
- v-if="preloadedTokenValues.length"
+ v-if="preloadedTokenValues.length && !searchKey"
name="token-values-list"
:token-values="preloadedTokenValues"
></slot>
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue
index 20b8cbfe933..e496d099a42 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue
@@ -93,15 +93,16 @@ export default {
<template>
<base-token
- :token-config="config"
- :token-value="value"
- :token-active="active"
+ :config="config"
+ :value="value"
+ :active="active"
:tokens-list-loading="loading"
:token-values="labels"
:fn-active-token-value="getActiveLabel"
:default-token-values="defaultLabels"
:recent-token-values-storage-key="config.recentTokenValuesStorageKey"
@fetch-token-values="fetchLabelBySearchTerm"
+ v-on="$listeners"
>
<template
#view-token="{ viewTokenProps: { inputValue, cssClasses, listeners, activeTokenValue } }"
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue
index d80b66fd9be..1f0704f7308 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue
@@ -1,5 +1,6 @@
<script>
-import { mapGetters, mapState } from 'vuex';
+import { GlButton } from '@gitlab/ui';
+import { mapActions, mapGetters, mapState } from 'vuex';
import DropdownContentsCreateView from './dropdown_contents_create_view.vue';
import DropdownContentsLabelsView from './dropdown_contents_labels_view.vue';
@@ -8,6 +9,7 @@ export default {
components: {
DropdownContentsLabelsView,
DropdownContentsCreateView,
+ GlButton,
},
props: {
renderOnTop: {
@@ -15,10 +17,14 @@ export default {
required: false,
default: false,
},
+ labelsCreateTitle: {
+ type: String,
+ required: true,
+ },
},
computed: {
- ...mapState(['showDropdownContentsCreateView']),
- ...mapGetters(['isDropdownVariantSidebar']),
+ ...mapState(['showDropdownContentsCreateView', 'labelsListTitle']),
+ ...mapGetters(['isDropdownVariantSidebar', 'isDropdownVariantEmbedded']),
dropdownContentsView() {
if (this.showDropdownContentsCreateView) {
return 'dropdown-contents-create-view';
@@ -29,6 +35,12 @@ export default {
const bottom = this.isDropdownVariantSidebar ? '3rem' : '2rem';
return this.renderOnTop ? { bottom } : {};
},
+ dropdownTitle() {
+ return this.showDropdownContentsCreateView ? this.labelsCreateTitle : this.labelsListTitle;
+ },
+ },
+ methods: {
+ ...mapActions(['toggleDropdownContentsCreateView', 'toggleDropdownContents']),
},
};
</script>
@@ -39,6 +51,30 @@ export default {
data-qa-selector="labels_dropdown_content"
:style="directionStyle"
>
- <component :is="dropdownContentsView" />
+ <div
+ v-if="isDropdownVariantSidebar || isDropdownVariantEmbedded"
+ class="dropdown-title gl-display-flex gl-align-items-center gl-pt-0 gl-pb-3!"
+ data-testid="dropdown-title"
+ >
+ <gl-button
+ v-if="showDropdownContentsCreateView"
+ :aria-label="__('Go back')"
+ variant="link"
+ size="small"
+ class="js-btn-back dropdown-header-button p-0"
+ icon="arrow-left"
+ @click="toggleDropdownContentsCreateView"
+ />
+ <span class="flex-grow-1">{{ dropdownTitle }}</span>
+ <gl-button
+ :aria-label="__('Close')"
+ variant="link"
+ size="small"
+ class="dropdown-header-button gl-p-0!"
+ icon="close"
+ @click="toggleDropdownContents"
+ />
+ </div>
+ <component :is="dropdownContentsView" @hideCreateView="toggleDropdownContentsCreateView" />
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue
index f8cc981ba3d..a7f20fbe851 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue
@@ -1,6 +1,10 @@
<script>
import { GlTooltipDirective, GlButton, GlFormInput, GlLink, GlLoadingIcon } from '@gitlab/ui';
-import { mapState, mapActions } from 'vuex';
+import createFlash from '~/flash';
+import { __ } from '~/locale';
+import createLabelMutation from './graphql/create_label.mutation.graphql';
+
+const errorMessage = __('Error creating label.');
export default {
components: {
@@ -12,14 +16,19 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
+ inject: {
+ projectPath: {
+ default: '',
+ },
+ },
data() {
return {
labelTitle: '',
selectedColor: '',
+ labelCreateInProgress: false,
};
},
computed: {
- ...mapState(['labelsCreateTitle', 'labelCreateInProgress']),
disableCreate() {
return !this.labelTitle.length || !this.selectedColor.length || this.labelCreateInProgress;
},
@@ -29,7 +38,6 @@ export default {
},
},
methods: {
- ...mapActions(['toggleDropdownContents', 'toggleDropdownContentsCreateView', 'createLabel']),
getColorCode(color) {
return Object.keys(color).pop();
},
@@ -39,11 +47,27 @@ export default {
handleColorClick(color) {
this.selectedColor = this.getColorCode(color);
},
- handleCreateClick() {
- this.createLabel({
- title: this.labelTitle,
- color: this.selectedColor,
- });
+ async createLabel() {
+ this.labelCreateInProgress = true;
+ try {
+ const {
+ data: { labelCreate },
+ } = await this.$apollo.mutate({
+ mutation: createLabelMutation,
+ variables: {
+ title: this.labelTitle,
+ color: this.selectedColor,
+ projectPath: this.projectPath,
+ },
+ });
+ if (labelCreate.errors.length) {
+ createFlash({ message: errorMessage });
+ }
+ } catch {
+ createFlash({ message: errorMessage });
+ }
+ this.labelCreateInProgress = false;
+ this.$emit('hideCreateView');
},
},
};
@@ -51,34 +75,16 @@ export default {
<template>
<div class="labels-select-contents-create js-labels-create">
- <div class="dropdown-title d-flex align-items-center pt-0 pb-2">
- <gl-button
- :aria-label="__('Go back')"
- variant="link"
- size="small"
- class="js-btn-back dropdown-header-button p-0"
- icon="arrow-left"
- @click="toggleDropdownContentsCreateView"
- />
- <span class="flex-grow-1">{{ labelsCreateTitle }}</span>
- <gl-button
- :aria-label="__('Close')"
- variant="link"
- size="small"
- class="dropdown-header-button p-0"
- icon="close"
- @click="toggleDropdownContents"
- />
- </div>
<div class="dropdown-input">
<gl-form-input
v-model.trim="labelTitle"
:placeholder="__('Name new label')"
:autofocus="true"
+ data-testid="label-title-input"
/>
</div>
- <div class="dropdown-content px-2">
- <div class="suggest-colors suggest-colors-dropdown mt-0 mb-2">
+ <div class="dropdown-content gl-px-3">
+ <div class="suggest-colors suggest-colors-dropdown gl-mt-0! gl-mb-3!">
<gl-link
v-for="(color, index) in suggestedColors"
:key="index"
@@ -90,28 +96,35 @@ export default {
</div>
<div class="color-input-container gl-display-flex">
<span
- class="dropdown-label-color-preview position-relative position-relative d-inline-block"
+ class="dropdown-label-color-preview gl-relative gl-display-inline-block"
+ data-testid="selected-color"
:style="{ backgroundColor: selectedColor }"
></span>
<gl-form-input
v-model.trim="selectedColor"
class="gl-rounded-top-left-none gl-rounded-bottom-left-none"
:placeholder="__('Use custom color #FF0000')"
+ data-testid="selected-color-text"
/>
</div>
</div>
- <div class="dropdown-actions clearfix pt-2 px-2">
+ <div class="dropdown-actions gl-display-flex gl-justify-content-space-between gl-pt-3 gl-px-3">
<gl-button
:disabled="disableCreate"
category="primary"
variant="success"
- class="float-left d-flex align-items-center"
- @click="handleCreateClick"
+ class="gl-display-flex gl-align-items-center"
+ data-testid="create-button"
+ @click="createLabel"
>
- <gl-loading-icon v-show="labelCreateInProgress" :inline="true" class="mr-1" />
+ <gl-loading-icon v-if="labelCreateInProgress" :inline="true" class="mr-1" />
{{ __('Create') }}
</gl-button>
- <gl-button class="float-right js-btn-cancel-create" @click="toggleDropdownContentsCreateView">
+ <gl-button
+ class="js-btn-cancel-create"
+ data-testid="cancel-button"
+ @click="$emit('hideCreateView')"
+ >
{{ __('Cancel') }}
</gl-button>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue
index 86788a84260..bff34743344 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue
@@ -1,11 +1,5 @@
<script>
-import {
- GlIntersectionObserver,
- GlLoadingIcon,
- GlButton,
- GlSearchBoxByType,
- GlLink,
-} from '@gitlab/ui';
+import { GlIntersectionObserver, GlLoadingIcon, GlSearchBoxByType, GlLink } from '@gitlab/ui';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import { mapState, mapGetters, mapActions } from 'vuex';
@@ -17,7 +11,6 @@ export default {
components: {
GlIntersectionObserver,
GlLoadingIcon,
- GlButton,
GlSearchBoxByType,
GlLink,
LabelItem,
@@ -149,21 +142,6 @@ export default {
<template>
<gl-intersection-observer @appear="handleComponentAppear" @disappear="handleComponentDisappear">
<div class="labels-select-contents-list js-labels-list" @keydown="handleKeyDown">
- <div
- v-if="isDropdownVariantSidebar || isDropdownVariantEmbedded"
- class="dropdown-title gl-display-flex gl-align-items-center gl-pt-0 gl-pb-3!"
- data-testid="dropdown-title"
- >
- <span class="flex-grow-1">{{ labelsListTitle }}</span>
- <gl-button
- :aria-label="__('Close')"
- variant="link"
- size="small"
- class="dropdown-header-button gl-p-0!"
- icon="close"
- @click="toggleDropdownContents"
- />
- </div>
<div class="dropdown-input" @click.stop="() => {}">
<gl-search-box-by-type
ref="searchInput"
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value_collapsed.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value_collapsed.vue
new file mode 100644
index 00000000000..122250d1ce7
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value_collapsed.vue
@@ -0,0 +1,55 @@
+<script>
+import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
+import { s__, sprintf } from '~/locale';
+
+export default {
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ components: {
+ GlIcon,
+ },
+ props: {
+ labels: {
+ type: Array,
+ required: true,
+ },
+ },
+ computed: {
+ labelsList() {
+ const labelsString = this.labels.length
+ ? this.labels
+ .slice(0, 5)
+ .map((label) => label.title)
+ .join(', ')
+ : s__('LabelSelect|Labels');
+
+ if (this.labels.length > 5) {
+ return sprintf(s__('LabelSelect|%{labelsString}, and %{remainingLabelCount} more'), {
+ labelsString,
+ remainingLabelCount: this.labels.length - 5,
+ });
+ }
+
+ return labelsString;
+ },
+ },
+ methods: {
+ handleClick() {
+ this.$emit('onValueClick');
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ v-gl-tooltip.left.viewport
+ :title="labelsList"
+ class="sidebar-collapsed-icon"
+ @click="handleClick"
+ >
+ <gl-icon name="labels" />
+ <span>{{ labels.length }}</span>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/create_label.mutation.graphql b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/create_label.mutation.graphql
new file mode 100644
index 00000000000..9aa4f5d165e
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/create_label.mutation.graphql
@@ -0,0 +1,15 @@
+mutation createLabel($title: String!, $color: String, $projectPath: ID, $groupPath: ID) {
+ labelCreate(
+ input: { title: $title, color: $color, projectPath: $projectPath, groupPath: $groupPath }
+ ) {
+ label {
+ id
+ color
+ description
+ descriptionHtml
+ title
+ textColor
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue
index bf30e3cfac5..7728c758e18 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue
@@ -5,13 +5,12 @@ import Vuex, { mapState, mapActions, mapGetters } from 'vuex';
import { isInViewport } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
-import DropdownValueCollapsed from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed.vue';
-
import { DropdownVariant } from './constants';
import DropdownButton from './dropdown_button.vue';
import DropdownContents from './dropdown_contents.vue';
import DropdownTitle from './dropdown_title.vue';
import DropdownValue from './dropdown_value.vue';
+import DropdownValueCollapsed from './dropdown_value_collapsed.vue';
import labelsSelectModule from './store';
Vue.use(Vuex);
@@ -163,7 +162,6 @@ export default {
labelsFilterBasePath: this.labelsFilterBasePath,
labelsFilterParam: this.labelsFilterParam,
labelsListTitle: this.labelsListTitle,
- labelsCreateTitle: this.labelsCreateTitle,
footerCreateLabelTitle: this.footerCreateLabelTitle,
footerManageLabelTitle: this.footerManageLabelTitle,
});
@@ -313,6 +311,7 @@ export default {
v-show="dropdownButtonVisible && showDropdownContents"
ref="dropdownContents"
:render-on-top="!contentIsOnViewport"
+ :labels-create-title="labelsCreateTitle"
/>
</template>
<template v-if="isDropdownVariantStandalone || isDropdownVariantEmbedded">
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/actions.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/actions.js
index 89f96ab916b..2b96b159ca3 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/actions.js
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/actions.js
@@ -28,31 +28,5 @@ export const fetchLabels = ({ state, dispatch }) => {
.catch(() => dispatch('receiveLabelsFailure'));
};
-export const requestCreateLabel = ({ commit }) => commit(types.REQUEST_CREATE_LABEL);
-export const receiveCreateLabelSuccess = ({ commit }) => commit(types.RECEIVE_CREATE_LABEL_SUCCESS);
-export const receiveCreateLabelFailure = ({ commit }) => {
- commit(types.RECEIVE_CREATE_LABEL_FAILURE);
- flash(__('Error creating label.'));
-};
-export const createLabel = ({ state, dispatch }, label) => {
- dispatch('requestCreateLabel');
- axios
- .post(state.labelsManagePath, {
- label,
- })
- .then(({ data }) => {
- if (data.id) {
- dispatch('receiveCreateLabelSuccess');
- dispatch('toggleDropdownContentsCreateView');
- } else {
- // eslint-disable-next-line @gitlab/require-i18n-strings
- throw new Error('Error Creating Label');
- }
- })
- .catch(() => {
- dispatch('receiveCreateLabelFailure');
- });
-};
-
export const updateSelectedLabels = ({ commit }, labels) =>
commit(types.UPDATE_SELECTED_LABELS, { labels });
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/mutation_types.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/mutation_types.js
index 2e044dc3b3c..b8da7a90b36 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/mutation_types.js
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/mutation_types.js
@@ -8,10 +8,6 @@ export const REQUEST_SET_LABELS = 'REQUEST_SET_LABELS';
export const RECEIVE_SET_LABELS_SUCCESS = 'RECEIVE_SET_LABELS_SUCCESS';
export const RECEIVE_SET_LABELS_FAILURE = 'RECEIVE_SET_LABELS_FAILURE';
-export const REQUEST_CREATE_LABEL = 'REQUEST_CREATE_LABEL';
-export const RECEIVE_CREATE_LABEL_SUCCESS = 'RECEIVE_CREATE_LABEL_SUCCESS';
-export const RECEIVE_CREATE_LABEL_FAILURE = 'RECEIVE_CREATE_LABEL_FAILURE';
-
export const TOGGLE_DROPDOWN_BUTTON = 'TOGGLE_DROPDOWN_VISIBILITY';
export const TOGGLE_DROPDOWN_CONTENTS = 'TOGGLE_DROPDOWN_CONTENTS';
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/mutations.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/mutations.js
index 55716e1105e..131c6e6fb57 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/mutations.js
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/mutations.js
@@ -46,17 +46,6 @@ export default {
[types.RECEIVE_SET_LABELS_FAILURE](state) {
state.labelsFetchInProgress = false;
},
-
- [types.REQUEST_CREATE_LABEL](state) {
- state.labelCreateInProgress = true;
- },
- [types.RECEIVE_CREATE_LABEL_SUCCESS](state) {
- state.labelCreateInProgress = false;
- },
- [types.RECEIVE_CREATE_LABEL_FAILURE](state) {
- state.labelCreateInProgress = false;
- },
-
[types.UPDATE_SELECTED_LABELS](state, { labels }) {
// Find the label to update from all the labels
// and change `set` prop value to represent their current state.
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/state.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/state.js
index d66cfed4163..220bab05ed2 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/state.js
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/state.js
@@ -3,7 +3,6 @@ export default () => ({
labels: [],
selectedLabels: [],
labelsListTitle: '',
- labelsCreateTitle: '',
footerCreateLabelTitle: '',
footerManageLabelTitle: '',
dropdownButtonText: '',
diff --git a/app/assets/javascripts/vue_shared/security_reports/components/security_report_download_dropdown.vue b/app/assets/javascripts/vue_shared/security_reports/components/security_report_download_dropdown.vue
index 9e941087da2..5d39d740c07 100644
--- a/app/assets/javascripts/vue_shared/security_reports/components/security_report_download_dropdown.vue
+++ b/app/assets/javascripts/vue_shared/security_reports/components/security_report_download_dropdown.vue
@@ -35,7 +35,7 @@ export default {
<template>
<gl-dropdown
v-gl-tooltip
- :title="s__('SecurityReports|Download results')"
+ :text="s__('SecurityReports|Download results')"
:loading="loading"
icon="download"
size="small"
diff --git a/app/assets/javascripts/webpack.js b/app/assets/javascripts/webpack.js
index 4f558843357..b901f17790f 100644
--- a/app/assets/javascripts/webpack.js
+++ b/app/assets/javascripts/webpack.js
@@ -2,6 +2,9 @@
* This is the first script loaded by webpack's runtime. It is used to manually configure
* config.output.publicPath to account for relative_url_root or CDN settings which cannot be
* baked-in to our webpack bundles.
+ *
+ * Note: This file should be at the top of an entry point and _cannot_ be moved to
+ * e.g. the `window` scope, because it needs to be executed in the scope of webpack.
*/
if (gon && gon.webpack_public_path) {
diff --git a/app/assets/stylesheets/framework/contextual_sidebar_refactoring/contextual_sidebar_variant.scss b/app/assets/stylesheets/framework/contextual_sidebar_refactoring/contextual_sidebar_variant.scss
index 154b8c31e8b..1ea50281204 100644
--- a/app/assets/stylesheets/framework/contextual_sidebar_refactoring/contextual_sidebar_variant.scss
+++ b/app/assets/stylesheets/framework/contextual_sidebar_refactoring/contextual_sidebar_variant.scss
@@ -13,13 +13,49 @@ $top-level-item-color: $purple-900;
box-shadow: none;
}
+&.gl-dark .nav-sidebar .sidebar-sub-level-items {
+ box-shadow: none;
+ border: 1px solid $border-color;
+}
+
+&.gl-dark .sidebar-top-level-items .context-header a .avatar-container.rect-avatar .avatar.s32 {
+ color: $white;
+}
+
&.gl-dark .nav-sidebar li a,
&.gl-dark .toggle-sidebar-button .collapse-text,
&.gl-dark .toggle-sidebar-button .icon-chevron-double-lg-left,
&.gl-dark .toggle-sidebar-button .icon-chevron-double-lg-right,
&.gl-dark .sidebar-top-level-items .context-header a .sidebar-context-title,
-&.gl-dark .nav-sidebar-inner-scroll > div.context-header a .sidebar-context-title {
+&.gl-dark .nav-sidebar-inner-scroll > div.context-header a .sidebar-context-title,
+&.gl-dark .nav-sidebar a.has-sub-items + .sidebar-sub-level-items .fly-out-top-item a,
+&.gl-dark .nav-sidebar a.has-sub-items + .sidebar-sub-level-items .fly-out-top-item a:hover,
+&.gl-dark .nav-sidebar a.has-sub-items + .sidebar-sub-level-items .fly-out-top-item.active a,
+&.gl-dark .nav-sidebar a.has-sub-items + .sidebar-sub-level-items .fly-out-top-item .fly-out-top-item-container {
+ color: $gray-darkest;
+}
+
+&.gl-dark .nav-sidebar a.has-sub-items + .sidebar-sub-level-items .fly-out-top-item a,
+&.gl-dark .nav-sidebar a.has-sub-items + .sidebar-sub-level-items .fly-out-top-item a:hover,
+&.gl-dark .nav-sidebar a.has-sub-items + .sidebar-sub-level-items .fly-out-top-item.active a,
+&.gl-dark .nav-sidebar a.has-sub-items + .sidebar-sub-level-items .fly-out-top-item .fly-out-top-item-container {
+ @include gl-mt-0;
+}
+
+&.gl-dark .nav-sidebar a:not(.has-sub-items) + .sidebar-sub-level-items .fly-out-top-item a,
+&.gl-dark .nav-sidebar a:not(.has-sub-items) + .sidebar-sub-level-items .fly-out-top-item a:hover,
+&.gl-dark .nav-sidebar a:not(.has-sub-items) + .sidebar-sub-level-items .fly-out-top-item.active a,
+&.gl-dark .nav-sidebar a:not(.has-sub-items) + .sidebar-sub-level-items .fly-out-top-item .fly-out-top-item-container {
+ background: $white;
color: $gray-darkest;
+
+ &::before {
+ border-right-color: $white;
+ }
+}
+
+&.gl-dark .nav-sidebar .sidebar-sub-level-items {
+ background-color: $white;
}
&.ui-indigo .nav-sidebar li.active:not(.fly-out-top-item) > a {
@@ -183,7 +219,7 @@ $top-level-item-color: $purple-900;
.avatar.s32 {
@extend .rect-avatar.s32;
- color: $gray-900;
+ //color: $gray-900;
box-shadow: $avatar-box-shadow;
}
}
@@ -226,7 +262,7 @@ $top-level-item-color: $purple-900;
color: $white;
@if $has-sub-items {
- @include gl-mt-n2;
+ @include gl-mt-0;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
} @else {
@@ -244,13 +280,13 @@ $top-level-item-color: $purple-900;
content: '';
display: block;
top: 50%;
- left: $gl-spacing-scale-3/-2;
- margin-top: -$gl-spacing-scale-3;
+ left: -$gl-spacing-scale-2;
+ margin-top: -$gl-spacing-scale-2;
width: 0;
height: 0;
- border-top: $gl-spacing-scale-3 solid transparent;
- border-bottom: $gl-spacing-scale-3 solid transparent;
- border-right: $gl-spacing-scale-3 solid $black;
+ border-top: $gl-spacing-scale-2 solid transparent;
+ border-bottom: $gl-spacing-scale-2 solid transparent;
+ border-right: $gl-spacing-scale-2 solid $black;
}
}
}
@@ -356,6 +392,8 @@ $top-level-item-color: $purple-900;
}
a.has-sub-items + .sidebar-sub-level-items {
+ @include gl-mt-n2;
+
.fly-out-top-item {
@include fly-out-top-item($has-sub-items: true);
}
diff --git a/app/assets/stylesheets/startup/startup-dark.scss b/app/assets/stylesheets/startup/startup-dark.scss
index c6f0b3a2ba7..00a6ee579d8 100644
--- a/app/assets/stylesheets/startup/startup-dark.scss
+++ b/app/assets/stylesheets/startup/startup-dark.scss
@@ -1240,6 +1240,18 @@ input {
body.sidebar-refactoring.gl-dark .nav-sidebar li.active {
box-shadow: none;
}
+body.sidebar-refactoring.gl-dark .nav-sidebar .sidebar-sub-level-items {
+ box-shadow: none;
+ border: 1px solid #404040;
+}
+body.sidebar-refactoring.gl-dark
+ .sidebar-top-level-items
+ .context-header
+ a
+ .avatar-container.rect-avatar
+ .avatar.s32 {
+ color: #333;
+}
body.sidebar-refactoring.gl-dark .nav-sidebar li a,
body.sidebar-refactoring.gl-dark .toggle-sidebar-button .collapse-text,
body.sidebar-refactoring.gl-dark
@@ -1257,9 +1269,91 @@ body.sidebar-refactoring.gl-dark
.nav-sidebar-inner-scroll
> div.context-header
a
- .sidebar-context-title {
+ .sidebar-context-title,
+body.sidebar-refactoring.gl-dark
+ .nav-sidebar
+ a.has-sub-items
+ + .sidebar-sub-level-items
+ .fly-out-top-item
+ a,
+body.sidebar-refactoring.gl-dark
+ .nav-sidebar
+ a.has-sub-items
+ + .sidebar-sub-level-items
+ .fly-out-top-item.active
+ a,
+body.sidebar-refactoring.gl-dark
+ .nav-sidebar
+ a.has-sub-items
+ + .sidebar-sub-level-items
+ .fly-out-top-item
+ .fly-out-top-item-container {
+ color: #c4c4c4;
+}
+body.sidebar-refactoring.gl-dark
+ .nav-sidebar
+ a.has-sub-items
+ + .sidebar-sub-level-items
+ .fly-out-top-item
+ a,
+body.sidebar-refactoring.gl-dark
+ .nav-sidebar
+ a.has-sub-items
+ + .sidebar-sub-level-items
+ .fly-out-top-item.active
+ a,
+body.sidebar-refactoring.gl-dark
+ .nav-sidebar
+ a.has-sub-items
+ + .sidebar-sub-level-items
+ .fly-out-top-item
+ .fly-out-top-item-container {
+ margin-top: 0;
+}
+body.sidebar-refactoring.gl-dark
+ .nav-sidebar
+ a:not(.has-sub-items)
+ + .sidebar-sub-level-items
+ .fly-out-top-item
+ a,
+body.sidebar-refactoring.gl-dark
+ .nav-sidebar
+ a:not(.has-sub-items)
+ + .sidebar-sub-level-items
+ .fly-out-top-item.active
+ a,
+body.sidebar-refactoring.gl-dark
+ .nav-sidebar
+ a:not(.has-sub-items)
+ + .sidebar-sub-level-items
+ .fly-out-top-item
+ .fly-out-top-item-container {
+ background: #333;
color: #c4c4c4;
}
+body.sidebar-refactoring.gl-dark
+ .nav-sidebar
+ a:not(.has-sub-items)
+ + .sidebar-sub-level-items
+ .fly-out-top-item
+ a::before,
+body.sidebar-refactoring.gl-dark
+ .nav-sidebar
+ a:not(.has-sub-items)
+ + .sidebar-sub-level-items
+ .fly-out-top-item.active
+ a::before,
+body.sidebar-refactoring.gl-dark
+ .nav-sidebar
+ a:not(.has-sub-items)
+ + .sidebar-sub-level-items
+ .fly-out-top-item
+ .fly-out-top-item-container::before {
+ border-right-color: #333;
+}
+body.sidebar-refactoring.gl-dark .nav-sidebar .sidebar-sub-level-items {
+ background-color: #333;
+}
body.sidebar-refactoring.ui-indigo
.nav-sidebar
li.active:not(.fly-out-top-item)
@@ -1482,12 +1576,18 @@ body.sidebar-refactoring
display: block;
top: 50%;
left: -0.25rem;
- margin-top: -0.5rem;
+ margin-top: -0.25rem;
width: 0;
height: 0;
- border-top: 0.5rem solid transparent;
- border-bottom: 0.5rem solid transparent;
- border-right: 0.5rem solid #fff;
+ border-top: 0.25rem solid transparent;
+ border-bottom: 0.25rem solid transparent;
+ border-right: 0.25rem solid #fff;
+}
+body.sidebar-refactoring
+ .nav-sidebar
+ a.has-sub-items
+ + .sidebar-sub-level-items {
+ margin-top: -0.25rem;
}
body.sidebar-refactoring
.nav-sidebar
@@ -1523,7 +1623,7 @@ body.sidebar-refactoring
font-size: 0.75rem;
background-color: #2f2a6b;
color: #333;
- margin-top: -0.25rem;
+ margin-top: 0;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
@@ -1691,7 +1791,6 @@ body.sidebar-refactoring
a
.avatar-container.rect-avatar
.avatar.s32 {
- color: #fafafa;
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.08);
}
body.sidebar-refactoring
@@ -1732,7 +1831,6 @@ body.sidebar-refactoring
a
.avatar-container.rect-avatar
.avatar.s32 {
- color: #fafafa;
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.08);
}
body.sidebar-refactoring
diff --git a/app/assets/stylesheets/startup/startup-general.scss b/app/assets/stylesheets/startup/startup-general.scss
index a05e27b6af0..4605b6de563 100644
--- a/app/assets/stylesheets/startup/startup-general.scss
+++ b/app/assets/stylesheets/startup/startup-general.scss
@@ -1444,12 +1444,18 @@ body.sidebar-refactoring
display: block;
top: 50%;
left: -0.25rem;
- margin-top: -0.5rem;
+ margin-top: -0.25rem;
width: 0;
height: 0;
- border-top: 0.5rem solid transparent;
- border-bottom: 0.5rem solid transparent;
- border-right: 0.5rem solid #000;
+ border-top: 0.25rem solid transparent;
+ border-bottom: 0.25rem solid transparent;
+ border-right: 0.25rem solid #000;
+}
+body.sidebar-refactoring
+ .nav-sidebar
+ a.has-sub-items
+ + .sidebar-sub-level-items {
+ margin-top: -0.25rem;
}
body.sidebar-refactoring
.nav-sidebar
@@ -1485,7 +1491,7 @@ body.sidebar-refactoring
font-size: 0.75rem;
background-color: #2f2a6b;
color: #fff;
- margin-top: -0.25rem;
+ margin-top: 0;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
@@ -1653,7 +1659,6 @@ body.sidebar-refactoring
a
.avatar-container.rect-avatar
.avatar.s32 {
- color: #303030;
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.08);
}
body.sidebar-refactoring
@@ -1694,7 +1699,6 @@ body.sidebar-refactoring
a
.avatar-container.rect-avatar
.avatar.s32 {
- color: #303030;
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.08);
}
body.sidebar-refactoring
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index 5ddeb9630ba..7960e5d64d0 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -208,7 +208,10 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
params[:application_setting][:import_sources]&.delete("")
params[:application_setting][:restricted_visibility_levels]&.delete("")
- params[:application_setting][:required_instance_ci_template] = nil if params[:application_setting][:required_instance_ci_template].blank?
+
+ if params[:application_setting].key?(:required_instance_ci_template)
+ params[:application_setting][:required_instance_ci_template] = nil if params[:application_setting][:required_instance_ci_template].empty?
+ end
remove_blank_params_for!(:elasticsearch_aws_secret_access_key, :eks_secret_access_key)
@@ -217,9 +220,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
params.delete(:domain_denylist_raw) if params[:domain_denylist]
params.delete(:domain_allowlist_raw) if params[:domain_allowlist]
- params.require(:application_setting).permit(
- visible_application_setting_attributes
- )
+ params[:application_setting].permit(visible_application_setting_attributes)
end
def recheck_user_consent?
diff --git a/app/controllers/admin/cohorts_controller.rb b/app/controllers/admin/cohorts_controller.rb
index c29b5224b09..8163f062b62 100644
--- a/app/controllers/admin/cohorts_controller.rb
+++ b/app/controllers/admin/cohorts_controller.rb
@@ -1,11 +1,28 @@
# frozen_string_literal: true
class Admin::CohortsController < Admin::ApplicationController
+ include Analytics::UniqueVisitsHelper
+
feature_category :devops_reports
- # Backwards compatibility. Remove it and routing in 14.0
- # @see https://gitlab.com/gitlab-org/gitlab/-/issues/299303
def index
- redirect_to cohorts_admin_users_path
+ @cohorts = load_cohorts
+ track_cohorts_visit
+ end
+
+ private
+
+ def load_cohorts
+ cohorts_results = Rails.cache.fetch('cohorts', expires_in: 1.day) do
+ CohortsService.new.execute
+ end
+
+ CohortsSerializer.new.represent(cohorts_results)
+ end
+
+ def track_cohorts_visit
+ if request.format.html? && request.headers['DNT'] != '1'
+ track_visit('i_analytics_cohorts')
+ end
end
end
diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb
index e397ecbadaf..700acc46d8d 100644
--- a/app/controllers/admin/users_controller.rb
+++ b/app/controllers/admin/users_controller.rb
@@ -2,9 +2,8 @@
class Admin::UsersController < Admin::ApplicationController
include RoutableActions
- include Analytics::UniqueVisitsHelper
- before_action :user, except: [:index, :cohorts, :new, :create]
+ before_action :user, except: [:index, :new, :create]
before_action :check_impersonation_availability, only: :impersonate
before_action :ensure_destroy_prerequisites_met, only: [:destroy]
before_action :check_ban_user_feature_flag, only: [:ban]
@@ -14,7 +13,7 @@ class Admin::UsersController < Admin::ApplicationController
PAGINATION_WITH_COUNT_LIMIT = 1000
def index
- return redirect_to cohorts_admin_users_path if params[:tab] == 'cohorts'
+ return redirect_to admin_cohorts_path if params[:tab] == 'cohorts'
@users = User.filter_items(params[:filter]).order_name_asc
@users = @users.search_with_secondary_emails(params[:search_query]) if params[:search_query].present?
@@ -24,11 +23,6 @@ class Admin::UsersController < Admin::ApplicationController
@users = @users.without_count if paginate_without_count?
end
- def cohorts
- @cohorts = load_cohorts
- track_cohorts_visit
- end
-
def show
end
@@ -376,20 +370,6 @@ class Admin::UsersController < Admin::ApplicationController
def log_impersonation_event
Gitlab::AppLogger.info(_("User %{current_user_username} has started impersonating %{username}") % { current_user_username: current_user.username, username: user.username })
end
-
- def load_cohorts
- cohorts_results = Rails.cache.fetch('cohorts', expires_in: 1.day) do
- CohortsService.new.execute
- end
-
- CohortsSerializer.new.represent(cohorts_results)
- end
-
- def track_cohorts_visit
- if request.format.html? && request.headers['DNT'] != '1'
- track_visit('i_analytics_cohorts')
- end
- end
end
Admin::UsersController.prepend_mod_with('Admin::UsersController')
diff --git a/app/controllers/confirmations_controller.rb b/app/controllers/confirmations_controller.rb
index 560369a8de4..0b833e149a4 100644
--- a/app/controllers/confirmations_controller.rb
+++ b/app/controllers/confirmations_controller.rb
@@ -13,7 +13,7 @@ class ConfirmationsController < Devise::ConfirmationsController
protected
def after_resending_confirmation_instructions_path_for(resource)
- return users_almost_there_path(email: resource.email) unless Feature.enabled?(:soft_email_confirmation)
+ return users_almost_there_path unless Feature.enabled?(:soft_email_confirmation)
stored_location_for(resource) || dashboard_projects_path
end
diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb
index 76de9a83c87..8519841ee16 100644
--- a/app/controllers/projects/environments_controller.rb
+++ b/app/controllers/projects/environments_controller.rb
@@ -15,9 +15,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
push_frontend_feature_flag(:prometheus_computed_alerts)
push_frontend_feature_flag(:disable_metric_dashboard_refresh_rate)
end
- before_action do
- push_frontend_feature_flag(:canary_ingress_weight_control, default_enabled: true)
- end
+
before_action :authorize_read_environment!, except: [:metrics, :additional_metrics, :metrics_dashboard, :metrics_redirect]
before_action :authorize_create_environment!, only: [:new, :create]
before_action :authorize_stop_environment!, only: [:stop]
diff --git a/app/controllers/projects/merge_requests/content_controller.rb b/app/controllers/projects/merge_requests/content_controller.rb
index dfc060c9204..399745151b1 100644
--- a/app/controllers/projects/merge_requests/content_controller.rb
+++ b/app/controllers/projects/merge_requests/content_controller.rb
@@ -14,8 +14,6 @@ class Projects::MergeRequests::ContentController < Projects::MergeRequests::Appl
SLOW_POLLING_INTERVAL = 5.minutes.in_milliseconds
def widget
- check_mergeability_async!
-
respond_to do |format|
format.json do
render json: serializer(MergeRequestPollWidgetEntity)
@@ -40,13 +38,6 @@ class Projects::MergeRequests::ContentController < Projects::MergeRequests::Appl
def serializer(entity)
serializer = MergeRequestSerializer.new(current_user: current_user, project: merge_request.project)
- serializer.represent(merge_request, { async_mergeability_check: params[:async_mergeability_check] }, entity)
- end
-
- def check_mergeability_async!
- return unless Feature.enabled?(:check_mergeability_async_in_widget, merge_request.project, default_enabled: :yaml)
- return if params[:async_mergeability_check].blank?
-
- merge_request.check_mergeability(async: true)
+ serializer.represent(merge_request, {}, entity)
end
end
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index 91920277c50..7690773354f 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -190,7 +190,6 @@ module IssuesHelper
email: current_user&.notification_email,
emails_help_page_path: help_page_path('development/emails', anchor: 'email-namespace'),
empty_state_svg_path: image_path('illustrations/issues.svg'),
- endpoint: expose_path(api_v4_projects_issues_path(id: project.id)),
export_csv_path: export_csv_project_issues_path(project),
has_project_issues: project_issues(project).exists?.to_s,
import_csv_issues_path: import_csv_namespace_project_issues_path,
diff --git a/app/models/ability.rb b/app/models/ability.rb
index c18bd21d754..6a63a8d46ba 100644
--- a/app/models/ability.rb
+++ b/app/models/ability.rb
@@ -54,7 +54,7 @@ class Ability
end
end
- def allowed?(user, action, subject = :global, opts = {})
+ def allowed?(user, ability, subject = :global, opts = {})
if subject.is_a?(Hash)
opts = subject
subject = :global
@@ -64,21 +64,76 @@ class Ability
case opts[:scope]
when :user
- DeclarativePolicy.user_scope { policy.can?(action) }
+ DeclarativePolicy.user_scope { policy.allowed?(ability) }
when :subject
- DeclarativePolicy.subject_scope { policy.can?(action) }
+ DeclarativePolicy.subject_scope { policy.allowed?(ability) }
else
- policy.can?(action)
+ policy.allowed?(ability)
end
+ ensure
+ # TODO: replace with runner invalidation:
+ # See: https://gitlab.com/gitlab-org/declarative-policy/-/merge_requests/24
+ # See: https://gitlab.com/gitlab-org/declarative-policy/-/merge_requests/25
+ forget_runner_result(policy.runner(ability)) if policy && ability_forgetting?
end
def policy_for(user, subject = :global)
- cache = Gitlab::SafeRequestStore.active? ? Gitlab::SafeRequestStore : {}
- DeclarativePolicy.policy_for(user, subject, cache: cache)
+ DeclarativePolicy.policy_for(user, subject, cache: ::Gitlab::SafeRequestStore.storage)
+ end
+
+ # This method is something of a band-aid over the problem. The problem is
+ # that some conditions may not be re-entrant, if facts change.
+ # (`BasePolicy#admin?` is a known offender, due to the effects of
+ # `admin_mode`)
+ #
+ # To deal with this we need to clear two elements of state: the offending
+ # conditions (selected by 'pattern') and the cached ability checks (cached
+ # on the `policy#runner(ability)`).
+ #
+ # Clearing the conditions (see `forget_all_but`) is fairly robust, provided
+ # the pattern is not _under_-selective. Clearing the runners is harder,
+ # since there is not good way to know which abilities any given condition
+ # may affect. The approach taken here (see `forget_runner_result`) is to
+ # discard all runner results generated during a `forgetting` block. This may
+ # be _under_-selective if a runner prior to this block cached a state value
+ # that might now be invalid.
+ #
+ # TODO: add some kind of reverse-dependency mapping in DeclarativePolicy
+ # See: https://gitlab.com/gitlab-org/declarative-policy/-/issues/14
+ def forgetting(pattern, &block)
+ was_forgetting = ability_forgetting?
+ ::Gitlab::SafeRequestStore[:ability_forgetting] = true
+ keys_before = ::Gitlab::SafeRequestStore.storage.keys
+
+ yield
+ ensure
+ ::Gitlab::SafeRequestStore[:ability_forgetting] = was_forgetting
+ forget_all_but(keys_before, matching: pattern)
end
private
+ def ability_forgetting?
+ ::Gitlab::SafeRequestStore[:ability_forgetting]
+ end
+
+ def forget_all_but(keys_before, matching:)
+ keys_after = ::Gitlab::SafeRequestStore.storage.keys
+
+ added_keys = keys_after - keys_before
+ added_keys.each do |key|
+ if key.is_a?(String) && key.start_with?('/dp') && key =~ matching
+ ::Gitlab::SafeRequestStore.delete(key)
+ end
+ end
+ end
+
+ def forget_runner_result(runner)
+ # TODO: add support in DP for this
+ # See: https://gitlab.com/gitlab-org/declarative-policy/-/issues/15
+ runner.instance_variable_set(:@state, nil)
+ end
+
def apply_filters_if_needed(elements, user, filters)
filters.each do |ability, filter|
elements = filter.call(elements) unless allowed?(user, ability)
diff --git a/app/models/analytics/cycle_analytics/project_level.rb b/app/models/analytics/cycle_analytics/project_level.rb
index 7a73bc75ed6..d43793f60c9 100644
--- a/app/models/analytics/cycle_analytics/project_level.rb
+++ b/app/models/analytics/cycle_analytics/project_level.rb
@@ -47,3 +47,4 @@ module Analytics
end
end
end
+Analytics::CycleAnalytics::ProjectLevel.prepend_mod_with('Analytics::CycleAnalytics::ProjectLevel')
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index ae06bea5a02..159d9d10878 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -1257,7 +1257,7 @@ module Ci
end
def build_matchers
- self.builds.build_matchers(project)
+ self.builds.latest.build_matchers(project)
end
private
diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb
index da5f4cc1862..7f5f87e3e36 100644
--- a/app/models/clusters/platforms/kubernetes.rb
+++ b/app/models/clusters/platforms/kubernetes.rb
@@ -98,11 +98,7 @@ module Clusters
pods = read_pods(environment.deployment_namespace)
deployments = read_deployments(environment.deployment_namespace)
- ingresses = if ::Feature.enabled?(:canary_ingress_weight_control, environment.project, default_enabled: true)
- read_ingresses(environment.deployment_namespace)
- else
- []
- end
+ ingresses = read_ingresses(environment.deployment_namespace)
# extract only the data required for display to avoid unnecessary caching
{
diff --git a/app/models/container_expiration_policy.rb b/app/models/container_expiration_policy.rb
index 0441a5f0f5b..9bacd9a0edf 100644
--- a/app/models/container_expiration_policy.rb
+++ b/app/models/container_expiration_policy.rb
@@ -38,6 +38,16 @@ class ContainerExpirationPolicy < ApplicationRecord
)
end
+ def self.without_container_repositories
+ where.not(
+ 'EXISTS(?)',
+ ContainerRepository.select(1)
+ .where(
+ 'container_repositories.project_id = container_expiration_policies.project_id'
+ )
+ )
+ end
+
def self.keep_n_options
{
1 => _('%{tags} tag per image name') % { tags: 1 },
diff --git a/app/models/integration.rb b/app/models/integration.rb
index 238ecbbf209..2fbcdc7f1cb 100644
--- a/app/models/integration.rb
+++ b/app/models/integration.rb
@@ -44,6 +44,10 @@ class Integration < ApplicationRecord
bamboo bugzilla buildkite
campfire confluence custom_issue_tracker
datadog discord drone_ci
+ emails_on_push ewm emails_on_push external_wiki
+ flowdock
+ hangouts_chat
+ irker
].to_set.freeze
def self.renamed?(name)
diff --git a/app/models/issue.rb b/app/models/issue.rb
index b0a126c4442..48f388ea48d 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -23,6 +23,7 @@ class Issue < ApplicationRecord
include IssueAvailableFeatures
include Todoable
include FromUnion
+ include EachBatch
extend ::Gitlab::Utils::Override
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 15f112690d5..68fb957759d 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -263,8 +263,9 @@ class MergeRequest < ApplicationRecord
scope :by_milestone, ->(milestone) { where(milestone_id: milestone) }
scope :of_projects, ->(ids) { where(target_project_id: ids) }
scope :from_project, ->(project) { where(source_project_id: project.id) }
+ scope :from_fork, -> { where('source_project_id <> target_project_id') }
scope :from_and_to_forks, ->(project) do
- where('source_project_id <> target_project_id AND (source_project_id = ? OR target_project_id = ?)', project.id, project.id)
+ from_fork.where('source_project_id = ? OR target_project_id = ?', project.id, project.id)
end
scope :merged, -> { with_state(:merged) }
scope :closed_and_merged, -> { with_states(:closed, :merged) }
diff --git a/app/models/packages/package.rb b/app/models/packages/package.rb
index 7b0bb72940e..b040c98ef09 100644
--- a/app/models/packages/package.rb
+++ b/app/models/packages/package.rb
@@ -158,7 +158,7 @@ class Packages::Package < ApplicationRecord
joins(:project).reorder(keyset_order)
end
- after_commit :update_composer_cache, on: :destroy, if: -> { composer? }
+ after_commit :update_composer_cache, on: :destroy, if: -> { composer? && Feature.disabled?(:disable_composer_callback) }
def self.only_maven_packages_with_path(path, use_cte: false)
if use_cte
diff --git a/app/models/project.rb b/app/models/project.rb
index 735dc185575..1f8e8b81015 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -166,12 +166,12 @@ class Project < ApplicationRecord
has_one :datadog_integration, class_name: 'Integrations::Datadog'
has_one :discord_integration, class_name: 'Integrations::Discord'
has_one :drone_ci_integration, class_name: 'Integrations::DroneCi'
- has_one :emails_on_push_service, class_name: 'Integrations::EmailsOnPush'
- has_one :ewm_service, class_name: 'Integrations::Ewm'
- has_one :external_wiki_service, class_name: 'Integrations::ExternalWiki'
- has_one :flowdock_service, class_name: 'Integrations::Flowdock'
- has_one :hangouts_chat_service, class_name: 'Integrations::HangoutsChat'
- has_one :irker_service, class_name: 'Integrations::Irker'
+ has_one :emails_on_push_integration, class_name: 'Integrations::EmailsOnPush'
+ has_one :ewm_integration, class_name: 'Integrations::Ewm'
+ has_one :external_wiki_integration, class_name: 'Integrations::ExternalWiki'
+ has_one :flowdock_integration, class_name: 'Integrations::Flowdock'
+ has_one :hangouts_chat_integration, class_name: 'Integrations::HangoutsChat'
+ has_one :irker_integration, class_name: 'Integrations::Irker'
has_one :jenkins_service, class_name: 'Integrations::Jenkins'
has_one :jira_service, class_name: 'Integrations::Jira'
has_one :mattermost_service, class_name: 'Integrations::Mattermost'
@@ -825,6 +825,21 @@ class Project < ApplicationRecord
from_union([with_issues_enabled, with_merge_requests_enabled]).select(:id)
end
+
+ def find_by_url(url)
+ uri = URI(url)
+
+ return unless uri.host == Gitlab.config.gitlab.host
+
+ match = Rails.application.routes.recognize_path(url)
+
+ return if match[:unmatched_route].present?
+ return if match[:namespace_id].blank? || match[:id].blank?
+
+ find_by_full_path(match.values_at(:namespace_id, :id).join("/"))
+ rescue ActionController::RoutingError, URI::InvalidURIError
+ nil
+ end
end
def initialize(attributes = nil)
diff --git a/app/models/remote_mirror.rb b/app/models/remote_mirror.rb
index c3ca90ca0ad..a700f104150 100644
--- a/app/models/remote_mirror.rb
+++ b/app/models/remote_mirror.rb
@@ -100,10 +100,11 @@ class RemoteMirror < ApplicationRecord
update_status == 'started'
end
- def update_repository
+ def update_repository(inmemory_remote:)
Gitlab::Git::RemoteMirror.new(
project.repository.raw,
remote_name,
+ inmemory_remote ? remote_url : nil,
**options_for_update
).update
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 8ee0421e45f..5fbd6271589 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -84,10 +84,11 @@ class User < ApplicationRecord
update_tracked_fields(request)
- lease = Gitlab::ExclusiveLease.new("user_update_tracked_fields:#{id}", timeout: 1.hour.to_i)
- return unless lease.try_obtain
-
- Users::UpdateService.new(self, user: self).execute(validate: false)
+ Gitlab::ExclusiveLease.throttle(id) do
+ ::Ability.forgetting(/admin/) do
+ Users::UpdateService.new(self, user: self).execute(validate: false)
+ end
+ end
end
# rubocop: enable CodeReuse/ServiceClass
@@ -1868,6 +1869,12 @@ class User < ApplicationRecord
!!(password_expires_at && password_expires_at < Time.current)
end
+ def password_expired_if_applicable?
+ return false unless allow_password_authentication?
+
+ password_expired?
+ end
+
def can_be_deactivated?
active? && no_recent_activity? && !internal?
end
diff --git a/app/policies/base_policy.rb b/app/policies/base_policy.rb
index 0f7a6b852ab..77897c5807f 100644
--- a/app/policies/base_policy.rb
+++ b/app/policies/base_policy.rb
@@ -27,6 +27,10 @@ class BasePolicy < DeclarativePolicy::Base
with_options scope: :user, score: 0
condition(:security_bot) { @user&.security_bot? }
+ desc "User is automation bot"
+ with_options scope: :user, score: 0
+ condition(:automation_bot) { @user&.automation_bot? }
+
desc "User email is unconfirmed or user account is locked"
with_options scope: :user, score: 0
condition(:inactive) { @user&.confirmation_required_on_sign_in? || @user&.access_locked? }
@@ -63,7 +67,7 @@ class BasePolicy < DeclarativePolicy::Base
rule { default }.enable :read_cross_project
- condition(:is_gitlab_com) { ::Gitlab.dev_env_or_com? }
+ condition(:is_gitlab_com, score: 0, scope: :global) { ::Gitlab.dev_env_or_com? }
end
BasePolicy.prepend_mod_with('BasePolicy')
diff --git a/app/policies/concerns/policy_actor.rb b/app/policies/concerns/policy_actor.rb
index cbc34bdeed3..8fa09683b06 100644
--- a/app/policies/concerns/policy_actor.rb
+++ b/app/policies/concerns/policy_actor.rb
@@ -53,6 +53,10 @@ module PolicyActor
false
end
+ def automation_bot?
+ false
+ end
+
def deactivated?
false
end
@@ -81,7 +85,7 @@ module PolicyActor
false
end
- def password_expired?
+ def password_expired_if_applicable?
false
end
diff --git a/app/policies/global_policy.rb b/app/policies/global_policy.rb
index 35d38bac7fa..c3b4b163cb4 100644
--- a/app/policies/global_policy.rb
+++ b/app/policies/global_policy.rb
@@ -16,7 +16,7 @@ class GlobalPolicy < BasePolicy
end
condition(:password_expired, scope: :user) do
- @user&.password_expired?
+ @user&.password_expired_if_applicable?
end
condition(:project_bot, scope: :user) { @user&.project_bot? }
diff --git a/app/serializers/merge_request_poll_widget_entity.rb b/app/serializers/merge_request_poll_widget_entity.rb
index c00dceadf22..3ce67d92af1 100644
--- a/app/serializers/merge_request_poll_widget_entity.rb
+++ b/app/serializers/merge_request_poll_widget_entity.rb
@@ -31,7 +31,6 @@ class MergeRequestPollWidgetEntity < Grape::Entity
expose :mergeable do |merge_request, options|
next merge_request.mergeable? if Feature.disabled?(:check_mergeability_async_in_widget, merge_request.project, default_enabled: :yaml)
- next false if options[:async_mergeability_check].present? && merge_request.checking?
merge_request.mergeable?
end
diff --git a/app/serializers/merge_request_widget_entity.rb b/app/serializers/merge_request_widget_entity.rb
index ac9970579ed..0616d94a1ed 100644
--- a/app/serializers/merge_request_widget_entity.rb
+++ b/app/serializers/merge_request_widget_entity.rb
@@ -36,7 +36,7 @@ class MergeRequestWidgetEntity < Grape::Entity
end
expose :merge_request_widget_path do |merge_request|
- widget_project_json_merge_request_path(merge_request.target_project, merge_request, async_mergeability_check: true, format: :json)
+ widget_project_json_merge_request_path(merge_request.target_project, merge_request, format: :json)
end
expose :merge_request_cached_widget_path do |merge_request|
diff --git a/app/services/environments/canary_ingress/update_service.rb b/app/services/environments/canary_ingress/update_service.rb
index 2b510280873..f9813e5e86d 100644
--- a/app/services/environments/canary_ingress/update_service.rb
+++ b/app/services/environments/canary_ingress/update_service.rb
@@ -34,10 +34,6 @@ module Environments
private
def validate(environment)
- unless Feature.enabled?(:canary_ingress_weight_control, environment.project, default_enabled: true)
- return error(_("Feature flag is not enabled on the environment's project."))
- end
-
unless can?(current_user, :update_environment, environment)
return error(_('You do not have permission to update the environment.'))
end
diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb
index 3a4e3ba38fd..f7a0f90b95f 100644
--- a/app/services/merge_requests/refresh_service.rb
+++ b/app/services/merge_requests/refresh_service.rb
@@ -294,14 +294,14 @@ module MergeRequests
@source_merge_requests ||= merge_requests_for(@push.branch_name)
end
- # rubocop: disable CodeReuse/ActiveRecord
def merge_requests_for_forks
@merge_requests_for_forks ||=
- MergeRequest.opened
- .where(source_branch: @push.branch_name, source_project: @project)
- .where.not(target_project: @project)
+ MergeRequest
+ .opened
+ .from_project(project)
+ .from_source_branches(@push.branch_name)
+ .from_fork
end
- # rubocop: enable CodeReuse/ActiveRecord
end
end
diff --git a/app/services/packages/helm/process_file_service.rb b/app/services/packages/helm/process_file_service.rb
new file mode 100644
index 00000000000..31b357c1616
--- /dev/null
+++ b/app/services/packages/helm/process_file_service.rb
@@ -0,0 +1,97 @@
+# frozen_string_literal: true
+
+module Packages
+ module Helm
+ class ProcessFileService
+ include Gitlab::Utils::StrongMemoize
+ include ExclusiveLeaseGuard
+
+ ExtractionError = Class.new(StandardError)
+ DEFAULT_LEASE_TIMEOUT = 1.hour.to_i
+
+ def initialize(channel, package_file)
+ @channel = channel
+ @package_file = package_file
+ end
+
+ def execute
+ raise ExtractionError, 'Helm chart was not processed - package_file is not set' unless package_file
+
+ try_obtain_lease do
+ temp_package.transaction do
+ rename_package_and_set_version
+ rename_package_file_and_set_metadata
+ cleanup_temp_package
+ end
+ end
+ end
+
+ private
+
+ attr_reader :channel, :package_file
+
+ def rename_package_and_set_version
+ package.update!(
+ name: metadata['name'],
+ version: metadata['version'],
+ status: :default
+ )
+ end
+
+ def rename_package_file_and_set_metadata
+ # Updating file_name updates the path where the file is stored.
+ # We must pass the file again so that CarrierWave can handle the update
+ package_file.update!(
+ file_name: file_name,
+ file: package_file.file,
+ package_id: package.id,
+ helm_file_metadatum_attributes: {
+ channel: channel,
+ metadata: metadata
+ }
+ )
+ end
+
+ def cleanup_temp_package
+ temp_package.destroy if package.id != temp_package.id
+ end
+
+ def temp_package
+ strong_memoize(:temp_package) do
+ package_file.package
+ end
+ end
+
+ def package
+ strong_memoize(:package) do
+ project_packages = package_file.package.project.packages
+ package = project_packages.with_package_type(:helm)
+ .with_name(metadata['name'])
+ .with_version(metadata['version'])
+ .last
+ package || temp_package
+ end
+ end
+
+ def metadata
+ strong_memoize(:metadata) do
+ ::Packages::Helm::ExtractFileMetadataService.new(package_file).execute
+ end
+ end
+
+ def file_name
+ "#{metadata['name']}-#{metadata['version']}.tgz"
+ end
+
+ # used by ExclusiveLeaseGuard
+ def lease_key
+ "packages:helm:process_file_service:package_file:#{package_file.id}"
+ end
+
+ # used by ExclusiveLeaseGuard
+ def lease_timeout
+ DEFAULT_LEASE_TIMEOUT
+ end
+ end
+ end
+end
diff --git a/app/services/projects/update_remote_mirror_service.rb b/app/services/projects/update_remote_mirror_service.rb
index 9f4f6133d92..eac84337967 100644
--- a/app/services/projects/update_remote_mirror_service.rb
+++ b/app/services/projects/update_remote_mirror_service.rb
@@ -39,12 +39,16 @@ module Projects
def update_mirror(remote_mirror)
remote_mirror.update_start!
- remote_mirror.ensure_remote!
# LFS objects must be sent first, or the push has dangling pointers
send_lfs_objects!(remote_mirror)
- response = remote_mirror.update_repository
+ response = if Feature.enabled?(:update_remote_mirror_inmemory, project, default_enabled: :yaml)
+ remote_mirror.update_repository(inmemory_remote: true)
+ else
+ remote_mirror.ensure_remote!
+ remote_mirror.update_repository(inmemory_remote: false)
+ end
if response.divergent_refs.any?
message = "Some refs have diverged and have not been updated on the remote:"
diff --git a/app/services/users/update_service.rb b/app/services/users/update_service.rb
index ff08c806319..23c67231a29 100644
--- a/app/services/users/update_service.rb
+++ b/app/services/users/update_service.rb
@@ -17,6 +17,7 @@ module Users
yield(@user) if block_given?
user_exists = @user.persisted?
+ @user.user_detail # prevent assignment
discard_read_only_attributes
assign_attributes
diff --git a/app/views/admin/users/_cohorts.html.haml b/app/views/admin/cohorts/_cohorts.html.haml
index 25b30adc5be..25b30adc5be 100644
--- a/app/views/admin/users/_cohorts.html.haml
+++ b/app/views/admin/cohorts/_cohorts.html.haml
diff --git a/app/views/admin/users/_cohorts_table.html.haml b/app/views/admin/cohorts/_cohorts_table.html.haml
index a92cfb5851a..a92cfb5851a 100644
--- a/app/views/admin/users/_cohorts_table.html.haml
+++ b/app/views/admin/cohorts/_cohorts_table.html.haml
diff --git a/app/views/admin/users/cohorts.html.haml b/app/views/admin/cohorts/index.html.haml
index 3f3d22fa410..7ba4cd6d733 100644
--- a/app/views/admin/users/cohorts.html.haml
+++ b/app/views/admin/cohorts/index.html.haml
@@ -1,6 +1,6 @@
- page_title _("Users")
-= render 'tabs'
+= render 'admin/users/tabs'
.tab-content
.tab-pane.active
diff --git a/app/views/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml
index d03a782756b..6f3c16f7abf 100644
--- a/app/views/admin/runners/show.html.haml
+++ b/app/views/admin/runners/show.html.haml
@@ -28,12 +28,14 @@
%tr
%td
.gl-alert.gl-alert-danger
- = sprite_icon('error', size: 16, css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
- .gl-alert-body
- %strong
- = project.full_name
- .gl-alert-actions
- = link_to s_('Disable'), admin_namespace_project_runner_project_path(project.namespace, project, runner_project), method: :delete, class: 'btn gl-alert-action btn-info btn-md gl-button'
+ .gl-alert-container
+ = sprite_icon('error', size: 16, css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
+ .gl-alert-content
+ .gl-alert-body
+ %strong
+ = project.full_name
+ .gl-alert-actions
+ = link_to s_('Disable'), admin_namespace_project_runner_project_path(project.namespace, project, runner_project), method: :delete, class: 'btn gl-alert-action btn-confirm btn-md gl-button'
%table.table{ data: { testid: 'unassigned-projects' } }
%thead
diff --git a/app/views/admin/users/_tabs.html.haml b/app/views/admin/users/_tabs.html.haml
index 1a3239897eb..90f06eeaf3f 100644
--- a/app/views/admin/users/_tabs.html.haml
+++ b/app/views/admin/users/_tabs.html.haml
@@ -3,5 +3,5 @@
%a.nav-link{ href: admin_users_path, class: active_when(current_page?(admin_users_path)), role: 'tab' }
= s_('AdminUsers|Users')
%li.nav-item{ role: 'presentation' }
- %a.nav-link{ href: cohorts_admin_users_path, class: active_when(current_page?(cohorts_admin_users_path)), role: 'tab' }
+ %a.nav-link{ href: admin_cohorts_path, class: active_when(current_page?(admin_cohorts_path)), role: 'tab' }
= s_('AdminUsers|Cohorts')
diff --git a/app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml b/app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml
index 5df368ef3af..81f4be9fce5 100644
--- a/app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml
+++ b/app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml
@@ -1,9 +1,11 @@
- link = link_to(s_('ClusterIntegration|sign up'), 'https://console.cloud.google.com/freetrial?utm_campaign=2018_cpanel&utm_source=gitlab&utm_medium=referral', target: '_blank', rel: 'noopener noreferrer')
.gcp-signup-offer.gl-alert.gl-alert-info.gl-my-3{ role: 'alert', data: { feature_id: UserCalloutsHelper::GCP_SIGNUP_OFFER, dismiss_endpoint: user_callouts_path } }
- %button.js-close.gl-alert-dismiss{ type: 'button', 'aria-label' => _('Dismiss') }
- = sprite_icon('close', size: 16, css_class: 'gl-icon')
- = sprite_icon('information-o', size: 16, css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
- %h4.gl-alert-title= s_('ClusterIntegration|Did you know?')
- %p.gl-alert-body= s_('ClusterIntegration|Every new Google Cloud Platform (GCP) account receives $300 in credit upon %{sign_up_link}. In partnership with Google, GitLab is able to offer an additional $200 for both new and existing GCP accounts to get started with GitLab\'s Google Kubernetes Engine Integration.').html_safe % { sign_up_link: link }
- %a.gl-button.btn-confirm.text-decoration-none{ href: 'https://cloud.google.com/partners/partnercredit/?pcn_code=0014M00001h35gDQAQ#contact-form', target: '_blank', rel: 'noopener noreferrer' }
- = s_("ClusterIntegration|Apply for credit")
+ .gl-alert-container
+ %button.js-close.btn.gl-dismiss-btn.btn-default.btn-sm.gl-button.btn-default-tertiary.btn-icon{ type: 'button', 'aria-label' => _('Dismiss') }
+ = sprite_icon('close', size: 16, css_class: 'gl-icon')
+ = sprite_icon('information-o', size: 16, css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
+ .gl-alert-content
+ %h4.gl-alert-title= s_('ClusterIntegration|Did you know?')
+ %p.gl-alert-body= s_('ClusterIntegration|Every new Google Cloud Platform (GCP) account receives $300 in credit upon %{sign_up_link}. In partnership with Google, GitLab is able to offer an additional $200 for both new and existing GCP accounts to get started with GitLab\'s Google Kubernetes Engine Integration.').html_safe % { sign_up_link: link }
+ %a.gl-button.btn-confirm.text-decoration-none{ href: 'https://cloud.google.com/partners/partnercredit/?pcn_code=0014M00001h35gDQAQ#contact-form', target: '_blank', rel: 'noopener noreferrer' }
+ = s_("ClusterIntegration|Apply for credit")
diff --git a/app/views/layouts/nav/sidebar/_admin.html.haml b/app/views/layouts/nav/sidebar/_admin.html.haml
index 7a80c4e0ba9..21c3d7cb7e2 100644
--- a/app/views/layouts/nav/sidebar/_admin.html.haml
+++ b/app/views/layouts/nav/sidebar/_admin.html.haml
@@ -10,14 +10,14 @@
%span.sidebar-context-title
= _('Admin Area')
%ul.sidebar-top-level-items{ data: { qa_selector: 'admin_sidebar_overview_submenu_content' } }
- = nav_link(controller: %w(dashboard admin admin/projects users groups jobs runners gitaly_servers), html_options: {class: 'home'}) do
+ = nav_link(controller: %w(dashboard admin admin/projects users groups jobs runners gitaly_servers cohorts), html_options: {class: 'home'}) do
= link_to admin_root_path, class: 'has-sub-items' do
.nav-icon-container
= sprite_icon('overview')
%span.nav-item-name
= _('Overview')
%ul.sidebar-sub-level-items
- = nav_link(controller: %w(dashboard admin admin/projects users groups jobs runners gitaly_servers), html_options: { class: "fly-out-top-item" } ) do
+ = nav_link(controller: %w(dashboard admin admin/projects users groups jobs runners gitaly_servers cohorts), html_options: { class: "fly-out-top-item" } ) do
= link_to admin_root_path do
%strong.fly-out-top-item-name
= _('Overview')
@@ -30,7 +30,7 @@
= link_to admin_projects_path, title: _('Projects') do
%span
= _('Projects')
- = nav_link(controller: :users) do
+ = nav_link(controller: %w(users cohorts)) do
= link_to admin_users_path, title: _('Users'), data: { qa_selector: 'users_overview_link' } do
%span
= _('Users')
diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml
index 49f2795538c..691ce8dc5fc 100644
--- a/app/views/projects/merge_requests/show.html.haml
+++ b/app/views/projects/merge_requests/show.html.haml
@@ -62,7 +62,7 @@
- add_page_startup_api_call notes_url
- else
- add_page_startup_api_call discussions_path(@merge_request)
- - add_page_startup_api_call widget_project_json_merge_request_path(@project, @merge_request, async_mergeability_check: true, format: :json)
+ - add_page_startup_api_call widget_project_json_merge_request_path(@project, @merge_request, format: :json)
- add_page_startup_api_call cached_widget_project_json_merge_request_path(@project, @merge_request, format: :json)
#js-vue-mr-discussions{ data: { notes_data: notes_data(@merge_request, Feature.enabled?(:paginated_notes, @project)).to_json,
endpoint_metadata: @endpoint_metadata_url,
diff --git a/app/workers/container_expiration_policy_worker.rb b/app/workers/container_expiration_policy_worker.rb
index b15d1bf90bd..8fc139ac87c 100644
--- a/app/workers/container_expiration_policy_worker.rb
+++ b/app/workers/container_expiration_policy_worker.rb
@@ -15,11 +15,19 @@ class ContainerExpirationPolicyWorker # rubocop:disable Scalability/IdempotentWo
def perform
process_stale_ongoing_cleanups
+ disable_policies_without_container_repositories
throttling_enabled? ? perform_throttled : perform_unthrottled
end
private
+ def disable_policies_without_container_repositories
+ ContainerExpirationPolicy.active.each_batch(of: BATCH_SIZE) do |policies|
+ policies.without_container_repositories
+ .update_all(enabled: false)
+ end
+ end
+
def process_stale_ongoing_cleanups
threshold = delete_tags_service_timeout.seconds + 30.minutes
ContainerRepository.with_stale_ongoing_cleanup(threshold.ago)
diff --git a/app/workers/web_hook_worker.rb b/app/workers/web_hook_worker.rb
index 3480f49d640..a2a53ca922a 100644
--- a/app/workers/web_hook_worker.rb
+++ b/app/workers/web_hook_worker.rb
@@ -8,7 +8,7 @@ class WebHookWorker
feature_category :integrations
worker_has_external_dependencies!
loggable_arguments 2
- data_consistency :delayed, feature_flag: :load_balancing_for_web_hook_worker
+ data_consistency :delayed
sidekiq_options retry: 4, dead: false