diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-06-17 10:07:47 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-06-17 10:07:47 +0000 |
commit | d670c3006e6e44901bce0d53cc4768d1d80ffa92 (patch) | |
tree | 8f65743c232e5b76850c4cc264ba15e1185815ff /app | |
parent | a5f4bba440d7f9ea47046a0a561d49adf0a1e6d4 (diff) | |
download | gitlab-ce-d670c3006e6e44901bce0d53cc4768d1d80ffa92.tar.gz |
Add latest changes from gitlab-org/gitlab@14-0-stable-ee
Diffstat (limited to 'app')
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 |