Commit dd85d9a5 authored by Kushal Pandya's avatar Kushal Pandya

Merge branch 'ph/28750/removeResolveCommentButton' into 'master'

Remove resolve comment functionality

See merge request gitlab-org/gitlab!45549
parents 6907d7a7 8acc6a6b

Too many changes to show.

To preserve performance only 1000 of 1000+ files are displayed.

......@@ -28,6 +28,7 @@ Personas are described at https://about.gitlab.com/handbook/marketing/product-ma
* [Allison (Application Ops)](https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas/#allison-application-ops)
* [Priyanka (Platform Engineer)](https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas/#priyanka-platform-engineer)
* [Dana (Data Analyst)](https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas/#dana-data-analyst)
* [Eddie (Content Editor)](https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas/#eddie-content-editor)
-->
### User experience goal
......
......@@ -329,7 +329,6 @@ linters:
- 'ee/app/views/errors/kerberos_denied.html.haml'
- 'ee/app/views/groups/ee/_settings_nav.html.haml'
- 'ee/app/views/groups/group_members/_ldap_sync.html.haml'
- 'ee/app/views/groups/group_members/_sync_button.html.haml'
- 'ee/app/views/groups/hooks/edit.html.haml'
- 'ee/app/views/groups/ldap_group_links/index.html.haml'
- 'ee/app/views/layouts/nav/ee/admin/_new_monitoring_sidebar.html.haml'
......@@ -362,7 +361,6 @@ linters:
- 'ee/app/views/projects/services/gitlab_slack_application/_help.html.haml'
- 'ee/app/views/projects/services/gitlab_slack_application/_slack_integration_form.html.haml'
- 'ee/app/views/projects/settings/slacks/edit.html.haml'
- 'ee/app/views/shared/_mirror_update_button.html.haml'
- 'ee/app/views/shared/epic/_search_bar.html.haml'
- 'ee/app/views/shared/issuable/_approvals.html.haml'
- 'ee/app/views/shared/issuable/_board_create_list_dropdown.html.haml'
......
......@@ -186,31 +186,21 @@ RSpec/ExpectChange:
# Offense count: 47
RSpec/ExpectGitlabTracking:
Exclude:
- 'ee/spec/controllers/groups/analytics/coverage_reports_controller_spec.rb'
- 'ee/spec/controllers/projects/settings/operations_controller_spec.rb'
- 'ee/spec/controllers/registrations_controller_spec.rb'
- 'ee/spec/requests/api/visual_review_discussions_spec.rb'
- 'ee/spec/services/epics/issue_promote_service_spec.rb'
- 'spec/controllers/groups/registry/repositories_controller_spec.rb'
- 'spec/controllers/groups_controller_spec.rb'
- 'spec/controllers/projects/registry/repositories_controller_spec.rb'
- 'spec/controllers/projects/registry/tags_controller_spec.rb'
- 'spec/controllers/projects/settings/operations_controller_spec.rb'
- 'spec/controllers/registrations_controller_spec.rb'
- 'spec/lib/api/helpers_spec.rb'
- 'spec/lib/gitlab/experimentation_spec.rb'
- 'spec/mailers/notify_spec.rb'
- 'spec/models/project_services/prometheus_service_spec.rb'
- 'spec/requests/api/project_container_repositories_spec.rb'
- 'spec/services/clusters/applications/check_installation_progress_service_spec.rb'
- 'spec/services/issues/zoom_link_service_spec.rb'
- 'spec/support/helpers/snowplow_helpers.rb'
- 'spec/support/shared_examples/controllers/trackable_shared_examples.rb'
- 'spec/support/shared_examples/requests/api/container_repositories_shared_examples.rb'
- 'spec/support/shared_examples/requests/api/discussions_shared_examples.rb'
- 'spec/support/shared_examples/requests/api/packages_shared_examples.rb'
- 'spec/support/shared_examples/requests/api/tracking_shared_examples.rb'
- 'spec/support/snowplow.rb'
# Offense count: 751
RSpec/ExpectInHook:
......@@ -1125,33 +1115,6 @@ Rails/SaveBang:
- 'spec/requests/api/labels_spec.rb'
- 'spec/requests/api/project_import_spec.rb'
- 'spec/requests/projects/cycle_analytics_events_spec.rb'
- 'spec/services/auth/container_registry_authentication_service_spec.rb'
- 'spec/services/auto_merge/base_service_spec.rb'
- 'spec/services/auto_merge_service_spec.rb'
- 'spec/services/clusters/update_service_spec.rb'
- 'spec/services/deployments/after_create_service_spec.rb'
- 'spec/services/design_management/generate_image_versions_service_spec.rb'
- 'spec/services/discussions/resolve_service_spec.rb'
- 'spec/services/draft_notes/destroy_service_spec.rb'
- 'spec/services/emails/confirm_service_spec.rb'
- 'spec/services/groups/destroy_service_spec.rb'
- 'spec/services/groups/import_export/import_service_spec.rb'
- 'spec/services/labels/promote_service_spec.rb'
- 'spec/services/notes/create_service_spec.rb'
- 'spec/services/notification_recipients/build_service_spec.rb'
- 'spec/services/notification_service_spec.rb'
- 'spec/services/packages/conan/create_package_file_service_spec.rb'
- 'spec/services/reset_project_cache_service_spec.rb'
- 'spec/services/resource_events/change_milestone_service_spec.rb'
- 'spec/services/system_hooks_service_spec.rb'
- 'spec/services/system_note_service_spec.rb'
- 'spec/services/system_notes/issuables_service_spec.rb'
- 'spec/services/todo_service_spec.rb'
- 'spec/services/todos/destroy/confidential_issue_service_spec.rb'
- 'spec/services/users/destroy_service_spec.rb'
- 'spec/services/users/repair_ldap_blocked_service_spec.rb'
- 'spec/services/verify_pages_domain_service_spec.rb'
- 'spec/sidekiq/cron/job_gem_dependency_spec.rb'
# Offense count: 187
# Cop supports --auto-correct.
......@@ -1269,11 +1232,10 @@ RSpec/TimecopTravel:
- 'spec/workers/concerns/reenqueuer_spec.rb'
- 'spec/lib/gitlab/analytics/cycle_analytics/median_spec.rb'
# Offense count: 43
# Offense count: 21
Graphql/IDType:
Exclude:
- 'ee/app/graphql/ee/mutations/issues/update.rb'
- 'ee/app/graphql/ee/types/boards/board_issue_input_base_type.rb'
- 'ee/app/graphql/mutations/iterations/update.rb'
- 'ee/app/graphql/resolvers/iterations_resolver.rb'
- 'app/graphql/mutations/boards/issues/issue_move_list.rb'
......
This diff is collapsed.
This diff is collapsed.
dac25a9c19af0a168a7927784295dd12fb5c8075
0ebfb705b79a8baecc1db46f31761f83f4e471f9
13.5.0-pre
13.6.0-pre
\ No newline at end of file
......@@ -2,6 +2,7 @@ import $ from 'jquery';
import '../commons/bootstrap';
import { isInIssuePage } from '../lib/utils/common_utils';
import { __ } from '~/locale';
import { add, show, hide } from '~/tooltips';
// Quick Submit behavior
//
......@@ -65,18 +66,17 @@ $(document).on(
return;
}
const $this = $(this);
const $el = $(this);
const title = isMac()
? __('You can also press ⌘-Enter')
? __('You can also press \u{2318}-Enter')
: __('You can also press Ctrl-Enter');
$this.tooltip({
container: 'body',
html: true,
placement: 'top',
add($el, {
triggers: 'manual',
show: true,
title,
trigger: 'manual',
});
$this.tooltip('show').one('blur click', () => $this.tooltip('hide'));
$el.one('blur click', () => hide($el));
show($el);
},
);
/* global DocumentTouch */
import $ from 'jquery';
import sortableConfig from 'ee_else_ce/sortable/sortable_config';
export function sortableStart() {
$('.has-tooltip')
.tooltip('hide')
.tooltip('disable');
document.body.classList.add('is-dragging');
}
export function sortableEnd() {
$('.has-tooltip').tooltip('enable');
document.body.classList.remove('is-dragging');
}
......
<script>
import { mapState, mapGetters, mapActions } from 'vuex';
import { GlSprintf, GlLink } from '@gitlab/ui';
import { GlSprintf, GlLink, GlIcon } from '@gitlab/ui';
import { s__ } from '~/locale';
import gkeDropdownMixin from './gke_dropdown_mixin';
......@@ -10,6 +10,7 @@ export default {
components: {
GlSprintf,
GlLink,
GlIcon,
},
mixins: [gkeDropdownMixin],
props: {
......@@ -178,14 +179,14 @@ export default {
'https://console.cloud.google.com/freetrial?utm_campaign=2018_cpanel&utm_source=gitlab&utm_medium=referral'
"
target="_blank"
>{{ content }} <i class="fa fa-external-link" aria-hidden="true"></i
></gl-link>
>{{ content }} <gl-icon name="external-link" aria-hidden="true"
/></gl-link>
</template>
<template #docsLink="{ content }">
<gl-link :href="docsUrl" target="_blank"
>{{ content }} <i class="fa fa-external-link" aria-hidden="true"></i
></gl-link>
>{{ content }} <gl-icon name="external-link" aria-hidden="true"
/></gl-link>
</template>
<template #error>
......
......@@ -21,6 +21,7 @@ import {
updateImageDiffNoteOptimisticResponse,
toDiffNoteGid,
extractDesignNoteId,
getPageLayoutElement,
} from '../../utils/design_management_utils';
import {
updateStoreAfterAddImageDiffNote,
......@@ -38,7 +39,7 @@ import {
} from '../../utils/error_messages';
import { trackDesignDetailView } from '../../utils/tracking';
import { DESIGNS_ROUTE_NAME } from '../../router/constants';
import { ACTIVE_DISCUSSION_SOURCE_TYPES } from '../../constants';
import { ACTIVE_DISCUSSION_SOURCE_TYPES, DESIGN_DETAIL_LAYOUT_CLASSLIST } from '../../constants';
const DEFAULT_SCALE = 1;
......@@ -300,6 +301,22 @@ export default {
this.resolvedDiscussionsExpanded = !this.resolvedDiscussionsExpanded;
},
},
beforeRouteEnter(to, from, next) {
const pageEl = getPageLayoutElement();
if (pageEl) {
pageEl.classList.add(...DESIGN_DETAIL_LAYOUT_CLASSLIST);
}
next();
},
beforeRouteLeave(to, from, next) {
const pageEl = getPageLayoutElement();
if (pageEl) {
pageEl.classList.remove(...DESIGN_DETAIL_LAYOUT_CLASSLIST);
}
next();
},
createImageDiffNoteMutation,
DESIGNS_ROUTE_NAME,
};
......
import Vue from 'vue';
import VueRouter from 'vue-router';
import routes from './routes';
import { DESIGN_ROUTE_NAME } from './constants';
import { getPageLayoutElement } from '~/design_management/utils/design_management_utils';
import { DESIGN_DETAIL_LAYOUT_CLASSLIST } from '../constants';
Vue.use(VueRouter);
......@@ -13,20 +10,6 @@ export default function createRouter(base) {
mode: 'history',
routes,
});
const pageEl = getPageLayoutElement();
router.beforeEach(({ name }, _, next) => {
// apply a fullscreen layout style in Design View (a.k.a design detail)
if (pageEl) {
if (name === DESIGN_ROUTE_NAME) {
pageEl.classList.add(...DESIGN_DETAIL_LAYOUT_CLASSLIST);
} else {
pageEl.classList.remove(...DESIGN_DETAIL_LAYOUT_CLASSLIST);
}
}
next();
});
return router;
}
......@@ -12,6 +12,7 @@ export default (IssuableTokenKeys, disableTargetBranchFilter = false) => {
tag: __('Yes or No'),
lowercaseValueOnSubmit: true,
capitalizeTokenValue: true,
hideNotEqual: true,
},
conditions: [
{
......@@ -30,20 +31,6 @@ export default (IssuableTokenKeys, disableTargetBranchFilter = false) => {
value: __('No'),
operator: '=',
},
{
url: 'not[wip]=yes',
replacementUrl: 'not[draft]=yes',
tokenKey: 'draft',
value: __('Yes'),
operator: '!=',
},
{
url: 'not[wip]=no',
replacementUrl: 'not[draft]=no',
tokenKey: 'draft',
value: __('No'),
operator: '!=',
},
],
};
......
......@@ -39,7 +39,7 @@ export default class DropdownOperator extends FilteredSearchDropdown {
this.dispatchInputEvent();
}
renderContent(forceShowList = false) {
renderContent(forceShowList = false, dropdownName = '') {
const dropdownData = [
{
tag: 'equal',
......@@ -48,8 +48,9 @@ export default class DropdownOperator extends FilteredSearchDropdown {
help: __('is'),
},
];
const dropdownToken = this.tokenKeys.searchByKey(dropdownName.toLowerCase());
if (gon.features?.notIssuableQueries) {
if (gon.features?.notIssuableQueries && !dropdownToken?.hideNotEqual) {
dropdownData.push({
tag: 'not-equal',
type: 'string',
......
......@@ -83,16 +83,16 @@ export default class FilteredSearchDropdown {
}
}
render(forceRenderContent = false, forceShowList = false) {
render(forceRenderContent = false, forceShowList = false, hideNotEqual = false) {
this.setAsDropdown();
const currentHook = this.getCurrentHook();
const firstTimeInitialized = currentHook === null;
if (firstTimeInitialized || forceRenderContent) {
this.renderContent(forceShowList);
this.renderContent(forceShowList, hideNotEqual);
} else if (currentHook.list.list.id !== this.dropdown.id) {
this.renderContent(forceShowList);
this.renderContent(forceShowList, hideNotEqual);
}
}
......
......@@ -107,7 +107,7 @@ export default class FilteredSearchDropdownManager {
this.mapping[key].reference.setOffset(offset);
}
load(key, firstLoad = false) {
load(key, firstLoad = false, dropdownKey = '') {
const mappingKey = this.mapping[key];
const glClass = mappingKey.gl;
const { element } = mappingKey;
......@@ -141,12 +141,12 @@ export default class FilteredSearchDropdownManager {
}
this.updateDropdownOffset(key);
mappingKey.reference.render(firstLoad, forceShowList);
mappingKey.reference.render(firstLoad, forceShowList, dropdownKey);
this.currentDropdown = key;
}
loadDropdown(dropdownName = '') {
loadDropdown(dropdownName = '', dropdownKey = '') {
let firstLoad = false;
if (!this.droplab) {
......@@ -155,7 +155,7 @@ export default class FilteredSearchDropdownManager {
}
if (dropdownName === DROPDOWN_TYPE.operator) {
this.load(dropdownName, firstLoad);
this.load(dropdownName, firstLoad, dropdownKey);
return;
}
......@@ -167,7 +167,7 @@ export default class FilteredSearchDropdownManager {
if (shouldOpenFilterDropdown || shouldOpenHintDropdown) {
const key = match && match.key ? match.key : DROPDOWN_TYPE.hint;
this.load(key, firstLoad);
this.load(key, firstLoad, dropdownKey);
}
}
......@@ -200,11 +200,11 @@ export default class FilteredSearchDropdownManager {
dropdownToOpen = hasOperator && lastOperatorToken ? dropdownName : DROPDOWN_TYPE.operator;
}
this.loadDropdown(dropdownToOpen);
this.loadDropdown(dropdownToOpen, dropdownName);
} else if (lastToken) {
const lastOperator = FilteredSearchVisualTokens.getLastTokenOperator();
// Token has been initialized into an object because it has a value
this.loadDropdown(lastOperator ? lastToken.key : DROPDOWN_TYPE.operator);
this.loadDropdown(lastOperator ? lastToken.key : DROPDOWN_TYPE.operator, lastToken.key);
} else {
this.loadDropdown(DROPDOWN_TYPE.hint);
}
......
<script>
import { GlIcon } from '@gitlab/ui';
import tooltip from '~/vue_shared/directives/tooltip';
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
import eventHub from '../event_hub';
import { COMMON_STR } from '../constants';
......@@ -9,7 +8,7 @@ export default {
GlIcon,
},
directives: {
tooltip,
GlTooltip: GlTooltipDirective,
},
props: {
parentGroup: {
......@@ -47,12 +46,10 @@ export default {
<div class="controls d-flex justify-content-end">
<a
v-if="group.canLeave"
v-tooltip
v-gl-tooltip.top
:href="group.leavePath"
:title="leaveBtnTitle"
:aria-label="leaveBtnTitle"
data-container="body"
data-placement="bottom"
data-testid="leave-group-btn"
class="leave-group btn btn-xs no-expand gl-text-gray-500 gl-ml-5"
@click.prevent="onLeaveGroup"
......@@ -61,12 +58,10 @@ export default {
</a>
<a
v-if="group.canEdit"
v-tooltip
v-gl-tooltip.top
:href="group.editPath"
:title="editBtnTitle"
:aria-label="editBtnTitle"
data-container="body"
data-placement="bottom"
data-testid="edit-group-btn"
class="edit-group btn btn-xs no-expand gl-text-gray-500 gl-ml-5"
>
......
<script>
import { mapState, mapActions, mapGetters } from 'vuex';
import tooltip from '~/vue_shared/directives/tooltip';
import CommitFilesList from './commit_sidebar/list.vue';
import EmptyState from './commit_sidebar/empty_state.vue';
import { stageKeys } from '../constants';
......@@ -10,9 +9,6 @@ export default {
CommitFilesList,
EmptyState,
},
directives: {
tooltip,
},
computed: {
...mapState(['changedFiles', 'stagedFiles', 'lastCommitMsg']),
...mapState('commit', ['commitMessage', 'submitCommitLoading']),
......
<script>
import { uniqBy } from 'lodash';
import { GlIcon } from '@gitlab/ui';
import { GlButton, GlIcon } from '@gitlab/ui';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
export default {
components: {
GlButton,
GlIcon,
UserAvatarLink,
TimeAgoTooltip,
......@@ -57,14 +58,15 @@ export default {
tooltip-placement="bottom"
/>
</div>
<button
class="btn btn-link js-replies-text"
<gl-button
class="js-replies-text"
category="tertiary"
variant="link"
data-qa-selector="expand_replies_button"
type="button"
@click="toggle"
>
{{ replies.length }} {{ n__('reply', 'replies', replies.length) }}
</button>
</gl-button>
{{ __('Last reply by') }}
<a :href="lastReply.author.path" class="btn btn-link author-link">
{{ lastReply.author.name }}
......
import AlertDetails from '~/alert_management/details';
document.addEventListener('DOMContentLoaded', () => {
AlertDetails('#js-alert_details');
});
AlertDetails('#js-alert_details');
import AlertManagementList from '~/alert_management/list';
document.addEventListener('DOMContentLoaded', () => {
AlertManagementList();
});
AlertManagementList();
import ErrorTrackingDetails from '~/error_tracking/details';
document.addEventListener('DOMContentLoaded', () => {
ErrorTrackingDetails();
});
ErrorTrackingDetails();
import ErrorTrackingList from '~/error_tracking/list';
document.addEventListener('DOMContentLoaded', () => {
ErrorTrackingList();
});
ErrorTrackingList();
import IncidentsList from '~/incidents/list';
document.addEventListener('DOMContentLoaded', () => {
IncidentsList();
});
IncidentsList();
......@@ -2,10 +2,8 @@ import initSidebarBundle from '~/sidebar/sidebar_bundle';
import initRelatedIssues from '~/related_issues';
import initShow from '../../issues/show';
document.addEventListener('DOMContentLoaded', () => {
initShow();
if (!gon.features?.vueIssuableSidebar) {
initSidebarBundle();
}
initRelatedIssues();
});
initShow();
if (!gon.features?.vueIssuableSidebar) {
initSidebarBundle();
}
initRelatedIssues();
import monitoringApp from '~/monitoring/monitoring_app';
document.addEventListener('DOMContentLoaded', monitoringApp);
monitoringApp();
import PersistentUserCallout from '~/persistent_user_callout';
document.addEventListener('DOMContentLoaded', () => {
const callout = document.querySelector('.js-webhooks-moved-alert');
PersistentUserCallout.factory(callout);
});
const callout = document.querySelector('.js-webhooks-moved-alert');
PersistentUserCallout.factory(callout);
......@@ -5,13 +5,11 @@ import mountGrafanaIntegration from '~/grafana_integration';
import initSettingsPanels from '~/settings_panels';
import initIncidentsSettings from '~/incidents_settings';
document.addEventListener('DOMContentLoaded', () => {
initIncidentsSettings();
mountErrorTrackingForm();
mountOperationSettings();
mountGrafanaIntegration();
if (!IS_EE) {
initSettingsPanels();
}
mountAlertsSettings(document.querySelector('.js-alerts-settings'));
});
initIncidentsSettings();
mountErrorTrackingForm();
mountOperationSettings();
mountGrafanaIntegration();
if (!IS_EE) {
initSettingsPanels();
}
mountAlertsSettings(document.querySelector('.js-alerts-settings'));
......@@ -13,7 +13,6 @@ import CIPaginationMixin from '~/vue_shared/mixins/ci_pagination_api_mixin';
import PipelinesFilteredSearch from './pipelines_filtered_search.vue';
import { validateParams } from '../../utils';
import { ANY_TRIGGER_AUTHOR, RAW_TEXT_WARNING, FILTER_TAG_IDENTIFIER } from '../../constants';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
components: {
......@@ -23,7 +22,7 @@ export default {
PipelinesFilteredSearch,
GlIcon,
},
mixins: [pipelinesMixin, CIPaginationMixin, glFeatureFlagsMixin()],
mixins: [pipelinesMixin, CIPaginationMixin],
props: {
store: {
type: Object,
......@@ -209,9 +208,6 @@ export default {
},
];
},
canFilterPipelines() {
return this.glFeatures.filterPipelinesSearch;
},
validatedParams() {
return validateParams(this.params);
},
......@@ -306,7 +302,6 @@ export default {
</div>
<pipelines-filtered-search
v-if="canFilterPipelines"
:project-id="projectId"
:params="validatedParams"
@filterPipelines="filterPipelines"
......
<script>
import { GlModal } from '@gitlab/ui';
import { __ } from '~/locale';
import CodeBlock from '~/vue_shared/components/code_block.vue';
export default {
name: 'TestCaseDetails',
components: {
CodeBlock,
GlModal,
},
props: {
modalId: {
type: String,
required: true,
},
testCase: {
type: Object,
required: true,
validator: ({ classname, formattedTime, name }) =>
Boolean(classname) && Boolean(formattedTime) && Boolean(name),
},
},
text: {
name: __('Name'),
duration: __('Execution time'),
trace: __('System output'),
},
modalCloseButton: {
text: __('Close'),
attributes: [{ variant: 'info' }],
},
};
</script>
<template>
<gl-modal
:modal-id="modalId"
:title="testCase.classname"
:action-primary="$options.modalCloseButton"
>
<div class="gl-display-flex gl-flex-wrap gl-mx-n4 gl-my-3">
<strong class="gl-text-right col-sm-3">{{ $options.text.name }}</strong>
<div class="col-sm-9" data-testid="test-case-name">
{{ testCase.name }}
</div>
</div>
<div class="gl-display-flex gl-flex-wrap gl-mx-n4 gl-my-3">
<strong class="gl-text-right col-sm-3">{{ $options.text.duration }}</strong>
<div class="col-sm-9" data-testid="test-case-duration">
{{ testCase.formattedTime }}
</div>
</div>
<div
v-if="testCase.system_output"
class="gl-display-flex gl-flex-wrap gl-mx-n4 gl-my-3"
data-testid="test-case-trace"
>
<strong class="gl-text-right col-sm-3">{{ $options.text.trace }}</strong>
<div class="col-sm-9">
<code-block :code="testCase.system_output" />
</div>
</div>
</gl-modal>
</template>
......@@ -61,7 +61,7 @@ export default {
<div
v-else-if="!isLoading && showTests"
ref="container"
class="tests-detail position-relative"
class="position-relative"
data-testid="tests-detail"
>
<transition
......@@ -69,13 +69,13 @@ export default {
@before-enter="beforeEnterTransition"
@after-leave="afterLeaveTransition"
>
<div v-if="showSuite" key="detail" class="w-100 position-absolute slide-enter-to-element">
<div v-if="showSuite" key="detail" class="w-100 slide-enter-to-element">
<test-summary :report="getSelectedSuite" show-back @on-back-click="summaryBackClick" />
<test-suite-table />
</div>
<div v-else key="summary" class="w-100 position-absolute slide-enter-from-element">
<div v-else key="summary" class="w-100 slide-enter-from-element">
<test-summary :report="testReports" />
<test-summary-table @row-click="summaryTableRowClick" />
......
<script>
import { mapGetters } from 'vuex';
import { GlTooltipDirective, GlFriendlyWrap, GlIcon, GlButton } from '@gitlab/ui';
import { GlModalDirective, GlTooltipDirective, GlFriendlyWrap, GlIcon, GlButton } from '@gitlab/ui';
import { __ } from '~/locale';
import TestCaseDetails from './test_case_details.vue';
export default {
name: 'TestsSuiteTable',
......@@ -9,9 +10,11 @@ export default {
GlIcon,
GlFriendlyWrap,
GlButton,
TestCaseDetails,
},
directives: {
GlTooltip: GlTooltipDirective,
GlModalDirective,
},
props: {
heading: {
......@@ -43,7 +46,7 @@ export default {
<div role="rowheader" class="table-section section-20">
{{ __('Suite') }}
</div>
<div role="rowheader" class="table-section section-20">
<div role="rowheader" class="table-section section-40">
{{ __('Name') }}
</div>
<div role="rowheader" class="table-section section-10">
......@@ -52,12 +55,12 @@ export default {
<div role="rowheader" class="table-section section-10 text-center">
{{ __('Status') }}
</div>
<div role="rowheader" class="table-section flex-grow-1">
{{ __('Trace'), }}
</div>
<div role="rowheader" class="table-section section-10 text-right">
<div role="rowheader" class="table-section section-10">
{{ __('Duration') }}
</div>
<div role="rowheader" class="table-section section-10">
{{ __('Details'), }}
</div>
</div>
<div
......@@ -72,7 +75,7 @@ export default {
</div>
</div>
<div class="table-section section-20 section-wrap">
<div class="table-section section-40 section-wrap">
<div role="rowheader" class="table-mobile-header">{{ __('Name') }}</div>
<div class="table-mobile-content gl-md-pr-2 gl-overflow-wrap-break">
<gl-friendly-wrap :symbols="$options.wrapSymbols" :text="testCase.name" />
......@@ -107,24 +110,24 @@ export default {
</div>
</div>
<div class="table-section flex-grow-1">
<div role="rowheader" class="table-mobile-header">{{ __('Trace'), }}</div>
<div class="table-mobile-content">
<pre
v-if="testCase.system_output"
class="build-trace build-trace-rounded text-left"
><code class="bash p-0">{{testCase.system_output}}</code></pre>
</div>
</div>
<div class="table-section section-10 section-wrap">
<div role="rowheader" class="table-mobile-header">
{{ __('Duration') }}
</div>
<div class="table-mobile-content text-right pr-sm-1">
<div class="table-mobile-content pr-sm-1">
{{ testCase.formattedTime }}
</div>
</div>
<div class="table-section section-10 section-wrap">
<div role="rowheader" class="table-mobile-header">{{ __('Details'), }}</div>
<div class="table-mobile-content">
<gl-button v-gl-modal-directive="`test-case-details-${index}`">{{
__('View details')
}}</gl-button>
<test-case-details :modal-id="`test-case-details-${index}`" :test-case="testCase" />
</div>
</div>
</div>
</div>
......
<script>
/* eslint-disable vue/no-v-html */
import { escape } from 'lodash';
import { GlButton } from '@gitlab/ui';
import { GlSafeHtmlDirective as SafeHtml, GlButton, GlModal, GlModalDirective } from '@gitlab/ui';
import axios from '~/lib/utils/axios_utils';
import DeprecatedModal2 from '~/vue_shared/components/deprecated_modal_2.vue';
import { s__, sprintf } from '~/locale';
import { deprecatedCreateFlash as Flash } from '~/flash';
export default {
components: {
GlModal: DeprecatedModal2,
GlModal,
GlButton,
},
directives: {
GlModalDirective,
SafeHtml,
},
props: {
actionUrl: {
type: String,
......@@ -54,6 +56,21 @@ Please update your Git repository remotes as soon as possible.`),
false,
);
},
primaryProps() {
return {
text: s__('Update username'),
attributes: [
{ variant: 'warning' },
{ category: 'primary' },
{ disabled: this.isRequestPending },
],
};
},
cancelProps() {
return {
text: s__('Cancel'),
};
},
},
methods: {
onConfirm() {
......@@ -103,22 +120,21 @@ Please update your Git repository remotes as soon as possible.`),
<p class="form-text text-muted">{{ path }}</p>
</div>
<gl-button
:data-target="`#${$options.modalId}`"
v-gl-modal-directive="$options.modalId"
:disabled="isRequestPending || newUsername === username"
category="primary"
variant="warning"
data-toggle="modal"
data-testid="username-change-confirmation-modal"
>{{ $options.buttonText }}</gl-button
>
{{ $options.buttonText }}
</gl-button>
<gl-modal
:id="$options.modalId"
:header-title-text="s__('Profiles|Change username') + '?'"
:footer-primary-button-text="$options.buttonText"
footer-primary-button-variant="warning"
@submit="onConfirm"
:modal-id="$options.modalId"
:title="s__('Profiles|Change username') + '?'"
:action-primary="primaryProps"
:action-cancel="cancelProps"
@primary="onConfirm"
>
<span v-html="modalText"></span>
<span v-safe-html="modalText"></span>
</gl-modal>
</div>
</template>
import { LOADING, ERROR, SUCCESS } from '../../constants';
import { sprintf, __, s__, n__ } from '~/locale';
import { spriteIcon } from '~/lib/utils/common_utils';
export const hasCodequalityIssues = state =>
Boolean(state.newIssues?.length || state.resolvedIssues?.length);
......@@ -48,7 +49,7 @@ export const codequalityPopover = state => {
s__('ciReport|%{linkStartTag}Learn more about codequality reports %{linkEndTag}'),
{
linkStartTag: `<a href="${state.helpPath}" target="_blank" rel="noopener noreferrer">`,
linkEndTag: '<i class="fa fa-external-link" aria-hidden="true"></i></a>',
linkEndTag: `${spriteIcon('external-link', 's16')}</a>`,
},
false,
),
......
......@@ -43,7 +43,15 @@ export default {
return this.filterData.filters.ANY.value;
},
set(filter) {
visitUrl(setUrlParams({ [this.filterData.filterParam]: filter }));
// we need to remove the pagination cursor to ensure the
// relevancy of the new results
visitUrl(
setUrlParams({
page: null,
[this.filterData.filterParam]: filter,
}),
);
},
},
selectedFilterText() {
......
......@@ -16,10 +16,6 @@ export default {
type: Object,
required: true,
},
rootPath: {
type: String,
required: true,
},
tooltipPlacement: {
type: String,
default: 'bottom',
......@@ -76,7 +72,7 @@ export default {
<!-- use d-flex so that slot can be appropriately styled -->
<span class="d-flex">
<assignee-avatar :user="user" :img-size="32" :issuable-type="issuableType" />
<slot :user="user"></slot>
<slot></slot>
</span>
</gl-link>
</template>
......@@ -13,10 +13,6 @@ export default {
type: Array,
required: true,
},
rootPath: {
type: String,
required: true,
},
issuableType: {
type: String,
required: false,
......@@ -66,22 +62,20 @@ export default {
<template>
<assignee-avatar-link
v-if="hasOneUser"
#default="{ user }"
tooltip-placement="left"
:tooltip-has-name="false"
:user="firstUser"
:root-path="rootPath"
:issuable-type="issuableType"
>
<div class="ml-2 gl-line-height-normal">
<div>{{ user.name }}</div>
<div>{{ firstUser.name }}</div>
<div>{{ username }}</div>
</div>
</assignee-avatar-link>
<div v-else>
<div class="user-list">
<div v-for="user in uncollapsedUsers" :key="user.id" class="user-item">
<assignee-avatar-link :user="user" :root-path="rootPath" :issuable-type="issuableType" />
<assignee-avatar-link :user="user" :issuable-type="issuableType" />
</div>
</div>
<div v-if="renderShowMoreSection" class="user-list-more">
......
<script>
import { GlForm, GlFormGroup, GlFormInput, GlFormTextarea } from '@gitlab/ui';
import AccessorUtilities from '~/lib/utils/accessor';
export default {
components: {
......@@ -19,55 +18,25 @@ export default {
required: true,
},
},
data() {
return {
editable: {
title: this.title,
description: this.description,
},
};
},
computed: {
editableStorageKey() {
return this.getId('local-storage', 'editable');
},
hasLocalStorage() {
return AccessorUtilities.isLocalStorageAccessSafe();
},
},
mounted() {
this.initCachedEditable();
this.preSelect();
},
methods: {
getId(type, key) {
return `sse-merge-request-meta-${type}-${key}`;
},
initCachedEditable() {
if (this.hasLocalStorage) {
const cachedEditable = JSON.parse(localStorage.getItem(this.editableStorageKey));
if (cachedEditable) {
this.editable = cachedEditable;
}
}
},
preSelect() {
this.$nextTick(() => {
this.$refs.title.$el.select();
});
},
resetCachedEditable() {
if (this.hasLocalStorage) {
window.localStorage.removeItem(this.editableStorageKey);
}
},
onUpdate() {
const payload = { ...this.editable };
onUpdate(field, value) {
const payload = {
title: this.title,
description: this.description,
[field]: value,
};
this.$emit('updateSettings', payload);
if (this.hasLocalStorage) {
window.localStorage.setItem(this.editableStorageKey, JSON.stringify(payload));
}
},
},
};
......@@ -83,9 +52,9 @@ export default {
<gl-form-input
:id="getId('control', 'title')"
ref="title"
v-model.lazy="editable.title"
:value="title"
type="text"
@input="onUpdate"
@input="onUpdate('title', $event)"
/>
</gl-form-group>
......@@ -96,8 +65,8 @@ export default {
>
<gl-form-textarea
:id="getId('control', 'description')"
v-model.lazy="editable.description"
@input="onUpdate"
:value="description"
@input="onUpdate('description', $event)"
/>
</gl-form-group>
</gl-form>
......
<script>
import { GlModal } from '@gitlab/ui';
import { __, s__, sprintf } from '~/locale';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import EditMetaControls from './edit_meta_controls.vue';
import { MR_META_LOCAL_STORAGE_KEY } from '../constants';
export default {
components: {
GlModal,
EditMetaControls,
LocalStorageSync,
},
props: {
sourcePath: {
......@@ -17,6 +21,7 @@ export default {
},
data() {
return {
clearStorage: false,
mergeRequestMeta: {
title: sprintf(s__(`StaticSiteEditor|Update %{sourcePath} file`), {
sourcePath: this.sourcePath,
......@@ -51,7 +56,7 @@ export default {
},
onPrimary() {
this.$emit('primary', this.mergeRequestMeta);
this.$refs.editMetaControls.resetCachedEditable();
this.clearStorage = true;
},
onSecondary() {
this.hide();
......@@ -60,6 +65,7 @@ export default {
this.mergeRequestMeta = { ...mergeRequestMeta };
},
},
storageKey: MR_META_LOCAL_STORAGE_KEY,
};
</script>
......@@ -75,6 +81,12 @@ export default {
@secondary="onSecondary"
@hide="() => $emit('hide')"
>
<local-storage-sync
v-model="mergeRequestMeta"
:storage-key="$options.storageKey"
:clear="clearStorage"
as-json
/>
<edit-meta-controls
ref="editMetaControls"
:title="mergeRequestMeta.title"
......
......@@ -21,3 +21,5 @@ export const TRACKING_ACTION_CREATE_MERGE_REQUEST = 'create_merge_request';
export const TRACKING_ACTION_INITIALIZE_EDITOR = 'initialize_editor';
export const DEFAULT_IMAGE_UPLOAD_PATH = 'source/images/uploads/';
export const MR_META_LOCAL_STORAGE_KEY = 'sse-merge-request-meta-storage-key';
......@@ -108,6 +108,7 @@ export default {
:container="tooltip.container"
:boundary="tooltip.boundary"
:disabled="tooltip.disabled"
:show="tooltip.show"
>
<span v-if="tooltip.html" v-safe-html="tooltip.title"></span>
<span v-else>{{ tooltip.title }}</span>
......
......@@ -96,6 +96,12 @@ export const initTooltips = (config = {}) => {
return invokeBootstrapApi(document.body, config);
};
export const add = (elements, config = {}) => {
if (isGlTooltipsEnabled()) {
return addTooltips(elements, config);
}
return invokeBootstrapApi(elements, config);
};
export const dispose = tooltipApiInvoker({
glHandler: element => tooltipsApp().dispose(element),
bsHandler: elements => invokeBootstrapApi(elements, 'dispose'),
......
<script>
import { isEmpty } from 'lodash';
import { GlIcon, GlButton, GlSprintf, GlLink } from '@gitlab/ui';
import {
GlIcon,
GlButton,
GlButtonGroup,
GlDropdown,
GlDropdownItem,
GlSprintf,
GlLink,
GlTooltipDirective,
} from '@gitlab/ui';
import readyToMergeMixin from 'ee_else_ce/vue_merge_request_widget/mixins/ready_to_merge';
import simplePoll from '~/lib/utils/simple_poll';
import { __ } from '~/locale';
......@@ -36,6 +45,9 @@ export default {
GlSprintf,
GlLink,
GlButton,
GlButtonGroup,
GlDropdown,
GlDropdownItem,
MergeTrainHelperText: () =>
import('ee_component/vue_merge_request_widget/components/merge_train_helper_text.vue'),
MergeImmediatelyConfirmationDialog: () =>
......@@ -43,6 +55,9 @@ export default {
'ee_component/vue_merge_request_widget/components/merge_immediately_confirmation_dialog.vue'
),
},
directives: {
GlTooltip: GlTooltipDirective,
},
mixins: [readyToMergeMixin],
props: {
mr: { type: Object, required: true },
......@@ -283,7 +298,7 @@ export default {
<status-icon :status="iconClass" />
<div class="media-body">
<div class="mr-widget-body-controls media space-children">
<span class="btn-group">
<gl-button-group>
<gl-button
size="medium"
category="primary"
......@@ -294,54 +309,33 @@ export default {
@click="handleMergeButtonClick(isAutoMergeAvailable)"
>{{ mergeButtonText }}</gl-button
>
<button
<gl-dropdown
v-if="shouldShowMergeImmediatelyDropdown"
v-gl-tooltip.hover.focus="__('Select merge moment')"
:disabled="isMergeButtonDisabled"
type="button"
class="btn btn-sm btn-info dropdown-toggle js-merge-moment"
data-toggle="dropdown"
variant="info"
data-qa-selector="merge_moment_dropdown"
:aria-label="__('Select merge moment')"
>
<i class="fa fa-chevron-down" aria-hidden="true"></i>
</button>
<ul
v-if="shouldShowMergeImmediatelyDropdown"
class="dropdown-menu dropdown-menu-right"
role="menu"
toggle-class="btn-icon js-merge-moment"
>
<li>
<a
class="auto_merge_enabled qa-merge-when-pipeline-succeeds-option"
href="#"
@click.prevent="handleMergeButtonClick(true)"
>
<span class="media">
<gl-icon name="status_success" class="merge-opt-icon" aria-hidden="true" />
<span class="media-body merge-opt-title">{{ autoMergeText }}</span>
</span>
</a>
</li>
<li>
<merge-immediately-confirmation-dialog
ref="confirmationDialog"
:docs-url="mr.mergeImmediatelyDocsPath"
@mergeImmediately="onMergeImmediatelyConfirmation"
/>
<a
class="accept-merge-request js-merge-immediately-button"
data-qa-selector="merge_immediately_option"
href="#"
@click.prevent="handleMergeImmediatelyButtonClick"
>
<span class="media">
<gl-icon name="status_warning" class="merge-opt-icon" aria-hidden="true" />
<span class="media-body merge-opt-title">{{ __('Merge immediately') }}</span>
</span>
</a>
</li>
</ul>
</span>
<template #button-content>
<gl-icon name="chevron-down" class="mr-0" />
<span class="sr-only">{{ __('Select merge moment') }}</span>
</template>
<gl-dropdown-item
icon-name="warning"
button-class="accept-merge-request js-merge-immediately-button"
data-qa-selector="merge_immediately_option"
@click="handleMergeImmediatelyButtonClick"
>
{{ __('Merge immediately') }}
</gl-dropdown-item>
<merge-immediately-confirmation-dialog
ref="confirmationDialog"
:docs-url="mr.mergeImmediatelyDocsPath"
@mergeImmediately="onMergeImmediatelyConfirmation"
/>
</gl-dropdown>
</gl-button-group>
<div class="media-body-wrap space-children">
<template v-if="shouldShowMergeControls">
<label v-if="mr.canRemoveSourceBranch">
......
......@@ -22,11 +22,21 @@ export default {
required: false,
default: true,
},
clear: {
type: Boolean,
required: false,
default: false,
},
},
watch: {
value(newVal) {
this.saveValue(this.serialize(newVal));
},
clear(newVal) {
if (newVal) {
localStorage.removeItem(this.storageKey);
}
},
},
mounted() {
// On mount, trigger update if we actually have a localStorageValue
......
......@@ -27,6 +27,10 @@ export default {
RoleDropdown,
RemoveGroupLinkModal,
ExpirationDatepicker,
LdapOverrideConfirmationModal: () =>
import(
'ee_component/vue_shared/components/members/ldap/ldap_override_confirmation_modal.vue'
),
},
computed: {
...mapState(['members', 'tableFields']),
......@@ -114,5 +118,6 @@ export default {
</template>
</gl-table>
<remove-group-link-modal />
<ldap-override-confirmation-modal />
</div>
</template>
<script>
import $ from 'jquery';
import { GlButton, GlTooltipDirective } from '@gitlab/ui';
import Clipboard from 'clipboard';
import { __ } from '~/locale';
import { uniqueId } from 'lodash';
export default {
components: {
......@@ -17,6 +16,11 @@ export default {
required: false,
default: '',
},
id: {
type: String,
required: false,
default: () => uniqueId('modal-copy-button-'),
},
container: {
type: String,
required: false,
......@@ -52,7 +56,6 @@ export default {
default: null,
},
},
copySuccessText: __('Copied'),
computed: {
modalDomId() {
return this.modalId ? `#${this.modalId}` : '';
......@@ -68,11 +71,11 @@ export default {
});
this.clipboard
.on('success', e => {
this.updateTooltip(e.trigger);
this.$root.$emit('bv::hide::tooltip', this.id);
this.$emit('success', e);
// Clear the selection and blur the trigger so it loses its border
e.clearSelection();
$(e.trigger).blur();
e.trigger.blur();
})
.on('error', e => this.$emit('error', e));
});
......@@ -82,29 +85,11 @@ export default {
this.clipboard.destroy();
}
},
methods: {
updateTooltip(target) {
const $target = $(target);
const originalTitle = $target.data('originalTitle');
if ($target.tooltip) {
/**
* The original tooltip will continue staying there unless we remove it by hand.
* $target.tooltip('hide') isn't working.
*/
$('.tooltip').remove();
$target.attr('title', this.$options.copySuccessText);
$target.tooltip('_fixTitle');
$target.tooltip('show');
$target.attr('title', originalTitle);
$target.tooltip('_fixTitle');
}
},
},
};
</script>
<template>
<gl-button
:id="id"
v-gl-tooltip="{ placement: tooltipPlacement, container: tooltipContainer }"
:class="cssClasses"
:data-clipboard-target="target"
......
import createState from 'ee_else_ce/vuex_shared/modules/members/state';
import * as actions from './actions';
import mutations from './mutations';
import mutations from 'ee_else_ce/vuex_shared/modules/members/mutations';
import * as actions from 'ee_else_ce/vuex_shared/modules/members/actions';
export default initialState => ({
namespaced: true,
......
......@@ -101,10 +101,6 @@
content: '\f071';
}
.fa-external-link::before {
content: '\f08e';
}
.fa-spinner::before {
content: '\f110';
}
......@@ -117,10 +113,6 @@
content: '\f0da';
}
.fa-refresh::before {
content: '\f021';
}
.fa-chevron-up::before {
content: '\f077';
}
......
......@@ -424,7 +424,6 @@ img.emoji {
.w-15p { width: 15%; }
.w-30p { width: 30%; }
.w-60p { width: 60%; }
.w-70p { width: 70%; }
.h-12em { height: 12em; }
.h-32-px { height: 32px;}
......
......@@ -117,12 +117,6 @@ body.modal-open {
border-bottom-right-radius: $modal-border-radius;
}
}
@include media-breakpoint-up(sm) {
.modal-dialog {
margin: 64px auto;
}
}
}
.recaptcha-modal .recaptcha-form {
......
......@@ -56,3 +56,7 @@
vertical-align: text-bottom;
}
}
.spin {
animation: spinner-rotate 2s infinite linear;
}
......@@ -453,11 +453,9 @@
h4,
h5,
h6 {
position: relative;
a.anchor {
left: -16px;
position: absolute;
float: left;
margin-left: -16px;
text-decoration: none;
outline: none;
......
......@@ -77,14 +77,6 @@
}
}
.issuable-filter-count {
span {
display: block;
margin-bottom: -16px;
padding: 13px 0;
}
}
.issuable-show-labels {
.gl-label {
margin-bottom: 5px;
......@@ -662,12 +654,6 @@
}
}
.issuable-form-padding-top {
@include media-breakpoint-up(sm) {
padding-top: 7px;
}
}
.issuable-status-box {
align-self: stretch;
display: flex;
......
......@@ -67,7 +67,6 @@ ul.related-merge-requests > li {
}
}
.merge-request-ci-status,
.related-merge-requests {
.ci-status-link {
display: block;
......@@ -93,11 +92,6 @@ ul.related-merge-requests > li {
}
}
.issues-footer {
padding-top: $gl-padding;
padding-bottom: 37px;
}
.issues-nav-controls,
.new-branch-col {
font-size: 0;
......
......@@ -463,8 +463,7 @@ $mr-widget-min-height: 69px;
.mr-list {
.merge-request {
padding: 10px 0 10px 15px;
position: relative;
padding: 10px $gl-padding;
display: flex;
.issuable-info-container {
......@@ -737,14 +736,6 @@ $mr-widget-min-height: 69px;
border-bottom: 0;
}
.comments-disabled-notif {
line-height: 28px;
.btn {
margin-left: 5px;
}
}
.mr-version-dropdown,
.mr-version-compare-dropdown {
margin: 0 7px;
......
......@@ -226,10 +226,6 @@ table {
display: none;
}
.parallel-comment {
padding: 6px;
}
.error-alert > .alert {
margin-top: 5px;
margin-bottom: 5px;
......@@ -311,31 +307,12 @@ table {
}
}
.discussion-notes-count {
font-size: 16px;
}
.edit_note {
.markdown-area {
min-height: 140px;
max-height: 500px;
}
.note-form-actions {
background: transparent;
}
}
.comment-toolbar {
padding-top: $gl-padding-top;
color: $gl-text-color-secondary;
border-top: 1px solid $border-color;
}
.md-helper {
padding-top: 10px;
}
.toolbar-button {
padding: 0;
background: none;
......
......@@ -801,14 +801,6 @@ $note-form-margin-left: 72px;
margin: 0 3px;
}
.note-role-special {
position: relative;
display: inline-block;
color: $gl-text-color-secondary;
font-size: 12px;
text-shadow: 0 0 15px $gl-text-color-inverted;
}
/**
* Line note button on the side of diffs
*/
......
......@@ -51,43 +51,6 @@
outline: 0;
}
.flex-users-panel {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
@include media-breakpoint-down(sm) {
display: block;
.flex-project-title {
vertical-align: top;
display: inline-block;
max-width: 90%;
}
}
.flex-project-title {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.badge.badge-pill {
height: 17px;
line-height: 16px;
margin-right: 5px;
padding-top: 1px;
padding-bottom: 1px;
}
.flex-users-form {
flex-wrap: nowrap;
white-space: nowrap;
margin-left: auto;
}
}
.content-list.members-list li {
display: flex;
justify-content: space-between;
......
......@@ -31,7 +31,7 @@ nav.navbar-collapse.collapse,
.nav,
.btn,
ul.notes-form,
.merge-request-ci-status .ci-status-link::after,
.ci-status-link::after,
.issuable-gutter-toggle,
.gutter-toggle,
.issuable-details .content-block-small,
......
......@@ -121,7 +121,7 @@ class ApplicationController < ActionController::Base
end
def route_not_found
if current_user
if current_user || browser.bot.search_engine?
not_found
else
store_location_for(:user, request.fullpath) unless request.xhr?
......
# frozen_string_literal: true
class JwksController < ActionController::Base # rubocop:disable Rails/ApplicationController
def index
render json: { keys: keys }
end
private
def keys
[
# We keep openid_connect_signing_key so that we can seamlessly
# replace it with ci_jwt_signing_key and remove it on the next release.
# TODO: Remove openid_connect_signing_key in 13.7
# https://gitlab.com/gitlab-org/gitlab/-/issues/221031
Rails.application.secrets.openid_connect_signing_key,
Gitlab::CurrentSettings.ci_jwt_signing_key
].compact.map do |key_data|
OpenSSL::PKey::RSA.new(key_data)
.public_key
.to_jwk
.slice(:kty, :kid, :e, :n)
.merge(use: 'sig', alg: 'RS256')
end
end
end
......@@ -12,7 +12,6 @@ class Projects::PipelinesController < Projects::ApplicationController
before_action :authorize_create_pipeline!, only: [:new, :create, :config_variables]
before_action :authorize_update_pipeline!, only: [:retry, :cancel]
before_action do
push_frontend_feature_flag(:filter_pipelines_search, project, default_enabled: true)
push_frontend_feature_flag(:dag_pipeline_tab, project, default_enabled: true)
push_frontend_feature_flag(:pipelines_security_report_summary, project)
push_frontend_feature_flag(:new_pipeline_form, project)
......
......@@ -47,6 +47,8 @@ class Projects::StaticSiteEditorController < Projects::ApplicationController
payload.transform_values do |value|
if value.is_a?(String) || value.is_a?(Integer)
value
elsif value.nil?
''
else
value.to_json
end
......
......@@ -15,7 +15,7 @@ class ProjectsController < Projects::ApplicationController
around_action :allow_gitaly_ref_name_caching, only: [:index, :show]
before_action :whitelist_query_limiting, only: [:create]
before_action :authenticate_user!, except: [:index, :show, :activity, :refs, :resolve]
before_action :authenticate_user!, except: [:index, :show, :activity, :refs, :resolve, :unfoldered_environment_names]
before_action :redirect_git_extension, only: [:show]
before_action :project, except: [:index, :new, :create, :resolve]
before_action :repository, except: [:index, :new, :create, :resolve]
......
......@@ -35,6 +35,8 @@ class SearchController < ApplicationController
return unless search_term_valid?
return if check_single_commit_result?
@search_term = params[:search]
@scope = search_service.scope
......@@ -47,8 +49,6 @@ class SearchController < ApplicationController
eager_load_user_status if @scope == 'users'
increment_search_counters
check_single_commit_result
end
def count
......@@ -103,14 +103,23 @@ class SearchController < ApplicationController
@search_objects = @search_objects.eager_load(:status) # rubocop:disable CodeReuse/ActiveRecord
end
def check_single_commit_result
if @search_results.single_commit_result?
only_commit = @search_results.objects('commits').first
query = params[:search].strip.downcase
found_by_commit_sha = Commit.valid_hash?(query) && only_commit.sha.start_with?(query)
def check_single_commit_result?
return false if params[:force_search_results]
return false unless @project.present?
# download_code project policy grants user the read_commit ability
return false unless Ability.allowed?(current_user, :download_code, @project)
redirect_to project_commit_path(@project, only_commit) if found_by_commit_sha
end
query = params[:search].strip.downcase
return false unless Commit.valid_hash?(query)
commit = @project.commit_by(oid: query)
return false unless commit.present?
link = search_path(safe_params.merge(force_search_results: true))
flash[:notice] = html_escape(_("You have been redirected to the only result; see the %{a_start}search results%{a_end} instead.")) % { a_start: "<a href=\"#{link}\"><u>".html_safe, a_end: '</u></a>'.html_safe }
redirect_to project_commit_path(@project, commit)
true
end
def increment_search_counters
......
......@@ -13,7 +13,7 @@
class EnvironmentNamesFinder
attr_reader :project_or_group, :current_user
def initialize(project_or_group, current_user)
def initialize(project_or_group, current_user = nil)
@project_or_group = project_or_group
@current_user = current_user
end
......@@ -38,7 +38,7 @@ class EnvironmentNamesFinder
end
def project_environments
if current_user.can?(:read_environment, project_or_group)
if Ability.allowed?(current_user, :read_environment, project_or_group)
project_or_group.environments
else
Environment.none
......
......@@ -66,6 +66,11 @@ class MergeRequestsFinder < IssuableFinder
by_source_project_id(items)
end
def filter_negated_items(items)
items = super(items)
by_negated_target_branch(items)
end
private
def by_commit(items)
......@@ -98,6 +103,14 @@ class MergeRequestsFinder < IssuableFinder
end
# rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord
def by_negated_target_branch(items)
return items unless not_params[:target_branch]
items.where.not(target_branch: not_params[:target_branch])
end
# rubocop: enable CodeReuse/ActiveRecord
def source_project_id
@source_project_id ||= params[:source_project_id].presence
end
......
# frozen_string_literal: true
module Resolvers
module Ci
class RunnerSetupResolver < BaseResolver
type Types::Ci::RunnerSetupType, null: true
argument :platform, GraphQL::STRING_TYPE,
required: true,
description: 'Platform to generate the instructions for'
argument :architecture, GraphQL::STRING_TYPE,
required: true,
description: 'Architecture to generate the instructions for'
argument :project_id, ::Types::GlobalIDType[::Project],
required: false,
description: 'Project to register the runner for'
argument :group_id, ::Types::GlobalIDType[::Group],
required: false,
description: 'Group to register the runner for'
def resolve(platform:, architecture:, **args)
instructions = Gitlab::Ci::RunnerInstructions.new(
{ current_user: current_user, os: platform, arch: architecture }.merge(target_param(args))
)
{
install_instructions: instructions.install_script,
register_instructions: instructions.register_command
}
ensure
raise Gitlab::Graphql::Errors::ResourceNotAvailable, 'User is not authorized to register a runner for the specified resource!' if instructions.errors.include?('Gitlab::Access::AccessDeniedError')
end
private
def target_param(args)
project_param(args[:project_id]) || group_param(args[:group_id]) || {}
end
def project_param(project_id)
return unless project_id
{ project: find_object(project_id) }
end
def group_param(group_id)
return unless group_id
{ group: find_object(group_id) }
end
def find_object(gid)
GlobalID::Locator.locate(gid)
end
end
end
end
# frozen_string_literal: true
module Types
module Ci
# rubocop: disable Graphql/AuthorizeTypes
class RunnerSetupType < BaseObject
graphql_name 'RunnerSetup'
field :install_instructions, GraphQL::STRING_TYPE, null: false,
description: 'Instructions for installing the runner on the specified architecture'
field :register_instructions, GraphQL::STRING_TYPE, null: false,
description: 'Instructions for registering the runner'
end
end
end
......@@ -3,7 +3,7 @@
module Types
# rubocop: disable Graphql/AuthorizeTypes
class CountableConnectionType < GraphQL::Types::Relay::BaseConnection
field :count, Integer, null: false,
field :count, GraphQL::INT_TYPE, null: false,
description: 'Total count of collection'
def count
......
# frozen_string_literal: true
module Types
# rubocop: disable Graphql/AuthorizeTypes
class IssueConnectionType < CountableConnectionType
end
end
Types::IssueConnectionType.prepend_if_ee('::EE::Types::IssueConnectionType')
......@@ -4,7 +4,7 @@ module Types
class IssueType < BaseObject
graphql_name 'Issue'
connection_type_class(Types::CountableConnectionType)
connection_type_class(Types::IssueConnectionType)
implements(Types::Notes::NoteableType)
implements(Types::CurrentUserTodos)
......@@ -74,6 +74,10 @@ module Types
description: 'Time estimate of the issue'
field :total_time_spent, GraphQL::INT_TYPE, null: false,
description: 'Total time reported as spent on the issue'
field :human_time_estimate, GraphQL::STRING_TYPE, null: true,
description: 'Human-readable time estimate of the issue'
field :human_total_time_spent, GraphQL::STRING_TYPE, null: true,
description: 'Human-readable total time reported as spent on the issue'
field :closed_at, Types::TimeType, null: true,
description: 'Timestamp of when the issue was closed'
......
......@@ -84,6 +84,10 @@ module Types
null: true, description: 'Supported runner platforms',
resolver: Resolvers::Ci::RunnerPlatformsResolver
field :runner_setup, Types::Ci::RunnerSetupType, null: true,
description: 'Get runner setup instructions',
resolver: Resolvers::Ci::RunnerSetupResolver
def design_management
DesignManagementObject.new(nil)
end
......
......@@ -196,7 +196,7 @@ module CommitsHelper
return unless external_url
link_to(external_url, class: 'btn btn-file-option has-tooltip', target: '_blank', rel: 'noopener noreferrer', title: "View on #{environment.formatted_external_url}", data: { container: 'body' }) do
icon('external-link')
sprite_icon('external-link')
end
end
......
......@@ -129,8 +129,7 @@ module DropdownsHelper
end
def dropdown_loading
content_tag :div, class: "dropdown-loading" do
icon('spinner spin')
end
spinner = loading_icon(container: true, size: "md", css_class: "gl-mt-7")
content_tag(:div, spinner, class: "dropdown-loading")
end
end
......@@ -89,15 +89,6 @@ module IconsHelper
sprite_icon(name, css_class: css_class)
end
def spinner(text = nil, visible = false)
css_class = ['loading']
css_class << 'hide' unless visible
content_tag :div, class: css_class.join(' ') do
icon('spinner spin') + text
end
end
def boolean_to_icon(value)
if value
sprite_icon('check', css_class: 'cgreen')
......
# frozen_string_literal: true
module SearchHelper
SEARCH_PERMITTED_PARAMS = [:search, :scope, :project_id, :group_id, :repository_ref, :snippets, :sort, :state, :confidential].freeze
SEARCH_PERMITTED_PARAMS = [
:search,
:scope,
:project_id,
:group_id,
:repository_ref,
:snippets,
:sort,
:state,
:confidential,
:force_search_results
].freeze
def search_autocomplete_opts(term)
return unless current_user
......@@ -9,7 +20,8 @@ module SearchHelper
resources_results = [
recent_items_autocomplete(term),
groups_autocomplete(term),
projects_autocomplete(term)
projects_autocomplete(term),
issue_autocomplete(term)
].flatten
search_pattern = Regexp.new(Regexp.escape(term), "i")
......@@ -172,6 +184,24 @@ module SearchHelper
end
# rubocop: enable CodeReuse/ActiveRecord
def issue_autocomplete(term)
return [] unless @project.present? && current_user && term =~ /\A#{Issue.reference_prefix}\d+\z/
iid = term.sub(Issue.reference_prefix, '').to_i
issue = @project.issues.find_by_iid(iid)
return [] unless issue && Ability.allowed?(current_user, :read_issue, issue)
[
{
category: 'In this project',
id: issue.id,
label: search_result_sanitize("#{issue.title} (#{issue.to_reference})"),
url: issue_path(issue),
avatar_url: issue.project.avatar_url || ''
}
]
end
# Autocomplete results for the current user's projects
# rubocop: disable CodeReuse/ActiveRecord
def projects_autocomplete(term, limit = 5)
......
......@@ -35,13 +35,6 @@ module ServicesHelper
"#{event}_events"
end
def service_save_button(disabled: false)
button_tag(class: 'btn btn-success', type: 'submit', disabled: disabled, data: { qa_selector: 'save_changes_button' }) do
icon('spinner spin', class: 'hidden js-btn-spinner') +
content_tag(:span, 'Save changes', class: 'js-btn-label')
end
end
def scoped_integrations_path
if @project.present?
project_settings_integrations_path(@project)
......
......@@ -9,7 +9,6 @@ class ApplicationSetting < ApplicationRecord
ignore_column :namespace_storage_size_limit, remove_with: '13.5', remove_after: '2020-09-22'
ignore_column :instance_statistics_visibility_private, remove_with: '13.6', remove_after: '2020-10-22'
ignore_column :snowplow_iglu_registry_url, remove_with: '13.6', remove_after: '2020-11-22'
INSTANCE_REVIEW_MIN_USERS = 50
GRAFANA_URL_ERROR_MESSAGE = 'Please check your Grafana URL setting in ' \
......@@ -385,6 +384,9 @@ class ApplicationSetting < ApplicationRecord
validates :raw_blob_request_limit,
numericality: { only_integer: true, greater_than_or_equal_to: 0 }
validates :ci_jwt_signing_key,
rsa_key: true, allow_nil: true
attr_encrypted :asset_proxy_secret_key,
mode: :per_attribute_iv,
key: Settings.attr_encrypted_db_key_base_truncated,
......@@ -410,6 +412,7 @@ class ApplicationSetting < ApplicationRecord
attr_encrypted :recaptcha_site_key, encryption_options_base_truncated_aes_256_gcm
attr_encrypted :slack_app_secret, encryption_options_base_truncated_aes_256_gcm
attr_encrypted :slack_app_verification_token, encryption_options_base_truncated_aes_256_gcm
attr_encrypted :ci_jwt_signing_key, encryption_options_base_truncated_aes_256_gcm
before_validation :ensure_uuid!
......
# frozen_string_literal: true
# The BulkImport import model links together all the models required to for a
# bulk import of groups and projects to a GitLab instance, and associates these
# with the user that initiated the import.
# The BulkImport model links all models required for a bulk import of groups and
# projects to a GitLab instance. It associates the import with the responsible
# user.
class BulkImport < ApplicationRecord
belongs_to :user, optional: false
......
# frozen_string_literal: true
# The BulkImport::Entity represents a Group or Project that is going to be
# imported during the bulk import process. An entity is nested under the a
# parent group when it is not a top level group.
# The BulkImport::Entity represents a Group or Project to be imported during the
# bulk import process. An entity is nested under the parent group when it is not
# a top level group.
#
# A full bulk import entity structure might look like this, where the links are
# parents:
......@@ -15,8 +15,8 @@
# | |
# ProjectEntity Project
#
# The tree structure of the entities will result in the same structure for the
# imported Groups and Projects.
# The tree structure of the entities results in the same structure for imported
# Groups and Projects.
class BulkImports::Entity < ApplicationRecord
self.table_name = 'bulk_import_entities'
......
......@@ -1059,7 +1059,7 @@ module Ci
jwt = Gitlab::Ci::Jwt.for_build(self)
variables.append(key: 'CI_JOB_JWT', value: jwt, public: false, masked: true)
rescue OpenSSL::PKey::RSAError => e
rescue OpenSSL::PKey::RSAError, Gitlab::Ci::Jwt::NoSigningKeyError => e
Gitlab::ErrorTracking.track_exception(e)
end
end
......
......@@ -205,13 +205,8 @@ class CommitStatus < ApplicationRecord
# 'rspec:linux: 1/10' => 'rspec:linux'
common_name = name.to_s.gsub(%r{\d+[\s:\/\\]+\d+\s*}, '')
if ::Gitlab::Ci::Features.one_dimensional_matrix_enabled?
# 'rspec:linux: [aws, max memory]' => 'rspec:linux', 'rspec:linux: [aws]' => 'rspec:linux'
common_name.gsub!(%r{: \[.*\]\s*\z}, '')
else
# 'rspec:linux: [aws, max memory]' => 'rspec:linux', 'rspec:linux: [aws]' => 'rspec:linux: [aws]'
common_name.gsub!(%r{: \[.*, .*\]\s*\z}, '')
end
# 'rspec:linux: [aws, max memory]' => 'rspec:linux', 'rspec:linux: [aws]' => 'rspec:linux'
common_name.gsub!(%r{: \[.*\]\s*\z}, '')
common_name.strip!
common_name
......
......@@ -21,6 +21,7 @@ class GroupMember < Member
scope :of_groups, ->(groups) { where(source_id: groups.select(:id)) }
scope :of_ldap_type, -> { where(ldap: true) }
scope :count_users_by_group_id, -> { group(:source_id).count }
scope :with_user, -> (user) { where(user: user) }
after_create :update_two_factor_requirement, unless: :invite?
after_destroy :update_two_factor_requirement, unless: :invite?
......
......@@ -284,7 +284,8 @@ class Namespace < ApplicationRecord
# that belongs to this namespace
def all_projects
if Feature.enabled?(:recursive_approach_for_all_projects)
Project.where(namespace: self_and_descendants)
namespace = user? ? self : self_and_descendants
Project.where(namespace: namespace)
else
Project.inside_path(full_path)
end
......
# frozen_string_literal: true
module Packages
module Debian
# Returns .deb file metadata
class ExtractDebMetadataService
CommandFailedError = Class.new(StandardError)
def initialize(file_path)
@file_path = file_path
end
def execute
unless success?
raise CommandFailedError, "The `#{cmd}` command failed (status: #{result.status}) with the following error:\n#{result.stderr}"
end
sections = ParseDebian822Service.new(result.stdout).execute
sections.each_value.first
end
private
def cmd
@cmd ||= begin
dpkg_deb_path = Gitlab.config.packages.dpkg_deb_path
[dpkg_deb_path, '--field', @file_path]
end
end
def result
@result ||= Gitlab::Popen.popen_with_detail(cmd)
end
def success?
result.status&.exitstatus == 0
end
end
end
end
# frozen_string_literal: true
module Packages
module Debian
# Parse String as Debian RFC822 control data format
# https://manpages.debian.org/unstable/dpkg-dev/deb822.5
class ParseDebian822Service
InvalidDebian822Error = Class.new(StandardError)
def initialize(input)
@input = input
end
def execute
output = {}
@input.each_line('', chomp: true) do |block|
section = {}
section_name, field = nil
block.each_line(chomp: true) do |line|
next if comment_line?(line)
if continuation_line?(line)
raise InvalidDebian822Error, "Parse error. Unexpected continuation line" if field.nil?
section[field] += "\n"
section[field] += line[1..] unless paragraph_separator?(line)
elsif match = match_section_line(line)
section_name = match[:name] if section_name.nil?
field = match[:field].to_sym
raise InvalidDebian822Error, "Duplicate field '#{field}' in section '#{section_name}'" if section.include?(field)
section[field] = match[:value]
else
raise InvalidDebian822Error, "Parse error on line #{line}"
end
end
raise InvalidDebian822Error, "Duplicate section '#{section_name}'" if output[section_name]
output[section_name] = section
end
output
end
private
def comment_line?(line)
line.match?(/^#/)
end
def continuation_line?(line)
line.match?(/^ /)
end
def paragraph_separator?(line)
line == ' .'
end
def match_section_line(line)
line.match(/(?<name>(?<field>^\S+):\s*(?<value>.*))/)
end
end
end
end
......@@ -62,7 +62,9 @@ class SearchService
end
def search_objects(preload_method = nil)
@search_objects ||= redact_unauthorized_results(search_results.objects(scope, page: params[:page], per_page: per_page, preload_method: preload_method))
@search_objects ||= redact_unauthorized_results(
search_results.objects(scope, page: page, per_page: per_page, preload_method: preload_method)
)
end
def search_highlight
......@@ -71,6 +73,10 @@ class SearchService
private
def page
[1, params[:page].to_i].max
end
def per_page
per_page_param = params[:per_page].to_i
......
# frozen_string_literal: true
# RsaKeyValidator
#
# Custom validator for RSA private keys.
#
# class Project < ActiveRecord::Base
# validates :signing_key, rsa_key: true
# end
#
class RsaKeyValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
unless valid_rsa_key?(value)
record.errors.add(attribute, "is not a valid RSA key")
end
end
private
def valid_rsa_key?(value)
return false unless value
OpenSSL::PKey::RSA.new(value)
rescue OpenSSL::PKey::RSAError
false
end
end
......@@ -14,4 +14,4 @@
'kubernetes-integration-help-path' => help_page_path('user/project/clusters/index'),
'account-and-external-ids-help-path' => help_page_path('user/project/clusters/add_remove_clusters.md', anchor: 'new-eks-cluster'),
'create-role-arn-help-path' => help_page_path('user/project/clusters/add_remove_clusters.md', anchor: 'new-eks-cluster'),
'external-link-icon' => icon('external-link') } }
'external-link-icon' => sprite_icon('external-link') } }
= javascript_include_tag 'https://apis.google.com/js/api.js'
- external_link_icon = icon('external-link')
- external_link_icon = sprite_icon('external-link')
- zones_link_url = 'https://cloud.google.com/compute/docs/regions-zones/regions-zones'
- machine_type_link_url = 'https://cloud.google.com/compute/docs/machine-types'
- pricing_link_url = 'https://cloud.google.com/compute/pricing#machinetype'
......
- @hide_top_links = true
- page_title _("Projects")
- header_title _("Projects"), dashboard_projects_path
- page_canonical_link explore_projects_url
= render_dashboard_gold_trial(current_user)
......
......@@ -272,7 +272,6 @@
.feature-highlight.js-feature-highlight{ disabled: true,
data: { trigger: 'manual',
container: 'body',
toggle: 'popover',
placement: 'right',
highlight: UserCalloutsHelper::GKE_CLUSTER_INTEGRATION,
highlight_priority: UserCallout.feature_names[:GKE_CLUSTER_INTEGRATION],
......
......@@ -9,7 +9,7 @@
= succeed ':' do
= link_to note.author_name, user_url(note.author)
- if discussion.nil?
commented
= link_to 'commented', target_url
- else
- if note.start_of_discussion?
started a new
......
......@@ -34,4 +34,5 @@
%div{ id: dom_id(@project) }
%ol#commits-list.list-unstyled.content_list
= render 'commits', project: @project, ref: @ref
= spinner
.loading.hide
= loading_icon(size: "lg")
......@@ -37,10 +37,10 @@
.detail-page-header-actions.js-issuable-actions.js-issuable-buttons{ data: { "action": "close-reopen" } }
.clearfix.issue-btn-group.dropdown
%button.btn.btn-default.float-left.d-md-none.d-lg-none.d-xl-none{ type: "button", data: { toggle: "dropdown" } }
%button.btn.btn-default.float-left.d-md-none{ type: "button", data: { toggle: "dropdown" } }
Options
= icon('caret-down')
.dropdown-menu.dropdown-menu-right.d-lg-none.d-xl-none
.dropdown-menu.dropdown-menu-right.d-lg-none
%ul
- unless current_user == @issue.author
%li= link_to 'Report abuse', new_abuse_report_path(user_id: @issue.author.id, ref_url: issue_url(@issue))
......@@ -58,9 +58,9 @@
= render 'shared/issuable/close_reopen_button', issuable: @issue, can_update: can_update_issue, can_reopen: can_reopen_issue, warn_before_close: defined?(@issue.blocked?) && @issue.blocked?
- if can_report_spam
= link_to 'Submit as spam', mark_as_spam_project_issue_path(@project, @issue), method: :post, class: 'd-none d-sm-none d-md-block gl-button btn btn-grouped btn-spam', title: 'Submit as spam'
= link_to 'Submit as spam', mark_as_spam_project_issue_path(@project, @issue), method: :post, class: 'd-none d-md-block gl-button btn btn-grouped btn-spam', title: 'Submit as spam'
- if can_create_issue
= link_to new_project_issue_path(@project), class: 'd-none d-sm-none d-md-block gl-button btn btn-grouped btn-success btn-inverted', title: 'New issue', id: 'new_issue_link' do
= link_to new_project_issue_path(@project), class: 'd-none d-md-block gl-button btn btn-grouped btn-success btn-inverted', title: 'New issue', id: 'new_issue_link' do
New issue
.issue-details.issuable-details
......
......@@ -9,7 +9,7 @@
To install this service,
= link_to "#{Gitlab.config.mattermost.host}/select_team", target: '__blank' do
join a team
= icon('external-link')
= sprite_icon('external-link')
and try again.
%hr
.clearfix
......
......@@ -19,7 +19,7 @@
To create a team,
= link_to "#{Gitlab.config.mattermost.host}/create_team" do
use Mattermost's interface
= icon('external-link')
= sprite_icon('external-link')
or ask your Mattermost system administrator.
%hr
%h4 Command trigger word
......@@ -38,7 +38,7 @@
Reserved:
= link_to 'https://docs.mattermost.com/help/messaging/executing-commands.html#built-in-commands', target: '__blank' do
see list of built-in slash commands
= icon('external-link')
= sprite_icon('external-link')
%hr
.clearfix
.float-right
......
......@@ -74,4 +74,4 @@
- if mirror.ssh_key_auth?
= clipboard_button(text: mirror.ssh_public_key, class: 'gl-button btn btn-default', title: _('Copy SSH public key'), qa_selector: 'copy_public_key_button')
= render 'shared/remote_mirror_update_button', remote_mirror: mirror
%button.js-delete-mirror.qa-delete-mirror.rspec-delete-mirror.btn.gl-button.btn-danger{ type: 'button', data: { mirror_id: mirror.id, toggle: 'tooltip', container: 'body' }, title: _('Remove') }= sprite_icon('remove')
%button.js-delete-mirror.qa-delete-mirror.rspec-delete-mirror.btn.btn-icon.gl-button.btn-danger{ type: 'button', data: { mirror_id: mirror.id, toggle: 'tooltip', container: 'body' }, title: _('Remove') }= sprite_icon('remove')
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment