diff options
Diffstat (limited to 'app')
40 files changed, 298 insertions, 198 deletions
diff --git a/app/assets/javascripts/editor/schema/ci.json b/app/assets/javascripts/editor/schema/ci.json index cd99e6ef64f..e56932a9a31 100644 --- a/app/assets/javascripts/editor/schema/ci.json +++ b/app/assets/javascripts/editor/schema/ci.json @@ -103,6 +103,7 @@ "workflow": { "type": "object", "properties": { + "name": { "$ref": "#/definitions/workflowName" }, "rules": { "type": "array", "items": { @@ -714,6 +715,12 @@ ] } }, + "workflowName": { + "type": "string", + "markdownDescription": "Defines the pipeline name. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#workflowname).", + "minLength": 1, + "maxLength": 255 + }, "globalVariables": { "markdownDescription": "Defines default variables for all jobs. Job level property overrides global variables. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#variables).", "type": "object", diff --git a/app/assets/javascripts/header_search/components/app.vue b/app/assets/javascripts/header_search/components/app.vue index f4b939fb20f..8fc0ce48e61 100644 --- a/app/assets/javascripts/header_search/components/app.vue +++ b/app/assets/javascripts/header_search/components/app.vue @@ -14,6 +14,7 @@ import { visitUrl } from '~/lib/utils/url_utility'; import { truncate } from '~/lib/utils/text_utility'; import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; import { s__, sprintf } from '~/locale'; +import Tracking from '~/tracking'; import DropdownKeyboardNavigation from '~/vue_shared/components/dropdown_keyboard_navigation.vue'; import { FIRST_DROPDOWN_INDEX, @@ -163,8 +164,17 @@ export default { ...mapActions(['setSearch', 'fetchAutocompleteOptions', 'clearAutocomplete']), openDropdown() { this.showDropdown = true; - this.isFocused = true; - this.$emit('expandSearchBar', true); + + // check isFocused state to avoid firing duplicate events + if (!this.isFocused) { + this.isFocused = true; + this.$emit('expandSearchBar', true); + + Tracking.event(undefined, 'focus_input', { + label: 'global_search', + property: 'top_navigation', + }); + } }, closeDropdown() { this.showDropdown = false; @@ -178,6 +188,11 @@ export default { this.showDropdown = false; this.isFocused = false; this.$emit('collapseSearchBar'); + + Tracking.event(undefined, 'blur_input', { + label: 'global_search', + property: 'top_navigation', + }); }, 200); }, submitSearch() { diff --git a/app/assets/javascripts/listbox/index.js b/app/assets/javascripts/listbox/index.js index 2eeb0a77032..7eacbf7fcdd 100644 --- a/app/assets/javascripts/listbox/index.js +++ b/app/assets/javascripts/listbox/index.js @@ -1,4 +1,4 @@ -import { GlDropdown, GlDropdownItem, GlListbox } from '@gitlab/ui'; +import { GlListbox } from '@gitlab/ui'; import Vue from 'vue'; import { parseBoolean } from '~/lib/utils/common_utils'; @@ -31,59 +31,25 @@ export function initListbox(el, { onChange } = {}) { }, }, render(h) { - if (gon.features?.glListboxForSortDropdowns) { - return h(GlListbox, { - props: { - items, - right, - selected: this.selected, - toggleText: this.text, - }, - class: className, - on: { - select: (selectedValue) => { - this.selected = selectedValue; - const selectedItem = items.find(({ value }) => value === selectedValue); - - if (typeof onChange === 'function') { - onChange(selectedItem); - } - }, - }, - }); - } - - return h( - GlDropdown, - { - props: { - text: this.text, - right, + return h(GlListbox, { + props: { + items, + right, + selected: this.selected, + toggleText: this.text, + }, + class: className, + on: { + select: (selectedValue) => { + this.selected = selectedValue; + const selectedItem = items.find(({ value }) => value === selectedValue); + + if (typeof onChange === 'function') { + onChange(selectedItem); + } }, - class: className, }, - items.map((item) => - h( - GlDropdownItem, - { - props: { - isCheckItem: true, - isChecked: this.selected === item.value, - }, - on: { - click: () => { - this.selected = item.value; - - if (typeof onChange === 'function') { - onChange(item); - } - }, - }, - }, - item.text, - ), - ), - ); + }); }, }); } diff --git a/app/assets/javascripts/nav/components/top_nav_app.vue b/app/assets/javascripts/nav/components/top_nav_app.vue index ca6e6567f74..e55bf25a60c 100644 --- a/app/assets/javascripts/nav/components/top_nav_app.vue +++ b/app/assets/javascripts/nav/components/top_nav_app.vue @@ -1,5 +1,6 @@ <script> import { GlNav, GlIcon, GlNavItemDropdown, GlDropdownForm, GlTooltipDirective } from '@gitlab/ui'; +import Tracking from '~/tracking'; import TopNavDropdownMenu from './top_nav_dropdown_menu.vue'; export default { @@ -19,6 +20,14 @@ export default { required: true, }, }, + methods: { + trackToggleEvent() { + Tracking.event(undefined, 'click_nav', { + label: 'hamburger_menu', + property: 'top_navigation', + }); + }, + }, }; </script> @@ -32,6 +41,7 @@ export default { toggle-class="top-nav-toggle js-top-nav-dropdown-toggle gl-px-3!" no-flip no-caret + @toggle="trackToggleEvent" > <template #button-content> <gl-icon name="hamburger" /> diff --git a/app/assets/javascripts/pdf/index.vue b/app/assets/javascripts/pdf/index.vue index ddc880db227..f35f9341fa1 100644 --- a/app/assets/javascripts/pdf/index.vue +++ b/app/assets/javascripts/pdf/index.vue @@ -1,9 +1,10 @@ <script> -import pdfjsLib from 'pdfjs-dist/build/pdf'; -import workerSrc from 'pdfjs-dist/build/pdf.worker.min'; +import { getDocument, GlobalWorkerOptions } from 'pdfjs-dist/legacy/build/pdf'; import Page from './page/index.vue'; +GlobalWorkerOptions.workerSrc = '/assets/webpack/pdfjs/pdf.worker.min.js'; + export default { components: { Page }, props: { @@ -30,18 +31,16 @@ export default { }, watch: { pdf: 'load' }, mounted() { - pdfjsLib.GlobalWorkerOptions.workerSrc = workerSrc; if (this.hasPDF) this.load(); }, methods: { load() { this.pages = []; - return pdfjsLib - .getDocument({ - url: this.document, - cMapUrl: '/assets/webpack/cmaps/', - cMapPacked: true, - }) + return getDocument({ + url: this.document, + cMapUrl: '/assets/webpack/pdfjs/cmaps/', + cMapPacked: true, + }) .promise.then(this.renderPages) .then((pages) => { this.pages = pages; diff --git a/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue b/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue index 34c44321a9b..dbaabb35cde 100644 --- a/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue +++ b/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue @@ -17,8 +17,6 @@ import allRunnersCountQuery from 'ee_else_ce/runner/graphql/list/all_runners_cou import RegistrationDropdown from '../components/registration/registration_dropdown.vue'; import RunnerStackedLayoutBanner from '../components/runner_stacked_layout_banner.vue'; import RunnerFilteredSearchBar from '../components/runner_filtered_search_bar.vue'; -import RunnerBulkDelete from '../components/runner_bulk_delete.vue'; -import RunnerBulkDeleteCheckbox from '../components/runner_bulk_delete_checkbox.vue'; import RunnerList from '../components/runner_list.vue'; import RunnerListEmptyState from '../components/runner_list_empty_state.vue'; import RunnerName from '../components/runner_name.vue'; @@ -45,8 +43,6 @@ export default { RegistrationDropdown, RunnerStackedLayoutBanner, RunnerFilteredSearchBar, - RunnerBulkDelete, - RunnerBulkDeleteCheckbox, RunnerList, RunnerListEmptyState, RunnerName, @@ -56,7 +52,7 @@ export default { RunnerActionsCell, }, mixins: [glFeatureFlagMixin()], - inject: ['emptyStateSvgPath', 'emptyStateFilteredSvgPath', 'localMutations'], + inject: ['emptyStateSvgPath', 'emptyStateFilteredSvgPath'], props: { registrationToken: { type: String, @@ -155,12 +151,6 @@ export default { reportToSentry(error) { captureException({ error, component: this.$options.name }); }, - onChecked({ runner, isChecked }) { - this.localMutations.setRunnerChecked({ - runner, - isChecked, - }); - }, onPaginationInput(value) { this.search.pagination = value; }, @@ -211,16 +201,12 @@ export default { :filtered-svg-path="emptyStateFilteredSvgPath" /> <template v-else> - <runner-bulk-delete :runners="runners.items" @deleted="onDeleted" /> <runner-list :runners="runners.items" :loading="runnersLoading" :checkable="true" - @checked="onChecked" + @deleted="onDeleted" > - <template #head-checkbox> - <runner-bulk-delete-checkbox :runners="runners.items" /> - </template> <template #runner-name="{ runner }"> <gl-link :href="runner.adminUrl"> <runner-name :runner="runner" /> diff --git a/app/assets/javascripts/runner/components/runner_bulk_delete_checkbox.vue b/app/assets/javascripts/runner/components/runner_bulk_delete_checkbox.vue index c4e7cad9da9..75afb7a00bc 100644 --- a/app/assets/javascripts/runner/components/runner_bulk_delete_checkbox.vue +++ b/app/assets/javascripts/runner/components/runner_bulk_delete_checkbox.vue @@ -26,14 +26,17 @@ export default { }, }, computed: { + deletableRunners() { + return this.runners.filter((runner) => runner.userPermissions?.deleteRunner); + }, disabled() { - return !this.runners.length; + return !this.deletableRunners.length; }, checked() { - return Boolean(this.runners.length) && this.runners.every(this.isChecked); + return Boolean(this.deletableRunners.length) && this.deletableRunners.every(this.isChecked); }, indeterminate() { - return !this.checked && this.runners.some(this.isChecked); + return !this.checked && this.deletableRunners.some(this.isChecked); }, label() { return this.checked ? s__('Runners|Unselect all') : s__('Runners|Select all'); @@ -45,7 +48,7 @@ export default { }, onChange($event) { this.localMutations.setRunnersChecked({ - runners: this.runners, + runners: this.deletableRunners, isChecked: $event, }); }, diff --git a/app/assets/javascripts/runner/components/runner_details.vue b/app/assets/javascripts/runner/components/runner_details.vue index 79f934764c6..3d72abcd393 100644 --- a/app/assets/javascripts/runner/components/runner_details.vue +++ b/app/assets/javascripts/runner/components/runner_details.vue @@ -4,7 +4,6 @@ import { helpPagePath } from '~/helpers/help_page_helper'; import { s__ } from '~/locale'; import HelpPopover from '~/vue_shared/components/help_popover.vue'; import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue'; -import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { timeIntervalInWords } from '~/lib/utils/datetime_utility'; import { ACCESS_LEVEL_REF_PROTECTED, GROUP_TYPE, PROJECT_TYPE } from '../constants'; import RunnerDetail from './runner_detail.vue'; @@ -29,7 +28,6 @@ export default { RunnerTags, TimeAgo, }, - mixins: [glFeatureFlagMixin()], props: { runner: { type: Object, @@ -117,10 +115,7 @@ export default { </template> </runner-detail> <runner-detail :label="s__('Runners|Maximum job timeout')" :value="maximumTimeout" /> - <runner-detail - v-if="glFeatures.enforceRunnerTokenExpiresAt" - :empty-value="s__('Runners|Never expires')" - > + <runner-detail :empty-value="s__('Runners|Never expires')"> <template #label> {{ s__('Runners|Token expiry') }} <help-popover :options="tokenExpirationHelpPopoverOptions"> diff --git a/app/assets/javascripts/runner/components/runner_list.vue b/app/assets/javascripts/runner/components/runner_list.vue index d4a3311ff9f..e895537dcdc 100644 --- a/app/assets/javascripts/runner/components/runner_list.vue +++ b/app/assets/javascripts/runner/components/runner_list.vue @@ -5,6 +5,8 @@ import { s__ } from '~/locale'; import HelpPopover from '~/vue_shared/components/help_popover.vue'; import checkedRunnerIdsQuery from '../graphql/list/checked_runner_ids.query.graphql'; import { formatJobCount, tableField } from '../utils'; +import RunnerBulkDelete from './runner_bulk_delete.vue'; +import RunnerBulkDeleteCheckbox from './runner_bulk_delete_checkbox.vue'; import RunnerStackedSummaryCell from './cells/runner_stacked_summary_cell.vue'; import RunnerStatusPopover from './runner_status_popover.vue'; import RunnerStatusCell from './cells/runner_status_cell.vue'; @@ -23,6 +25,8 @@ export default { GlTableLite, GlSkeletonLoader, HelpPopover, + RunnerBulkDelete, + RunnerBulkDeleteCheckbox, RunnerStatusPopover, RunnerStackedSummaryCell, RunnerStatusCell, @@ -39,6 +43,7 @@ export default { }, }, }, + inject: ['localMutations'], props: { checkable: { type: Boolean, @@ -55,7 +60,7 @@ export default { required: true, }, }, - emits: ['checked'], + emits: ['deleted'], data() { return { checkedRunnerIds: [] }; }, @@ -84,6 +89,12 @@ export default { }, }, methods: { + canDelete(runner) { + return runner.userPermissions?.deleteRunner; + }, + onDeleted(event) { + this.$emit('deleted', event); + }, formatJobCount(jobCount) { return formatJobCount(jobCount); }, @@ -96,7 +107,7 @@ export default { return {}; }, onCheckboxChange(runner, isChecked) { - this.$emit('checked', { + this.localMutations.setRunnerChecked({ runner, isChecked, }); @@ -109,6 +120,7 @@ export default { </script> <template> <div> + <runner-bulk-delete v-if="checkable" :runners="runners" @deleted="onDeleted" /> <gl-table-lite :aria-busy="loading" :class="tableClass" @@ -121,11 +133,15 @@ export default { fixed > <template #head(checkbox)> - <slot name="head-checkbox"></slot> + <runner-bulk-delete-checkbox :runners="runners" /> </template> <template #cell(checkbox)="{ item }"> - <gl-form-checkbox :checked="isChecked(item)" @change="onCheckboxChange(item, $event)" /> + <gl-form-checkbox + v-if="canDelete(item)" + :checked="isChecked(item)" + @change="onCheckboxChange(item, $event)" + /> </template> <template #head(status)="{ label }"> diff --git a/app/assets/javascripts/runner/graphql/list/local_state.js b/app/assets/javascripts/runner/graphql/list/local_state.js index 4e1625cb1ac..e0477c660b4 100644 --- a/app/assets/javascripts/runner/graphql/list/local_state.js +++ b/app/assets/javascripts/runner/graphql/list/local_state.js @@ -48,16 +48,18 @@ export const createLocalState = () => { const localMutations = { setRunnerChecked({ runner, isChecked }) { - checkedRunnerIdsVar({ - ...checkedRunnerIdsVar(), - [runner.id]: isChecked, - }); + const { id, userPermissions } = runner; + if (userPermissions?.deleteRunner) { + checkedRunnerIdsVar({ + ...checkedRunnerIdsVar(), + [id]: isChecked, + }); + } }, setRunnersChecked({ runners, isChecked }) { - const newVal = runners.reduce( - (acc, { id }) => ({ ...acc, [id]: isChecked }), - checkedRunnerIdsVar(), - ); + const newVal = runners + .filter(({ userPermissions }) => userPermissions?.deleteRunner) + .reduce((acc, { id }) => ({ ...acc, [id]: isChecked }), checkedRunnerIdsVar()); checkedRunnerIdsVar(newVal); }, clearChecked() { diff --git a/app/assets/javascripts/runner/group_runners/group_runners_app.vue b/app/assets/javascripts/runner/group_runners/group_runners_app.vue index bc943c03a62..7f56d895682 100644 --- a/app/assets/javascripts/runner/group_runners/group_runners_app.vue +++ b/app/assets/javascripts/runner/group_runners/group_runners_app.vue @@ -12,6 +12,7 @@ import { } from 'ee_else_ce/runner/runner_search_utils'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import groupRunnersQuery from 'ee_else_ce/runner/graphql/list/group_runners.query.graphql'; +import groupRunnersCountQuery from 'ee_else_ce/runner/graphql/list/group_runners_count.query.graphql'; import RegistrationDropdown from '../components/registration/registration_dropdown.vue'; import RunnerStackedLayoutBanner from '../components/runner_stacked_layout_banner.vue'; @@ -173,13 +174,17 @@ export default { editUrl(runner) { return this.runners.urlsById[runner.id]?.edit; }, + refetchCounts() { + this.$apollo.getClient().refetchQueries({ include: [groupRunnersCountQuery] }); + }, onToggledPaused() { // When a runner becomes Paused, the tab count can // become stale, refetch outdated counts. - this.$refs['runner-type-tabs'].refetch(); + this.refetchCounts(); }, onDeleted({ message }) { this.$root.$toast?.show(message); + this.refetchCounts(); }, reportToSentry(error) { captureException({ error, component: this.$options.name }); @@ -245,7 +250,7 @@ export default { :filtered-svg-path="emptyStateFilteredSvgPath" /> <template v-else> - <runner-list :runners="runners.items" :loading="runnersLoading"> + <runner-list :runners="runners.items" :loading="runnersLoading" @deleted="onDeleted"> <template #runner-name="{ runner }"> <gl-link :href="webUrl(runner)"> <runner-name :runner="runner" /> diff --git a/app/assets/javascripts/runner/group_runners/index.js b/app/assets/javascripts/runner/group_runners/index.js index feed6b0ceb7..0e7efd2b8a1 100644 --- a/app/assets/javascripts/runner/group_runners/index.js +++ b/app/assets/javascripts/runner/group_runners/index.js @@ -2,6 +2,7 @@ import { GlToast } from '@gitlab/ui'; import Vue from 'vue'; import VueApollo from 'vue-apollo'; import createDefaultClient from '~/lib/graphql'; +import { createLocalState } from '../graphql/list/local_state'; import GroupRunnersApp from './group_runners_app.vue'; Vue.use(GlToast); @@ -26,8 +27,10 @@ export const initGroupRunners = (selector = '#js-group-runners') => { emptyStateFilteredSvgPath, } = el.dataset; + const { cacheConfig, typeDefs, localMutations } = createLocalState(); + const apolloProvider = new VueApollo({ - defaultClient: createDefaultClient(), + defaultClient: createDefaultClient({}, { cacheConfig, typeDefs }), }); return new Vue({ @@ -35,6 +38,7 @@ export const initGroupRunners = (selector = '#js-group-runners') => { apolloProvider, provide: { runnerInstallHelpPage, + localMutations, groupId, onlineContactTimeoutSecs: parseInt(onlineContactTimeoutSecs, 10), staleTimeoutSecs: parseInt(staleTimeoutSecs, 10), diff --git a/app/assets/javascripts/work_items/components/work_item_detail.vue b/app/assets/javascripts/work_items/components/work_item_detail.vue index 3255c7e5e4d..ccb58706fe0 100644 --- a/app/assets/javascripts/work_items/components/work_item_detail.vue +++ b/app/assets/javascripts/work_items/components/work_item_detail.vue @@ -23,6 +23,7 @@ import { WIDGET_TYPE_WEIGHT, WIDGET_TYPE_HIERARCHY, WORK_ITEM_VIEWED_STORAGE_KEY, + WIDGET_TYPE_ITERATION, } from '../constants'; import workItemQuery from '../graphql/work_item.query.graphql'; @@ -65,6 +66,7 @@ export default { WorkItemInformation, LocalStorageSync, WorkItemTypeIcon, + WorkItemIteration: () => import('ee_component/work_items/components/work_item_iteration.vue'), }, mixins: [glFeatureFlagMixin()], props: { @@ -134,7 +136,7 @@ export default { }; }, skip() { - return !this.workItemDueDate; + return !this.isWidgetPresent(WIDGET_TYPE_START_AND_DUE_DATE); }, }, { @@ -145,7 +147,7 @@ export default { }; }, skip() { - return !this.workItemAssignees; + return !this.isWidgetPresent(WIDGET_TYPE_ASSIGNEES); }, }, ], @@ -170,28 +172,8 @@ export default { workItemsMvc2Enabled() { return this.glFeatures.workItemsMvc2; }, - hasDescriptionWidget() { - return this.workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_DESCRIPTION); - }, - workItemAssignees() { - return this.workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_ASSIGNEES); - }, - workItemLabels() { - return this.workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_LABELS); - }, - workItemDueDate() { - return this.workItem?.widgets?.find( - (widget) => widget.type === WIDGET_TYPE_START_AND_DUE_DATE, - ); - }, - workItemWeight() { - return this.workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_WEIGHT); - }, - workItemHierarchy() { - return this.workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_HIERARCHY); - }, parentWorkItem() { - return this.workItemHierarchy?.parent; + return this.isWidgetPresent(WIDGET_TYPE_HIERARCHY)?.parent; }, parentWorkItemConfidentiality() { return this.parentWorkItem?.confidential; @@ -205,6 +187,27 @@ export default { noAccessSvgPath() { return `data:image/svg+xml;utf8,${encodeURIComponent(noAccessSvg)}`; }, + hasDescriptionWidget() { + return this.isWidgetPresent(WIDGET_TYPE_DESCRIPTION); + }, + workItemAssignees() { + return this.isWidgetPresent(WIDGET_TYPE_ASSIGNEES); + }, + workItemLabels() { + return this.isWidgetPresent(WIDGET_TYPE_LABELS); + }, + workItemDueDate() { + return this.isWidgetPresent(WIDGET_TYPE_START_AND_DUE_DATE); + }, + workItemWeight() { + return this.isWidgetPresent(WIDGET_TYPE_WEIGHT); + }, + workItemHierarchy() { + return this.isWidgetPresent(WIDGET_TYPE_HIERARCHY); + }, + workItemIteration() { + return this.isWidgetPresent(WIDGET_TYPE_ITERATION); + }, }, beforeDestroy() { /** make sure that if the user has not even dismissed the alert , @@ -212,6 +215,9 @@ export default { this.dismissBanner(); }, methods: { + isWidgetPresent(type) { + return this.workItem?.widgets?.find((widget) => widget.type === type); + }, dismissBanner() { this.showInfoBanner = false; }, @@ -416,6 +422,17 @@ export default { :work-item-type="workItemType" @error="updateError = $event" /> + <template v-if="workItemsMvc2Enabled"> + <work-item-iteration + v-if="workItemIteration" + class="gl-mb-5" + :iteration="workItemIteration.iteration" + :can-update="canUpdate" + :work-item-id="workItem.id" + :work-item-type="workItemType" + @error="updateError = $event" + /> + </template> <work-item-description v-if="hasDescriptionWidget" :work-item-id="workItem.id" diff --git a/app/assets/javascripts/work_items/components/work_item_links/index.js b/app/assets/javascripts/work_items/components/work_item_links/index.js index 8f31b07b6a3..37aa48be6e5 100644 --- a/app/assets/javascripts/work_items/components/work_item_links/index.js +++ b/app/assets/javascripts/work_items/components/work_item_links/index.js @@ -16,7 +16,13 @@ export default function initWorkItemLinks() { return; } - const { projectPath, wiHasIssueWeightsFeature, iid } = workItemLinksRoot.dataset; + const { + projectPath, + wiHasIssueWeightsFeature, + iid, + wiHasIterationsFeature, + projectNamespace, + } = workItemLinksRoot.dataset; // eslint-disable-next-line no-new new Vue({ @@ -31,6 +37,8 @@ export default function initWorkItemLinks() { iid, fullPath: projectPath, hasIssueWeightsFeature: wiHasIssueWeightsFeature, + hasIterationsFeature: wiHasIterationsFeature, + projectNamespace, }, render: (createElement) => createElement('work-item-links', { diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue index 827ec64f98a..0d3e951de7e 100644 --- a/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue +++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue @@ -5,7 +5,7 @@ import { s__ } from '~/locale'; import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils'; import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; import { TYPE_WORK_ITEM } from '~/graphql_shared/constants'; -import issueConfidentialQuery from '~/sidebar/queries/issue_confidential.query.graphql'; +import getIssueDetailsQuery from 'ee_else_ce/work_items/graphql/get_issue_details.query.graphql'; import { isMetaKey } from '~/lib/utils/common_utils'; import { setUrlParams, updateHistory } from '~/lib/utils/url_utility'; @@ -59,7 +59,7 @@ export default { }, }, parentIssue: { - query: issueConfidentialQuery, + query: getIssueDetailsQuery, variables() { return { fullPath: this.projectPath, @@ -86,6 +86,9 @@ export default { confidential() { return this.parentIssue?.confidential || this.workItem?.confidential || false; }, + issuableIteration() { + return this.parentIssue?.iteration; + }, children() { return ( this.workItem?.widgets.find((widget) => widget.type === WIDGET_TYPE_HIERARCHY)?.children @@ -305,6 +308,7 @@ export default { :issuable-gid="issuableGid" :children-ids="childrenIds" :parent-confidential="confidential" + :parent-iteration="issuableIteration" @cancel="hideAddForm" @addWorkItemChild="addChild" /> diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue index eafd9ee88dd..a01f4616cab 100644 --- a/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue +++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue @@ -16,7 +16,7 @@ export default { GlFormGroup, GlFormInput, }, - inject: ['projectPath'], + inject: ['projectPath', 'hasIterationsFeature'], props: { issuableGid: { type: String, @@ -33,6 +33,11 @@ export default { required: false, default: false, }, + parentIteration: { + type: Object, + required: false, + default: () => {}, + }, }, apollo: { workItemTypes: { @@ -77,6 +82,9 @@ export default { taskWorkItemType() { return this.workItemTypes.find((type) => type.name === TASK_TYPE_NAME)?.id; }, + parentIterationId() { + return this.parentIteration?.id; + }, }, methods: { getIdFromGraphQLId, @@ -133,6 +141,13 @@ export default { } else { this.unsetError(); this.$emit('addWorkItemChild', data.workItemCreate.workItem); + /** + * call update mutation only when there is an iteration associated with the issue + */ + // TODO: setting the iteration should be moved to the creation mutation once the backend is done + if (this.parentIterationId && this.hasIterationsFeature) { + this.addIterationToWorkItem(data.workItemCreate.workItem.id); + } } }) .catch(() => { @@ -143,6 +158,19 @@ export default { this.childToCreateTitle = null; }); }, + async addIterationToWorkItem(workItemId) { + await this.$apollo.mutate({ + mutation: updateWorkItemMutation, + variables: { + input: { + id: workItemId, + iterationWidget: { + iterationId: this.parentIterationId, + }, + }, + }, + }); + }, }, i18n: { inputLabel: __('Title'), diff --git a/app/assets/javascripts/work_items/constants.js b/app/assets/javascripts/work_items/constants.js index 97c445de711..0d426299408 100644 --- a/app/assets/javascripts/work_items/constants.js +++ b/app/assets/javascripts/work_items/constants.js @@ -17,6 +17,8 @@ export const WIDGET_TYPE_LABELS = 'LABELS'; export const WIDGET_TYPE_START_AND_DUE_DATE = 'START_AND_DUE_DATE'; export const WIDGET_TYPE_WEIGHT = 'WEIGHT'; export const WIDGET_TYPE_HIERARCHY = 'HIERARCHY'; +export const WIDGET_TYPE_ITERATION = 'ITERATION'; + export const WORK_ITEM_VIEWED_STORAGE_KEY = 'gl-show-work-item-banner'; export const WORK_ITEM_TYPE_ENUM_INCIDENT = 'INCIDENT'; @@ -54,6 +56,10 @@ export const I18N_WORK_ITEM_ARE_YOU_SURE_DELETE = s__( ); export const I18N_WORK_ITEM_DELETED = s__('WorkItem|%{workItemType} deleted'); +export const I18N_WORK_ITEM_FETCH_ITERATIONS_ERROR = s__( + 'WorkItem|Something went wrong when fetching iterations. Please try again.', +); + export const sprintfWorkItem = (msg, workItemTypeArg) => { const workItemType = workItemTypeArg || s__('WorkItem|Work item'); return capitalizeFirstCharacter( diff --git a/app/assets/javascripts/work_items/graphql/get_issue_details.query.graphql b/app/assets/javascripts/work_items/graphql/get_issue_details.query.graphql new file mode 100644 index 00000000000..6edb6c89f16 --- /dev/null +++ b/app/assets/javascripts/work_items/graphql/get_issue_details.query.graphql @@ -0,0 +1,9 @@ +query issuableDetails($fullPath: ID!, $iid: String) { + workspace: project(fullPath: $fullPath) { + id + issuable: issue(iid: $iid) { + id + confidential + } + } +} diff --git a/app/assets/javascripts/work_items/index.js b/app/assets/javascripts/work_items/index.js index bb4c7052238..f872d8c6b12 100644 --- a/app/assets/javascripts/work_items/index.js +++ b/app/assets/javascripts/work_items/index.js @@ -6,7 +6,13 @@ import { createRouter } from './router'; export const initWorkItemsRoot = () => { const el = document.querySelector('#js-work-items'); - const { fullPath, hasIssueWeightsFeature, issuesListPath } = el.dataset; + const { + fullPath, + hasIssueWeightsFeature, + issuesListPath, + projectNamespace, + hasIterationsFeature, + } = el.dataset; return new Vue({ el, @@ -17,6 +23,8 @@ export const initWorkItemsRoot = () => { fullPath, hasIssueWeightsFeature: parseBoolean(hasIssueWeightsFeature), issuesListPath, + projectNamespace, + hasIterationsFeature: parseBoolean(hasIterationsFeature), }, render(createElement) { return createElement(App); diff --git a/app/assets/stylesheets/page_bundles/work_items.scss b/app/assets/stylesheets/page_bundles/work_items.scss index d0fc011dde7..7a5cc72ceb8 100644 --- a/app/assets/stylesheets/page_bundles/work_items.scss +++ b/app/assets/stylesheets/page_bundles/work_items.scss @@ -63,3 +63,23 @@ display: none; } } + +.work-item-iteration { + .gl-dropdown-toggle { + background: none !important; + + &:hover, + &:focus { + box-shadow: inset 0 0 0 $gl-border-size-1 var(--gray-darkest, $gray-darkest) !important; + } + + &.is-not-focused:not(:hover, :focus) { + box-shadow: none; + + .gl-button-icon { + display: none; + } + } + } +} + diff --git a/app/controllers/admin/runners_controller.rb b/app/controllers/admin/runners_controller.rb index 2e0807cdab4..96fe0c9331d 100644 --- a/app/controllers/admin/runners_controller.rb +++ b/app/controllers/admin/runners_controller.rb @@ -5,10 +5,6 @@ class Admin::RunnersController < Admin::ApplicationController before_action :runner, except: [:index, :tag_list, :runner_setup_scripts] - before_action only: [:show] do - push_frontend_feature_flag(:enforce_runner_token_expires_at) - end - feature_category :runner urgency :low diff --git a/app/controllers/groups/runners_controller.rb b/app/controllers/groups/runners_controller.rb index 01c1529e831..18b055b3f05 100644 --- a/app/controllers/groups/runners_controller.rb +++ b/app/controllers/groups/runners_controller.rb @@ -5,10 +5,6 @@ class Groups::RunnersController < Groups::ApplicationController before_action :authorize_update_runner!, only: [:edit, :update, :destroy, :pause, :resume] before_action :runner, only: [:edit, :update, :destroy, :pause, :resume, :show] - before_action only: [:show] do - push_frontend_feature_flag(:enforce_runner_token_expires_at) - end - feature_category :runner urgency :low diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 853437b2d27..4287c0b7884 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -617,6 +617,15 @@ module Ci # auto_canceled_by_pipeline_id - store the pipeline_id of the pipeline that triggered cancellation # execute_async - if true cancel the children asyncronously def cancel_running(retries: 1, cascade_to_children: true, auto_canceled_by_pipeline_id: nil, execute_async: true) + Gitlab::AppJsonLogger.info( + event: 'pipeline_cancel_running', + pipeline_id: id, + auto_canceled_by_pipeline_id: auto_canceled_by_pipeline_id, + cascade_to_children: cascade_to_children, + execute_async: execute_async, + **Gitlab::ApplicationContext.current + ) + update(auto_canceled_by_id: auto_canceled_by_pipeline_id) if auto_canceled_by_pipeline_id cancel_jobs(cancelable_statuses, retries: retries, auto_canceled_by_pipeline_id: auto_canceled_by_pipeline_id) diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index 2bd26e15953..3be627989b1 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -14,7 +14,7 @@ module Ci include Presentable include EachBatch - add_authentication_token_field :token, encrypted: :optional, expires_at: :compute_token_expiration, expiration_enforced?: :token_expiration_enforced? + add_authentication_token_field :token, encrypted: :optional, expires_at: :compute_token_expiration enum access_level: { not_protected: 0, @@ -480,10 +480,6 @@ module Ci end end - def self.token_expiration_enforced? - Feature.enabled?(:enforce_runner_token_expires_at) - end - private scope :with_upgrade_status, ->(upgrade_status) do diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb index f7f29b3e82e..ed04b0c3d1f 100644 --- a/app/models/hooks/web_hook.rb +++ b/app/models/hooks/web_hook.rb @@ -7,7 +7,7 @@ class WebHook < ApplicationRecord MAX_FAILURES = 100 FAILURE_THRESHOLD = 3 # three strikes - INITIAL_BACKOFF = 10.minutes + INITIAL_BACKOFF = 1.minute MAX_BACKOFF = 1.day BACKOFF_GROWTH_FACTOR = 2.0 diff --git a/app/views/admin/application_settings/_account_and_limit.html.haml b/app/views/admin/application_settings/_account_and_limit.html.haml index e676dae37b3..b08a549148d 100644 --- a/app/views/admin/application_settings/_account_and_limit.html.haml +++ b/app/views/admin/application_settings/_account_and_limit.html.haml @@ -17,14 +17,14 @@ .form-group = f.label :receive_max_input_size, _('Maximum push size (MB)'), class: 'label-light' - = f.number_field :receive_max_input_size, class: 'form-control gl-form-input qa-receive-max-input-size-field', title: _('Maximum size limit for a single commit.'), data: { toggle: 'tooltip', container: 'body' } + = f.number_field :receive_max_input_size, class: 'form-control gl-form-input', title: _('Maximum size limit for a single commit.'), data: { toggle: 'tooltip', container: 'body', qa_selector: 'receive_max_input_size_field' } .form-group = f.label :max_export_size, _('Maximum export size (MB)'), class: 'label-light' = f.number_field :max_export_size, class: 'form-control gl-form-input', title: _('Maximum size of export files.'), data: { toggle: 'tooltip', container: 'body' } %span.form-text.text-muted= _('Set to 0 for no size limit.') .form-group = f.label :max_import_size, _('Maximum import size (MB)'), class: 'label-light' - = f.number_field :max_import_size, class: 'form-control gl-form-input qa-receive-max-import-size-field', title: _('Maximum size of import files.'), data: { toggle: 'tooltip', container: 'body' } + = f.number_field :max_import_size, class: 'form-control gl-form-input', title: _('Maximum size of import files.'), data: { toggle: 'tooltip', container: 'body' } %span.form-text.text-muted= _('Only effective when remote storage is enabled. Set to 0 for no size limit.') .form-group = f.label :session_expire_delay, _('Session duration (minutes)'), class: 'label-light' @@ -69,4 +69,4 @@ = render 'admin/application_settings/invitation_flow_enforcement', form: f = render 'admin/application_settings/user_restrictions', form: f = render_if_exists 'admin/application_settings/availability_on_namespace_setting', form: f - = f.submit _('Save changes'), class: 'qa-save-changes-button', pajamas_button: true + = f.submit _('Save changes'), pajamas_button: true, data: { qa_selector: 'save_changes_button' } diff --git a/app/views/admin/application_settings/_ci_cd.html.haml b/app/views/admin/application_settings/_ci_cd.html.haml index 05aea2b343d..f6635ad17ef 100644 --- a/app/views/admin/application_settings/_ci_cd.html.haml +++ b/app/views/admin/application_settings/_ci_cd.html.haml @@ -53,8 +53,7 @@ = link_to sprite_icon('question-o'), help_page_path('ci/pipelines/settings', anchor: 'specify-a-custom-cicd-configuration-file'), target: '_blank', rel: 'noopener noreferrer' .form-group = f.gitlab_ui_checkbox_component :suggest_pipeline_enabled, s_('AdminSettings|Enable pipeline suggestion banner'), help_text: s_('AdminSettings|Display a banner on merge requests in projects with no pipelines to initiate steps to add a .gitlab-ci.yml file.') - - if Feature.enabled?(:enforce_runner_token_expires_at) - #js-runner-token-expiration-intervals{ data: runner_token_expiration_interval_attributes } + #js-runner-token-expiration-intervals{ data: runner_token_expiration_interval_attributes } = f.submit _('Save changes'), pajamas_button: true diff --git a/app/views/admin/application_settings/_performance_bar.html.haml b/app/views/admin/application_settings/_performance_bar.html.haml index 823bc0380af..d4f6d84ea74 100644 --- a/app/views/admin/application_settings/_performance_bar.html.haml +++ b/app/views/admin/application_settings/_performance_bar.html.haml @@ -10,4 +10,4 @@ = f.label :performance_bar_allowed_group_path, _('Allow access to members of the following group'), class: 'label-bold' = f.text_field :performance_bar_allowed_group_path, class: 'form-control gl-form-input', placeholder: 'my-org/my-group', value: @application_setting.performance_bar_allowed_group&.full_path - = f.submit _('Save changes'), class: 'qa-save-changes-button', pajamas_button: true + = f.submit _('Save changes'), pajamas_button: true, data: { qa_selector: 'save_changes_button' } diff --git a/app/views/ci/variables/_url_query_variable_row.html.haml b/app/views/ci/variables/_url_query_variable_row.html.haml index 9c34daf88bd..77bcacdb94b 100644 --- a/app/views/ci/variables/_url_query_variable_row.html.haml +++ b/app/views/ci/variables/_url_query_variable_row.html.haml @@ -15,12 +15,12 @@ %input.js-ci-variable-input-destroy{ type: "hidden", name: destroy_input_name } %select.js-ci-variable-input-variable-type.ci-variable-body-item.form-control.select-control.custom-select.table-section.section-15{ name: variable_type_input_name } = options_for_select(ci_variable_type_options, variable_type) - %input.js-ci-variable-input-key.ci-variable-body-item.qa-ci-variable-input-key.form-control.table-section.section-15{ type: "text", + %input.js-ci-variable-input-key.ci-variable-body-item.form-control.table-section.section-15{ type: "text", name: key_input_name, value: key, placeholder: s_('CiVariables|Input variable key') } .ci-variable-body-item.gl-show-field-errors.table-section.section-15.border-top-0.p-0 - %textarea.js-ci-variable-input-value.js-secret-value.qa-ci-variable-input-value.form-control{ rows: 1, + %textarea.js-ci-variable-input-value.js-secret-value.form-control{ rows: 1, name: value_input_name, placeholder: s_('CiVariables|Input variable value') } = value diff --git a/app/views/ci/variables/_variable_row.html.haml b/app/views/ci/variables/_variable_row.html.haml index 483c767d029..c5e518d8526 100644 --- a/app/views/ci/variables/_variable_row.html.haml +++ b/app/views/ci/variables/_variable_row.html.haml @@ -18,14 +18,14 @@ %input.js-ci-variable-input-destroy{ type: "hidden", name: destroy_input_name } %select.js-ci-variable-input-variable-type.ci-variable-body-item.form-control.select-control.custom-select.table-section.section-15{ name: variable_type_input_name } = options_for_select(ci_variable_type_options, variable_type) - %input.js-ci-variable-input-key.ci-variable-body-item.qa-ci-variable-input-key.form-control.gl-form-input.table-section.section-15{ type: "text", + %input.js-ci-variable-input-key.ci-variable-body-item.form-control.gl-form-input.table-section.section-15{ type: "text", name: key_input_name, value: key, placeholder: s_('CiVariables|Input variable key') } .ci-variable-body-item.gl-show-field-errors.table-section.section-15.border-top-0.p-0 - .form-control.js-secret-value-placeholder.qa-ci-variable-input-value.overflow-hidden{ class: ('hide' unless id) } + .form-control.js-secret-value-placeholder.overflow-hidden{ class: ('hide' unless id) } = '*' * 17 - %textarea.js-ci-variable-input-value.js-secret-value.qa-ci-variable-input-value.form-control.gl-form-input{ class: ('hide' if id), + %textarea.js-ci-variable-input-value.js-secret-value.form-control.gl-form-input{ class: ('hide' if id), rows: 1, name: value_input_name, placeholder: s_('CiVariables|Input variable value') } diff --git a/app/views/clusters/clusters/_health.html.haml b/app/views/clusters/clusters/_health.html.haml index 50facdf91a2..9e7820d3136 100644 --- a/app/views/clusters/clusters/_health.html.haml +++ b/app/views/clusters/clusters/_health.html.haml @@ -1,6 +1,6 @@ - add_page_specific_style 'page_bundles/prometheus' -%section.settings.no-animate.expanded.cluster-health-graphs.qa-cluster-health-section#cluster-health +%section.settings.no-animate.expanded.cluster-health-graphs#cluster-health - if @cluster&.integration_prometheus_available? #prometheus-graphs{ data: @cluster.health_data(clusterable) } diff --git a/app/views/clusters/clusters/user/_form.html.haml b/app/views/clusters/clusters/user/_form.html.haml index bf7b24181c1..557c95f8478 100644 --- a/app/views/clusters/clusters/user/_form.html.haml +++ b/app/views/clusters/clusters/user/_form.html.haml @@ -36,7 +36,7 @@ = platform_kubernetes_field.form_group :authorization_type, { help: '%{help_text} %{help_link}'.html_safe % { help_text: rbac_help_text, help_link: rbac_help_link } } do = platform_kubernetes_field.check_box :authorization_type, - { class: 'qa-rbac-checkbox', label: s_('ClusterIntegration|RBAC-enabled cluster'), + { data: { qa_selector: 'rbac_checkbox'}, label: s_('ClusterIntegration|RBAC-enabled cluster'), label_class: 'label-bold', inline: true }, 'rbac', 'abac' .form-group diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index 800dcef5b72..b74dfd4d3a1 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -132,7 +132,7 @@ - if header_link?(:user_dropdown) %li.nav-item.header-user.js-nav-user-dropdown.dropdown{ data: { track_label: "profile_dropdown", track_action: "click_dropdown", track_value: "", qa_selector: 'user_menu', testid: 'user-menu' }, class: ('mr-0' if has_impersonation_link) } = link_to current_user, class: user_dropdown_class, data: { toggle: "dropdown" } do - = render Pajamas::AvatarComponent.new(current_user, size: 24, class: 'header-user-avatar qa-user-avatar') + = render Pajamas::AvatarComponent.new(current_user, size: 24, class: 'header-user-avatar', avatar_options: { data: { qa_selector: 'user_avatar_content' } }) = render_if_exists 'layouts/header/user_notification_dot', project: project, namespace: group = sprite_icon('chevron-down', css_class: 'caret-down') .dropdown-menu.dropdown-menu-right diff --git a/app/views/layouts/nav/sidebar/_admin.html.haml b/app/views/layouts/nav/sidebar/_admin.html.haml index 56f333664df..8815dec5a6b 100644 --- a/app/views/layouts/nav/sidebar/_admin.html.haml +++ b/app/views/layouts/nav/sidebar/_admin.html.haml @@ -1,4 +1,4 @@ -%aside.nav-sidebar.qa-admin-sidebar{ class: ("sidebar-collapsed-desktop" if collapsed_sidebar?), 'aria-label': _('Admin navigation') } +%aside.nav-sidebar{ class: ("sidebar-collapsed-desktop" if collapsed_sidebar?), 'aria-label': _('Admin navigation'), data: { qa_selector: 'admin_sidebar_content' } } .nav-sidebar-inner-scroll .context-header = link_to admin_root_path, title: _('Admin Overview'), class: 'has-tooltip', data: { container: 'body', placement: 'right' } do @@ -6,7 +6,7 @@ = sprite_icon('admin', size: 18) %span.sidebar-context-title = _('Admin Area') - %ul.sidebar-top-level-items{ data: { qa_selector: 'admin_sidebar_overview_submenu_content' } } + %ul.sidebar-top-level-items{ data: { qa_selector: 'admin_overview_submenu_content' } } = nav_link(controller: %w[dashboard admin admin/projects users groups admin/topics jobs runners gitaly_servers cohorts], html_options: {class: 'home'}) do = link_to admin_root_path, class: 'has-sub-items' do .nav-icon-container @@ -28,15 +28,15 @@ %span = _('Projects') = nav_link(controller: %w[users cohorts]) do - = link_to admin_users_path, title: _('Users'), data: { qa_selector: 'users_overview_link' } do + = link_to admin_users_path, title: _('Users'), data: { qa_selector: 'admin_overview_users_link' } do %span = _('Users') = nav_link(controller: :groups) do - = link_to admin_groups_path, title: _('Groups'), data: { qa_selector: 'groups_overview_link' } do + = link_to admin_groups_path, title: _('Groups'), data: { qa_selector: 'admin_overview_groups_link' } do %span = _('Groups') = nav_link(controller: [:admin, 'admin/topics']) do - = link_to admin_topics_path, title: _('Topics'), data: { qa_selector: 'topics_overview_link' } do + = link_to admin_topics_path, title: _('Topics') do %span = _('Topics') = nav_link path: 'jobs#index' do @@ -75,13 +75,13 @@ = _('Usage Trends') = nav_link(controller: admin_monitoring_nav_links) do - = link_to admin_system_info_path, data: { qa_selector: 'admin_monitoring_link' }, class: 'has-sub-items' do + = link_to admin_system_info_path, data: { qa_selector: 'admin_monitoring_menu_link' }, class: 'has-sub-items' do .nav-icon-container = sprite_icon('monitor') %span.nav-item-name = _('Monitoring') - %ul.sidebar-sub-level-items{ data: { qa_selector: 'admin_sidebar_monitoring_submenu_content' } } + %ul.sidebar-sub-level-items{ data: { qa_selector: 'admin_monitoring_submenu_content' } } = nav_link(controller: admin_monitoring_nav_links, html_options: { class: "fly-out-top-item" } ) do = link_to admin_system_info_path do %strong.fly-out-top-item-name @@ -222,10 +222,10 @@ = link_to general_admin_application_settings_path, class: 'has-sub-items' do .nav-icon-container = sprite_icon('settings') - %span.nav-item-name.qa-admin-settings-item + %span.nav-item-name{ data: { qa_selector: 'admin_settings_menu_link' } } = _('Settings') - %ul.sidebar-sub-level-items{ data: { qa_selector: 'admin_sidebar_settings_submenu_content' } } + %ul.sidebar-sub-level-items{ data: { qa_selector: 'admin_settings_submenu_content' } } -# This active_nav_link check is also used in `app/views/layouts/admin.html.haml` = nav_link(controller: [:application_settings, :integrations, :appearances], html_options: { class: "fly-out-top-item" } ) do = link_to general_admin_application_settings_path do @@ -233,24 +233,24 @@ = _('Settings') %li.divider.fly-out-top-item = nav_link(path: 'application_settings#general') do - = link_to general_admin_application_settings_path, title: _('General'), class: 'qa-admin-settings-general-item' do + = link_to general_admin_application_settings_path, title: _('General'), data: { qa_selector: 'admin_settings_general_link' } do %span = _('General') - = render_if_exists 'layouts/nav/sidebar/advanced_search', class: 'qa-admin-settings-advanced-search' + = render_if_exists 'layouts/nav/sidebar/advanced_search', data: { qa_selector: 'admin_settings_advanced_search_link' } - if instance_level_integrations? = nav_link(path: ['application_settings#integrations', 'integrations#edit']) do - = link_to integrations_admin_application_settings_path, title: _('Integrations'), data: { qa_selector: 'integration_settings_link' } do + = link_to integrations_admin_application_settings_path, title: _('Integrations'), data: { qa_selector: 'admin_settings_integrations_link' } do %span = _('Integrations') = nav_link(path: 'application_settings#repository') do - = link_to repository_admin_application_settings_path, title: _('Repository'), class: 'qa-admin-settings-repository-item' do + = link_to repository_admin_application_settings_path, title: _('Repository'), data: { qa_selector: 'admin_settings_repository_link' } do %span = _('Repository') - if Gitlab.ee? && License.feature_available?(:custom_file_templates) = nav_link(path: 'application_settings#templates') do - = link_to templates_admin_application_settings_path, title: _('Templates'), class: 'qa-admin-settings-template-item' do + = link_to templates_admin_application_settings_path, title: _('Templates'), data: { qa_selector: 'admin_settings_templates_link' } do %span = _('Templates') = nav_link(path: 'application_settings#ci_cd') do @@ -262,7 +262,7 @@ %span = _('Reporting') = nav_link(path: 'application_settings#metrics_and_profiling') do - = link_to metrics_and_profiling_admin_application_settings_path, title: _('Metrics and profiling'), class: 'qa-admin-settings-metrics-and-profiling-item' do + = link_to metrics_and_profiling_admin_application_settings_path, title: _('Metrics and profiling'), data: { qa_selector: 'admin_settings_metrics_and_profiling_link' } do %span = _('Metrics and profiling') = nav_link(path: ['application_settings#service_usage_data']) do @@ -270,7 +270,7 @@ %span = _('Service usage data') = nav_link(path: 'application_settings#network') do - = link_to network_admin_application_settings_path, title: _('Network'), data: { qa_selector: 'admin_settings_network_item' } do + = link_to network_admin_application_settings_path, title: _('Network'), data: { qa_selector: 'admin_settings_network_link' } do %span = _('Network') = nav_link(controller: :appearances ) do diff --git a/app/views/layouts/terms.html.haml b/app/views/layouts/terms.html.haml index 91848b4f54b..032be73f70c 100644 --- a/app/views/layouts/terms.html.haml +++ b/app/views/layouts/terms.html.haml @@ -25,7 +25,7 @@ %ul.nav.navbar-nav %li.header-user.dropdown = link_to current_user, class: user_dropdown_class, data: { toggle: "dropdown" } do - = render Pajamas::AvatarComponent.new(current_user, size: 24, class: 'gl-mr-3', avatar_options: { data: { qa_selector: 'user_avatar' } }) + = render Pajamas::AvatarComponent.new(current_user, size: 24, class: 'gl-mr-3', avatar_options: { data: { qa_selector: 'user_avatar_content' } }) = sprite_icon('chevron-down') .dropdown-menu.dropdown-menu-right = render 'layouts/header/current_user_dropdown' diff --git a/app/views/projects/blob/_template_selectors.html.haml b/app/views/projects/blob/_template_selectors.html.haml index a76e61bc3dd..249c474587c 100644 --- a/app/views/projects/blob/_template_selectors.html.haml +++ b/app/views/projects/blob/_template_selectors.html.haml @@ -2,14 +2,14 @@ .template-selector-dropdowns-wrap .template-type-selector.js-template-type-selector-wrap.hidden - toggle_text = should_suggest_gitlab_ci_yml? ? '.gitlab-ci.yml' : 'Select a template type' - = dropdown_tag(_(toggle_text), options: { toggle_class: 'js-template-type-selector qa-template-type-dropdown', dropdown_class: 'dropdown-menu-selectable' }) + = dropdown_tag(_(toggle_text), options: { toggle_class: 'js-template-type-selector', dropdown_class: 'dropdown-menu-selectable', data: { qa_selector: 'template_type_dropdown' } }) .license-selector.js-license-selector-wrap.js-template-selector-wrap.hidden - = dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-license-selector qa-license-dropdown', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: licenses_for_select(@project), project: @project.name, fullname: @project.namespace.human_name } } ) + = dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-license-selector', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: licenses_for_select(@project), project: @project.name, fullname: @project.namespace.human_name, qa_selector: 'license_dropdown' } } ) .gitignore-selector.js-gitignore-selector-wrap.js-template-selector-wrap.hidden - = dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-gitignore-selector qa-gitignore-dropdown', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: gitignore_names(@project) } } ) + = dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-gitignore-selector', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: gitignore_names(@project), qa_selector: 'gitignore_dropdown' } } ) .metrics-dashboard-selector.js-metrics-dashboard-selector-wrap.js-template-selector-wrap.hidden - = dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-metrics-dashboard-selector qa-metrics-dashboard-dropdown', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: metrics_dashboard_ymls(@project) } } ) + = dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-metrics-dashboard-selector', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: metrics_dashboard_ymls(@project), qa_selector: 'metrics_dashboard_dropdown' } } ) #gitlab-ci-yml-selector.gitlab-ci-yml-selector.js-gitlab-ci-yml-selector-wrap.js-template-selector-wrap.hidden - = dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-gitlab-ci-yml-selector qa-gitlab-ci-yml-dropdown', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: gitlab_ci_ymls(@project), selected: params[:template] } } ) + = dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-gitlab-ci-yml-selector', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: gitlab_ci_ymls(@project), selected: params[:template], qa_selector: 'gitlab_ci_yml_dropdown' } } ) .dockerfile-selector.js-dockerfile-selector-wrap.js-template-selector-wrap.hidden - = dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-dockerfile-selector qa-dockerfile-dropdown', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: dockerfile_names(@project) } } ) + = dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-dockerfile-selector', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: dockerfile_names(@project), qa_selector: 'dockerfile_dropdown' } } ) diff --git a/app/views/projects/buttons/_clone.html.haml b/app/views/projects/buttons/_clone.html.haml index 10a6bc6b524..34aecd31c57 100644 --- a/app/views/projects/buttons/_clone.html.haml +++ b/app/views/projects/buttons/_clone.html.haml @@ -2,17 +2,17 @@ - dropdown_class = local_assigns.fetch(:dropdown_class, '') .git-clone-holder.js-git-clone-holder - %a#clone-dropdown.gl-button.btn.btn-confirm.clone-dropdown-btn.qa-clone-dropdown{ href: '#', data: { toggle: 'dropdown' } } + %a#clone-dropdown.gl-button.btn.btn-confirm.clone-dropdown-btn{ href: '#', data: { toggle: 'dropdown', qa_selector: 'clone_dropdown' } } %span.gl-mr-2.js-clone-dropdown-label = _('Clone') = sprite_icon("chevron-down", css_class: "icon") - %ul.dropdown-menu.dropdown-menu-large.dropdown-menu-selectable.clone-options-dropdown.qa-clone-options{ class: dropdown_class } + %ul.dropdown-menu.dropdown-menu-large.dropdown-menu-selectable.clone-options-dropdown{ class: dropdown_class, data: { qa_selector: 'clone_dropdown_content' } } - if ssh_enabled? %li{ class: 'gl-px-4!' } %label.label-bold = _('Clone with SSH') .input-group.btn-group - = text_field_tag :ssh_project_clone, project.ssh_url_to_repo, class: "js-select-on-focus form-control qa-ssh-clone-url", readonly: true, aria: { label: _('Repository clone URL') } + = text_field_tag :ssh_project_clone, project.ssh_url_to_repo, class: "js-select-on-focus form-control", readonly: true, aria: { label: _('Repository clone URL') }, data: { qa_selector: 'ssh_clone_url_content' } .input-group-append = clipboard_button(target: '#ssh_project_clone', title: _("Copy URL"), class: "input-group-text gl-button btn btn-icon btn-default") = render_if_exists 'projects/buttons/geo' @@ -21,7 +21,7 @@ %label.label-bold = _('Clone with %{http_label}') % { http_label: gitlab_config.protocol.upcase } .input-group.btn-group - = text_field_tag :http_project_clone, project.http_url_to_repo, class: "js-select-on-focus form-control qa-http-clone-url", readonly: true, aria: { label: _('Repository clone URL') } + = text_field_tag :http_project_clone, project.http_url_to_repo, class: "js-select-on-focus form-control", readonly: true, aria: { label: _('Repository clone URL') }, data: { qa_selector: 'http_clone_url_content' } .input-group-append = clipboard_button(target: '#http_project_clone', title: _("Copy URL"), class: "input-group-text gl-button btn btn-icon btn-default") = render_if_exists 'projects/buttons/geo' diff --git a/app/views/projects/issues/_work_item_links.html.haml b/app/views/projects/issues/_work_item_links.html.haml index bc2136b89fb..c0de711136a 100644 --- a/app/views/projects/issues/_work_item_links.html.haml +++ b/app/views/projects/issues/_work_item_links.html.haml @@ -1,2 +1,2 @@ - if Feature.enabled?(:work_items_hierarchy, @project) - .js-work-item-links-root{ data: { issuable_id: @issue.id, iid: @issue.iid, project_path: @project.full_path, wi: work_items_index_data(@project) } } + .js-work-item-links-root{ data: { issuable_id: @issue.id, iid: @issue.iid, project_namespace: @project.namespace.path, project_path: @project.full_path, wi: work_items_index_data(@project) } } diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml index 6b502ee928e..abaf250fa69 100644 --- a/app/views/shared/_clone_panel.html.haml +++ b/app/views/shared/_clone_panel.html.haml @@ -9,14 +9,14 @@ %span.js-clone-dropdown-label = default_clone_protocol.upcase = sprite_icon('chevron-down', css_class: 'gl-icon') - %ul.dropdown-menu.dropdown-menu-selectable.clone-options-dropdown + %ul.dropdown-menu.dropdown-menu-selectable{ data: { qa_selector: 'clone_dropdown_content' } } %li = ssh_clone_button(container) %li = http_clone_button(container) = render_if_exists 'shared/kerberos_clone_button', container: container - = text_field_tag :clone_url, default_url_to_repo(container), class: "js-select-on-focus btn gl-button", readonly: true, aria: { label: _('Repository clone URL') } + = text_field_tag :clone_url, default_url_to_repo(container), class: "js-select-on-focus btn gl-button", readonly: true, aria: { label: _('Repository clone URL') }, data: { qa_selector: 'clone_url_content' } .input-group-append = clipboard_button(target: '#clone_url', title: _("Copy URL"), class: "input-group-text gl-button btn-default btn-clipboard") diff --git a/app/views/shared/web_hooks/_hook_errors.html.haml b/app/views/shared/web_hooks/_hook_errors.html.haml index d95efe83e15..098cc19c435 100644 --- a/app/views/shared/web_hooks/_hook_errors.html.haml +++ b/app/views/shared/web_hooks/_hook_errors.html.haml @@ -4,16 +4,12 @@ - link_end = '</a>'.html_safe - if hook.rate_limited? - - support_path = 'https://support.gitlab.com/hc/en-us/requests/new' - - placeholders = { strong_start: strong_start, - strong_end: strong_end, - limit: hook.rate_limit, - support_link_start: link_start % { url: support_path }, - support_link_end: link_end } - = render Pajamas::AlertComponent.new(title: s_('Webhooks|Webhook was automatically disabled'), + - placeholders = { limit: number_with_delimiter(hook.rate_limit), + root_namespace: hook.parent.root_namespace.path } + = render Pajamas::AlertComponent.new(title: s_('Webhooks|Webhook rate limit has been reached'), variant: :danger) do |c| = c.body do - = s_('Webhooks|The webhook was triggered more than %{limit} times per minute and is now disabled. To re-enable this webhook, fix the problems shown in %{strong_start}Recent events%{strong_end}, then re-test your settings. %{support_link_start}Contact Support%{support_link_end} if you need help re-enabling your webhook.').html_safe % placeholders + = s_("Webhooks|Webhooks for %{root_namespace} are now disabled because they've been triggered more than %{limit} times per minute. They'll be automatically re-enabled in the next minute.").html_safe % placeholders - elsif hook.permanently_disabled? = render Pajamas::AlertComponent.new(title: s_('Webhooks|Webhook failed to connect'), variant: :danger) do |c| |