Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
067b3d0457
commit
7a124e225e
|
@ -84,9 +84,9 @@ linters:
|
|||
|
||||
RuboCop:
|
||||
enabled: true
|
||||
# These cops are incredibly noisy when it comes to HAML templates, so we
|
||||
# ignore them.
|
||||
ignored_cops:
|
||||
# These cops are incredibly noisy when it comes to HAML templates, so we
|
||||
# ignore them.
|
||||
- Layout/BlockAlignment
|
||||
- Layout/EndAlignment
|
||||
- Layout/LineLength
|
||||
|
@ -103,6 +103,7 @@ linters:
|
|||
- Style/Next
|
||||
- Style/TrailingWhitespace
|
||||
- Style/WhileUntilModifier
|
||||
- Cop/StaticTranslationDefinition
|
||||
|
||||
# These cops should eventually get enabled
|
||||
- Cop/LineBreakAfterGuardClauses
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
---
|
||||
Cop/StaticTranslationDefinition:
|
||||
Exclude:
|
||||
- 'app/models/application_setting.rb'
|
||||
- 'app/models/diff_viewer/image.rb'
|
||||
- 'app/models/diff_viewer/rich.rb'
|
||||
- 'app/models/diff_viewer/simple.rb'
|
||||
- 'app/models/group_group_link.rb'
|
||||
- 'app/models/integrations/bamboo.rb'
|
||||
- 'app/models/integrations/buildkite.rb'
|
||||
- 'app/models/integrations/drone_ci.rb'
|
||||
- 'app/models/integrations/jenkins.rb'
|
||||
- 'app/models/integrations/jira.rb'
|
||||
- 'app/models/integrations/mock_ci.rb'
|
||||
- 'app/models/integrations/teamcity.rb'
|
||||
- 'app/models/jira_import_state.rb'
|
||||
- 'app/models/member.rb'
|
||||
- 'app/models/project.rb'
|
||||
- 'app/models/project_group_link.rb'
|
||||
- 'app/models/user.rb'
|
||||
- 'app/models/users/banned_user.rb'
|
||||
- 'ee/app/models/allowed_email_domain.rb'
|
||||
- 'ee/app/models/dast/site_profile_secret_variable.rb'
|
||||
- 'ee/app/models/group_merge_request_approval_setting.rb'
|
||||
- 'ee/app/models/incident_management/escalation_policy.rb'
|
||||
- 'ee/app/models/incident_management/escalation_rule.rb'
|
||||
- 'ee/app/models/vulnerabilities/read.rb'
|
||||
- 'ee/lib/ee/gitlab/quick_actions/epic_actions.rb'
|
||||
- 'ee/lib/ee/gitlab/quick_actions/issue_actions.rb'
|
||||
- 'ee/lib/ee/gitlab/quick_actions/issue_and_merge_request_actions.rb'
|
||||
- 'ee/lib/ee/gitlab/quick_actions/merge_request_actions.rb'
|
||||
- 'lib/gitlab/quick_actions/commit_actions.rb'
|
||||
- 'lib/gitlab/quick_actions/issuable_actions.rb'
|
||||
- 'lib/gitlab/quick_actions/issue_actions.rb'
|
||||
- 'lib/gitlab/quick_actions/issue_and_merge_request_actions.rb'
|
||||
- 'lib/gitlab/quick_actions/merge_request_actions.rb'
|
||||
- 'lib/gitlab/quick_actions/relate_actions.rb'
|
|
@ -1101,7 +1101,6 @@ Gitlab/NamespacedClass:
|
|||
- 'ee/lib/gitlab/items_collection.rb'
|
||||
- 'ee/lib/gitlab/manual_banner.rb'
|
||||
- 'ee/lib/gitlab/manual_quarterly_co_term_banner.rb'
|
||||
- 'ee/lib/gitlab/manual_renewal_banner.rb'
|
||||
- 'ee/lib/gitlab/pagination_delegate.rb'
|
||||
- 'ee/lib/gitlab/path_locks_finder.rb'
|
||||
- 'ee/lib/gitlab/proxy.rb'
|
||||
|
|
|
@ -1052,7 +1052,6 @@ RSpec/ContextWording:
|
|||
- 'ee/spec/services/vulnerability_exports/export_service_spec.rb'
|
||||
- 'ee/spec/services/vulnerability_external_issue_links/create_service_spec.rb'
|
||||
- 'ee/spec/support/features/manual_quarterly_co_term_banner_examples.rb'
|
||||
- 'ee/spec/support/features/manual_renewal_banner_examples.rb'
|
||||
- 'ee/spec/support/protected_tags/access_control_shared_examples.rb'
|
||||
- 'ee/spec/support/shared_contexts/audit_event_not_licensed_shared_context.rb'
|
||||
- 'ee/spec/support/shared_contexts/audit_event_queue_shared_context.rb'
|
||||
|
|
|
@ -263,7 +263,6 @@ Style/FormatString:
|
|||
- 'ee/lib/gitlab/expiring_subscription_message.rb'
|
||||
- 'ee/lib/gitlab/geo.rb'
|
||||
- 'ee/lib/gitlab/manual_quarterly_co_term_banner.rb'
|
||||
- 'ee/lib/gitlab/manual_renewal_banner.rb'
|
||||
- 'ee/lib/gitlab/vulnerabilities/container_scanning_vulnerability.rb'
|
||||
- 'ee/lib/tasks/gitlab/elastic.rake'
|
||||
- 'ee/spec/controllers/admin/licenses_controller_spec.rb'
|
||||
|
|
|
@ -1 +1 @@
|
|||
1.58.0
|
||||
1.59.0
|
||||
|
|
4
Gemfile
4
Gemfile
|
@ -479,7 +479,7 @@ gem 'sys-filesystem', '~> 1.4.3'
|
|||
gem 'net-ntp'
|
||||
|
||||
# SSH keys support
|
||||
gem 'ssh_data', '~> 1.2'
|
||||
gem 'ssh_data', '~> 1.3'
|
||||
|
||||
# Spamcheck GRPC protocol definitions
|
||||
gem 'spamcheck', '~> 0.1.0'
|
||||
|
@ -547,3 +547,5 @@ gem 'ipaddress', '~> 0.8.3'
|
|||
gem 'parslet', '~> 1.8'
|
||||
|
||||
gem 'ipynbdiff', '0.4.7'
|
||||
|
||||
gem 'ed25519', '~> 1.3.0'
|
||||
|
|
|
@ -313,6 +313,7 @@ GEM
|
|||
e2mmap (0.1.0)
|
||||
ecma-re-validator (0.3.0)
|
||||
regexp_parser (~> 2.0)
|
||||
ed25519 (1.3.0)
|
||||
elasticsearch (7.13.3)
|
||||
elasticsearch-api (= 7.13.3)
|
||||
elasticsearch-transport (= 7.13.3)
|
||||
|
@ -1285,7 +1286,7 @@ GEM
|
|||
activesupport (>= 5.2)
|
||||
sprockets (>= 3.0.0)
|
||||
sqlite3 (1.4.2)
|
||||
ssh_data (1.2.0)
|
||||
ssh_data (1.3.0)
|
||||
ssrf_filter (1.0.7)
|
||||
stackprof (0.2.15)
|
||||
state_machines (0.5.0)
|
||||
|
@ -1501,6 +1502,7 @@ DEPENDENCIES
|
|||
discordrb-webhooks (~> 3.4)
|
||||
doorkeeper (~> 5.5.0.rc2)
|
||||
doorkeeper-openid_connect (~> 1.7.5)
|
||||
ed25519 (~> 1.3.0)
|
||||
elasticsearch-api (= 7.13.3)
|
||||
elasticsearch-model (~> 7.2)
|
||||
elasticsearch-rails (~> 7.2)
|
||||
|
@ -1704,7 +1706,7 @@ DEPENDENCIES
|
|||
spring-commands-rspec (~> 1.0.4)
|
||||
sprite-factory (~> 1.7)
|
||||
sprockets (~> 3.7.0)
|
||||
ssh_data (~> 1.2)
|
||||
ssh_data (~> 1.3)
|
||||
stackprof (~> 0.2.15)
|
||||
state_machines-activerecord (~> 0.8.0)
|
||||
sys-filesystem (~> 1.4.3)
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<script>
|
||||
import { mapActions, mapGetters } from 'vuex';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { refreshCurrentPage } from '~/lib/utils/url_utility';
|
||||
import BoardContent from '~/boards/components/board_content.vue';
|
||||
import BoardSettingsSidebar from '~/boards/components/board_settings_sidebar.vue';
|
||||
import BoardTopBar from '~/boards/components/board_top_bar.vue';
|
||||
|
@ -14,11 +15,11 @@ export default {
|
|||
computed: {
|
||||
...mapGetters(['isSidebarOpen']),
|
||||
},
|
||||
mounted() {
|
||||
this.performSearch();
|
||||
created() {
|
||||
window.addEventListener('popstate', refreshCurrentPage);
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['performSearch']),
|
||||
destroyed() {
|
||||
window.removeEventListener('popstate', refreshCurrentPage);
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -14,6 +14,8 @@ import { mapActions, mapGetters, mapState } from 'vuex';
|
|||
import BoardForm from 'ee_else_ce/boards/components/board_form.vue';
|
||||
|
||||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
import { isMetaKey } from '~/lib/utils/common_utils';
|
||||
import { updateHistory } from '~/lib/utils/url_utility';
|
||||
import { s__ } from '~/locale';
|
||||
|
||||
import eventHub from '../eventhub';
|
||||
|
@ -21,6 +23,7 @@ import groupBoardsQuery from '../graphql/group_boards.query.graphql';
|
|||
import projectBoardsQuery from '../graphql/project_boards.query.graphql';
|
||||
import groupRecentBoardsQuery from '../graphql/group_recent_boards.query.graphql';
|
||||
import projectRecentBoardsQuery from '../graphql/project_recent_boards.query.graphql';
|
||||
import { fullBoardId } from '../boards_util';
|
||||
|
||||
const MIN_BOARDS_TO_VIEW_RECENT = 10;
|
||||
|
||||
|
@ -112,6 +115,9 @@ export default {
|
|||
this.scrollFadeInitialized = false;
|
||||
this.$nextTick(this.setScrollFade);
|
||||
},
|
||||
board(newBoard) {
|
||||
document.title = newBoard.name;
|
||||
},
|
||||
},
|
||||
created() {
|
||||
eventHub.$on('showBoardModal', this.showPage);
|
||||
|
@ -120,7 +126,7 @@ export default {
|
|||
eventHub.$off('showBoardModal', this.showPage);
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['setError', 'setBoardConfig']),
|
||||
...mapActions(['setError', 'fetchBoard', 'unsetActiveId']),
|
||||
showPage(page) {
|
||||
this.currentPage = page;
|
||||
},
|
||||
|
@ -196,6 +202,22 @@ export default {
|
|||
|
||||
this.hasScrollFade = this.isScrolledUp();
|
||||
},
|
||||
fetchCurrentBoard(boardId) {
|
||||
this.fetchBoard({
|
||||
fullPath: this.fullPath,
|
||||
fullBoardId: fullBoardId(boardId),
|
||||
boardType: this.boardType,
|
||||
});
|
||||
},
|
||||
async switchBoard(boardId, e) {
|
||||
if (isMetaKey(e)) {
|
||||
window.open(`${this.boardBaseUrl}/${boardId}`, '_blank');
|
||||
} else {
|
||||
this.unsetActiveId();
|
||||
this.fetchCurrentBoard(boardId);
|
||||
updateHistory({ url: `${this.boardBaseUrl}/${boardId}` });
|
||||
}
|
||||
},
|
||||
},
|
||||
i18n: {
|
||||
errorFetchingBoard: s__('Board|An error occurred while fetching the board, please try again.'),
|
||||
|
@ -242,8 +264,8 @@ export default {
|
|||
<gl-dropdown-item
|
||||
v-for="recentBoard in recentBoards"
|
||||
:key="`recent-${recentBoard.id}`"
|
||||
:href="`${boardBaseUrl}/${recentBoard.id}`"
|
||||
data-testid="dropdown-item"
|
||||
@click.prevent="switchBoard(recentBoard.id, $event)"
|
||||
>
|
||||
{{ recentBoard.name }}
|
||||
</gl-dropdown-item>
|
||||
|
@ -258,8 +280,8 @@ export default {
|
|||
<gl-dropdown-item
|
||||
v-for="otherBoard in filteredBoards"
|
||||
:key="otherBoard.id"
|
||||
:href="`${boardBaseUrl}/${otherBoard.id}`"
|
||||
data-testid="dropdown-item"
|
||||
@click.prevent="switchBoard(otherBoard.id, $event)"
|
||||
>
|
||||
{{ otherBoard.name }}
|
||||
</gl-dropdown-item>
|
||||
|
|
|
@ -68,8 +68,7 @@ export default {
|
|||
commit(types.RECEIVE_BOARD_FAILURE);
|
||||
} else {
|
||||
const board = data.workspace?.board;
|
||||
commit(types.RECEIVE_BOARD_SUCCESS, board);
|
||||
dispatch('setBoardConfig', board);
|
||||
dispatch('setBoard', board);
|
||||
}
|
||||
})
|
||||
.catch(() => commit(types.RECEIVE_BOARD_FAILURE));
|
||||
|
@ -420,9 +419,6 @@ export default {
|
|||
fetchItemsForList: ({ state, commit }, { listId, fetchNext = false }) => {
|
||||
if (!listId) return null;
|
||||
|
||||
if (!fetchNext) {
|
||||
commit(types.RESET_ITEMS_FOR_LIST, listId);
|
||||
}
|
||||
commit(types.REQUEST_ITEMS_FOR_LIST, { listId, fetchNext });
|
||||
|
||||
const { fullPath, fullBoardId, boardType, filterParams } = state;
|
||||
|
@ -444,6 +440,7 @@ export default {
|
|||
isSingleRequest: true,
|
||||
},
|
||||
variables,
|
||||
...(!fetchNext ? { fetchPolicy: fetchPolicies.NO_CACHE } : {}),
|
||||
})
|
||||
.then(({ data }) => {
|
||||
const { lists } = data[boardType].board;
|
||||
|
|
|
@ -18,7 +18,6 @@ export const MOVE_LISTS = 'MOVE_LISTS';
|
|||
export const TOGGLE_LIST_COLLAPSED = 'TOGGLE_LIST_COLLAPSED';
|
||||
export const REMOVE_LIST = 'REMOVE_LIST';
|
||||
export const REMOVE_LIST_FAILURE = 'REMOVE_LIST_FAILURE';
|
||||
export const RESET_ITEMS_FOR_LIST = 'RESET_ITEMS_FOR_LIST';
|
||||
export const REQUEST_ITEMS_FOR_LIST = 'REQUEST_ITEMS_FOR_LIST';
|
||||
export const RECEIVE_ITEMS_FOR_LIST_FAILURE = 'RECEIVE_ITEMS_FOR_LIST_FAILURE';
|
||||
export const RECEIVE_ITEMS_FOR_LIST_SUCCESS = 'RECEIVE_ITEMS_FOR_LIST_SUCCESS';
|
||||
|
|
|
@ -145,11 +145,6 @@ export default {
|
|||
state.boardLists = listsBackup;
|
||||
},
|
||||
|
||||
[mutationTypes.RESET_ITEMS_FOR_LIST]: (state, listId) => {
|
||||
Vue.set(state, 'backupItemsList', state.boardItemsByListId[listId]);
|
||||
Vue.set(state.boardItemsByListId, listId, []);
|
||||
},
|
||||
|
||||
[mutationTypes.REQUEST_ITEMS_FOR_LIST]: (state, { listId, fetchNext }) => {
|
||||
Vue.set(state.listsFlags, listId, { [fetchNext ? 'isLoadingMore' : 'isLoading']: true });
|
||||
},
|
||||
|
@ -185,7 +180,6 @@ export default {
|
|||
'Boards|An error occurred while fetching the board issues. Please reload the page.',
|
||||
);
|
||||
Vue.set(state.listsFlags, listId, { isLoading: false, isLoadingMore: false });
|
||||
Vue.set(state.boardItemsByListId, listId, state.backupItemsList);
|
||||
},
|
||||
|
||||
[mutationTypes.RESET_ISSUES]: (state) => {
|
||||
|
|
|
@ -13,7 +13,6 @@ export default () => ({
|
|||
boardLists: {},
|
||||
listsFlags: {},
|
||||
boardItemsByListId: {},
|
||||
backupItemsList: [],
|
||||
isSettingAssignees: false,
|
||||
pageInfoByListId: {},
|
||||
boardItems: {},
|
||||
|
|
|
@ -19,6 +19,7 @@ const PRODUCT_INFO = {
|
|||
variant: 'SaaS',
|
||||
},
|
||||
};
|
||||
const EMPTY_NAMESPACE_ID_VALUE = 'not available';
|
||||
|
||||
const generateProductInfo = (sku, quantity) => {
|
||||
const product = PRODUCT_INFO[sku];
|
||||
|
@ -200,6 +201,10 @@ export const trackCheckout = (selectedPlan, quantity) => {
|
|||
pushEnhancedEcommerceEvent('EECCheckout', eventData);
|
||||
};
|
||||
|
||||
export const getNamespaceId = () => {
|
||||
return window.gl.snowplowStandardContext?.data?.namespace_id || EMPTY_NAMESPACE_ID_VALUE;
|
||||
};
|
||||
|
||||
export const trackTransaction = (transactionDetails) => {
|
||||
if (!isSupported()) {
|
||||
return;
|
||||
|
@ -208,6 +213,7 @@ export const trackTransaction = (transactionDetails) => {
|
|||
const transactionId = uuidv4();
|
||||
const { paymentOption, revenue, tax, selectedPlan, quantity } = transactionDetails;
|
||||
const product = generateProductInfo(selectedPlan, quantity);
|
||||
const namespaceId = getNamespaceId();
|
||||
|
||||
if (Object.keys(product).length === 0) {
|
||||
return;
|
||||
|
@ -224,7 +230,7 @@ export const trackTransaction = (transactionDetails) => {
|
|||
revenue: revenue.toString(),
|
||||
tax: tax.toString(),
|
||||
},
|
||||
products: [product],
|
||||
products: [{ ...product, dimension36: namespaceId }],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -16,7 +16,7 @@ export default {
|
|||
if (isEndingStatus(this.session.status)) {
|
||||
return {
|
||||
action: () => this.restartSession(),
|
||||
variant: 'info',
|
||||
variant: 'confirm',
|
||||
category: 'primary',
|
||||
text: __('Restart Terminal'),
|
||||
};
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
GlModal,
|
||||
GlModalDirective,
|
||||
} from '@gitlab/ui';
|
||||
import { __ } from '~/locale';
|
||||
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
|
||||
import { I18N_PAGERDUTY_SETTINGS_FORM, CONFIGURE_PAGERDUTY_WEBHOOK_DOCS_LINK } from '../constants';
|
||||
|
||||
|
@ -42,6 +43,21 @@ export default {
|
|||
};
|
||||
},
|
||||
i18n: I18N_PAGERDUTY_SETTINGS_FORM,
|
||||
modal: {
|
||||
id: 'resetWebhookModal',
|
||||
actionPrimary: {
|
||||
text: I18N_PAGERDUTY_SETTINGS_FORM.webhookUrl.resetWebhookUrl,
|
||||
attributes: {
|
||||
variant: 'danger',
|
||||
},
|
||||
},
|
||||
actionCancel: {
|
||||
text: __('Cancel'),
|
||||
attributes: {
|
||||
variant: 'default',
|
||||
},
|
||||
},
|
||||
},
|
||||
CONFIGURE_PAGERDUTY_WEBHOOK_DOCS_LINK,
|
||||
computed: {
|
||||
formData() {
|
||||
|
@ -152,11 +168,11 @@ export default {
|
|||
{{ $options.i18n.webhookUrl.resetWebhookUrl }}
|
||||
</gl-button>
|
||||
<gl-modal
|
||||
modal-id="resetWebhookModal"
|
||||
:modal-id="$options.modal.id"
|
||||
:title="$options.i18n.webhookUrl.resetWebhookUrl"
|
||||
:ok-title="$options.i18n.webhookUrl.resetWebhookUrl"
|
||||
ok-variant="danger"
|
||||
@ok="resetWebhookUrl"
|
||||
:action-primary="$options.modal.actionPrimary"
|
||||
:action-cancel="$options.modal.actionCancel"
|
||||
@primary="resetWebhookUrl"
|
||||
>
|
||||
{{ $options.i18n.webhookUrl.restKeyInfo }}
|
||||
</gl-modal>
|
||||
|
|
|
@ -57,7 +57,7 @@ export function initShow() {
|
|||
const { issueType, ...issuableData } = parseIssuableData(el);
|
||||
|
||||
if (issueType === IssueType.Incident) {
|
||||
initIncidentApp(issuableData);
|
||||
initIncidentApp({ ...issuableData, issuableId: el.dataset.issuableId });
|
||||
initHeaderActions(store, IssueType.Incident);
|
||||
initRelatedIssues(IssueType.Incident);
|
||||
} else {
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
query GetTimelineEvents($fullPath: ID!, $incidentId: IssueID!) {
|
||||
project(fullPath: $fullPath) {
|
||||
id
|
||||
incidentManagementTimelineEvents(incidentId: $incidentId) {
|
||||
nodes {
|
||||
id
|
||||
author {
|
||||
id
|
||||
name
|
||||
username
|
||||
}
|
||||
note
|
||||
noteHtml
|
||||
action
|
||||
occurredAt
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
<script>
|
||||
import { formatDate } from '~/lib/utils/datetime_utility';
|
||||
import IncidentTimelineEventListItem from './timeline_events_list_item.vue';
|
||||
|
||||
export default {
|
||||
name: 'IncidentTimelineEventList',
|
||||
components: {
|
||||
IncidentTimelineEventListItem,
|
||||
},
|
||||
props: {
|
||||
timelineEventLoading: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
timelineEvents: {
|
||||
type: Array,
|
||||
required: true,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
dateGroupedEvents() {
|
||||
const groupedEvents = new Map();
|
||||
|
||||
this.timelineEvents.forEach((event) => {
|
||||
const date = formatDate(event.occurredAt, 'isoDate', true);
|
||||
|
||||
if (groupedEvents.has(date)) {
|
||||
groupedEvents.get(date).push(event);
|
||||
} else {
|
||||
groupedEvents.set(date, [event]);
|
||||
}
|
||||
});
|
||||
|
||||
return groupedEvents;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
isLastItem(groups, groupIndex, events, eventIndex) {
|
||||
if (groupIndex < groups.size - 1) {
|
||||
return false;
|
||||
}
|
||||
return eventIndex === events.length - 1;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="issuable-discussion incident-timeline-events">
|
||||
<div
|
||||
v-for="([eventDate, events], groupIndex) in dateGroupedEvents"
|
||||
:key="eventDate"
|
||||
data-testid="timeline-group"
|
||||
>
|
||||
<div class="gl-pb-3 gl-border-gray-50 gl-border-1 gl-border-b-solid">
|
||||
<strong class="gl-font-size-h2" data-testid="event-date">{{ eventDate }}</strong>
|
||||
</div>
|
||||
<ul class="notes main-notes-list gl-pl-n3">
|
||||
<incident-timeline-event-list-item
|
||||
v-for="(event, eventIndex) in events"
|
||||
:key="event.id"
|
||||
:action="event.action"
|
||||
:occurred-at="event.occurredAt"
|
||||
:note-html="event.noteHtml"
|
||||
:is-last-item="isLastItem(dateGroupedEvents, groupIndex, events, eventIndex)"
|
||||
data-testid="timeline-event"
|
||||
/>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,71 @@
|
|||
<script>
|
||||
import { GlIcon, GlSafeHtmlDirective, GlSprintf } from '@gitlab/ui';
|
||||
import { formatDate } from '~/lib/utils/datetime_utility';
|
||||
import { __ } from '~/locale';
|
||||
import { getEventIcon } from './utils';
|
||||
|
||||
export default {
|
||||
name: 'IncidentTimelineEventListItem',
|
||||
i18n: {
|
||||
timeUTC: __('%{time} UTC'),
|
||||
},
|
||||
components: {
|
||||
GlIcon,
|
||||
GlSprintf,
|
||||
},
|
||||
directives: {
|
||||
SafeHtml: GlSafeHtmlDirective,
|
||||
},
|
||||
props: {
|
||||
isLastItem: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
occurredAt: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
action: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
noteHtml: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
time() {
|
||||
return formatDate(this.occurredAt, 'HH:MM', true);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getEventIcon,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<li
|
||||
class="timeline-entry timeline-entry-vertical-line note system-note note-wrapper gl-my-2! gl-pr-0!"
|
||||
>
|
||||
<div class="gl-display-flex gl-align-items-center">
|
||||
<div
|
||||
class="gl-display-flex gl-align-items-center gl-justify-content-center gl-bg-white gl-text-gray-200 gl-border-gray-100 gl-border-1 gl-border-solid gl-rounded-full gl-mt-n2 gl-mr-3 gl-w-8 gl-h-8 gl-p-3 gl-z-index-1"
|
||||
>
|
||||
<gl-icon :name="getEventIcon(action)" class="note-icon" />
|
||||
</div>
|
||||
<div
|
||||
class="timeline-event-note gl-w-full"
|
||||
:class="{ 'gl-pb-3 gl-border-gray-50 gl-border-1 gl-border-b-solid': !isLastItem }"
|
||||
data-testid="event-text-container"
|
||||
>
|
||||
<strong class="gl-font-lg" data-testid="event-time">
|
||||
<gl-sprintf :message="$options.i18n.timeUTC">
|
||||
<template #time>{{ time }}</template>
|
||||
</gl-sprintf>
|
||||
</strong>
|
||||
<div v-safe-html="noteHtml"></div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
|
@ -1,21 +1,70 @@
|
|||
<script>
|
||||
import { GlTab, GlButton } from '@gitlab/ui';
|
||||
import { GlEmptyState, GlLoadingIcon, GlTab } from '@gitlab/ui';
|
||||
import { convertToGraphQLId } from '~/graphql_shared/utils';
|
||||
import { TYPE_ISSUE } from '~/graphql_shared/constants';
|
||||
import { fetchPolicies } from '~/lib/graphql';
|
||||
import getTimelineEvents from './graphql/queries/get_timeline_events.query.graphql';
|
||||
import { displayAndLogError } from './utils';
|
||||
|
||||
import IncidentTimelineEventsList from './timeline_events_list.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlEmptyState,
|
||||
GlLoadingIcon,
|
||||
GlTab,
|
||||
GlButton,
|
||||
IncidentTimelineEventsList,
|
||||
},
|
||||
inject: ['fullPath', 'issuableId'],
|
||||
data() {
|
||||
return {
|
||||
timelineEvents: [],
|
||||
};
|
||||
},
|
||||
apollo: {
|
||||
timelineEvents: {
|
||||
fetchPolicy: fetchPolicies.CACHE_AND_NETWORK,
|
||||
query: getTimelineEvents,
|
||||
variables() {
|
||||
return {
|
||||
fullPath: this.fullPath,
|
||||
incidentId: convertToGraphQLId(TYPE_ISSUE, this.issuableId),
|
||||
};
|
||||
},
|
||||
update(data) {
|
||||
return data.project.incidentManagementTimelineEvents.nodes;
|
||||
},
|
||||
error(error) {
|
||||
displayAndLogError(error);
|
||||
},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
timelineEventLoading() {
|
||||
return this.$apollo.queries.timelineEvents.loading;
|
||||
},
|
||||
hasTimelineEvents() {
|
||||
return Boolean(this.timelineEvents.length);
|
||||
},
|
||||
showEmptyState() {
|
||||
return !this.timelineEventLoading && !this.hasTimelineEvents;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-tab :title="s__('Incident|Timeline')">
|
||||
<div class="gl-my-4">
|
||||
<p>{{ s__('Incident|No timeline items have been added yet.') }}</p>
|
||||
</div>
|
||||
<gl-button class="gl-my-3">
|
||||
{{ s__('Incident|Add new timeline event') }}
|
||||
</gl-button>
|
||||
<gl-loading-icon v-if="timelineEventLoading" size="lg" color="dark" class="gl-mt-5" />
|
||||
<gl-empty-state
|
||||
v-else-if="showEmptyState"
|
||||
:compact="true"
|
||||
:description="s__('Incident|No timeline items have been added yet.')"
|
||||
/>
|
||||
<incident-timeline-events-list
|
||||
v-if="hasTimelineEvents"
|
||||
:timeline-event-loading="timelineEventLoading"
|
||||
:timeline-events="timelineEvents"
|
||||
/>
|
||||
</gl-tab>
|
||||
</template>
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
import { createAlert } from '~/flash';
|
||||
import { s__ } from '~/locale';
|
||||
|
||||
export const displayAndLogError = (error) =>
|
||||
createAlert({
|
||||
message: s__('Incident|Something went wrong while fetching incident timeline events.'),
|
||||
captureError: true,
|
||||
error,
|
||||
});
|
||||
|
||||
const EVENT_ICONS = {
|
||||
comment: 'comment',
|
||||
default: 'comment',
|
||||
};
|
||||
|
||||
export const getEventIcon = (actionName) => {
|
||||
return EVENT_ICONS[actionName] ?? EVENT_ICONS.default;
|
||||
};
|
|
@ -33,6 +33,7 @@ export function initIncidentApp(issueData = {}) {
|
|||
canCreateIncident,
|
||||
canUpdate,
|
||||
iid,
|
||||
issuableId,
|
||||
projectNamespace,
|
||||
projectPath,
|
||||
projectId,
|
||||
|
@ -53,6 +54,7 @@ export function initIncidentApp(issueData = {}) {
|
|||
canUpdate,
|
||||
fullPath,
|
||||
iid,
|
||||
issuableId,
|
||||
projectId,
|
||||
slaFeatureAvailable: parseBoolean(slaFeatureAvailable),
|
||||
uploadMetricsFeatureAvailable: parseBoolean(uploadMetricsFeatureAvailable),
|
||||
|
|
|
@ -47,7 +47,7 @@ export default {
|
|||
<gl-button category="secondary" @click="cancelHandler">{{ s__('Metrics|Cancel') }}</gl-button>
|
||||
<gl-button
|
||||
category="secondary"
|
||||
variant="info"
|
||||
variant="confirm"
|
||||
target="_blank"
|
||||
:href="addDashboardDocumentationPath"
|
||||
data-testid="create-dashboard-modal-docs-button"
|
||||
|
|
|
@ -162,7 +162,7 @@ export default {
|
|||
ref="viewDocumentationBtn"
|
||||
category="secondary"
|
||||
class="gl-xs-w-full gl-xs-mb-3"
|
||||
variant="info"
|
||||
variant="confirm"
|
||||
target="_blank"
|
||||
:href="addDashboardDocumentationPath"
|
||||
>
|
||||
|
|
|
@ -19,7 +19,7 @@ export default class PayloadDownloader {
|
|||
}
|
||||
|
||||
requestPayload() {
|
||||
this.spinner.classList.add('d-inline-flex');
|
||||
this.spinner.classList.add('gl-display-inline');
|
||||
|
||||
return axios
|
||||
.get(this.trigger.dataset.endpoint, {
|
||||
|
@ -34,7 +34,7 @@ export default class PayloadDownloader {
|
|||
});
|
||||
})
|
||||
.finally(() => {
|
||||
this.spinner.classList.remove('d-inline-flex');
|
||||
this.spinner.classList.remove('gl-display-inline');
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -29,7 +29,7 @@ export default class PayloadPreviewer {
|
|||
requestPayload() {
|
||||
if (this.isInserted) return this.showPayload();
|
||||
|
||||
this.spinner.classList.add('gl-display-inline-flex');
|
||||
this.spinner.classList.add('gl-display-inline');
|
||||
|
||||
const container = this.getContainer();
|
||||
|
||||
|
@ -38,11 +38,11 @@ export default class PayloadPreviewer {
|
|||
responseType: 'text',
|
||||
})
|
||||
.then(({ data }) => {
|
||||
this.spinner.classList.remove('gl-display-inline-flex');
|
||||
this.spinner.classList.remove('gl-display-inline');
|
||||
this.insertPayload(data);
|
||||
})
|
||||
.catch(() => {
|
||||
this.spinner.classList.remove('gl-display-inline-flex');
|
||||
this.spinner.classList.remove('gl-display-inline');
|
||||
createFlash({
|
||||
message: __('Error fetching payload data.'),
|
||||
});
|
||||
|
|
|
@ -4,7 +4,8 @@ import { localTimeAgo } from '~/lib/utils/datetime_utility';
|
|||
import initCompareAutocomplete from './compare_autocomplete';
|
||||
import initTargetProjectDropdown from './target_project_dropdown';
|
||||
|
||||
const updateCommitList = (url, $loadingIndicator, $commitList, params) => {
|
||||
const updateCommitList = (url, $emptyState, $loadingIndicator, $commitList, params) => {
|
||||
$emptyState.hide();
|
||||
$loadingIndicator.show();
|
||||
$commitList.empty();
|
||||
|
||||
|
@ -16,6 +17,10 @@ const updateCommitList = (url, $loadingIndicator, $commitList, params) => {
|
|||
$loadingIndicator.hide();
|
||||
$commitList.html(data);
|
||||
localTimeAgo($commitList.get(0).querySelectorAll('.js-timeago'));
|
||||
|
||||
if (!data) {
|
||||
$emptyState.show();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -26,6 +31,7 @@ export default (mrNewCompareNode) => {
|
|||
const updateSourceBranchCommitList = () =>
|
||||
updateCommitList(
|
||||
sourceBranchUrl,
|
||||
$(mrNewCompareNode).find('.js-source-commit-empty'),
|
||||
$(mrNewCompareNode).find('.js-source-loading'),
|
||||
$(mrNewCompareNode).find('.mr_source_commit'),
|
||||
{
|
||||
|
@ -35,6 +41,7 @@ export default (mrNewCompareNode) => {
|
|||
const updateTargetBranchCommitList = () =>
|
||||
updateCommitList(
|
||||
targetBranchUrl,
|
||||
$(mrNewCompareNode).find('.js-target-commit-empty'),
|
||||
$(mrNewCompareNode).find('.js-target-loading'),
|
||||
$(mrNewCompareNode).find('.mr_target_commit'),
|
||||
{
|
||||
|
|
|
@ -12,6 +12,7 @@ export default function initCompareAutocomplete(limitTo = null, clickHandler = (
|
|||
$('.js-compare-dropdown').each(function () {
|
||||
const $dropdown = $(this);
|
||||
const selected = $dropdown.data('selected');
|
||||
const defaultText = $dropdown.data('defaultText').trim();
|
||||
const $dropdownContainer = $dropdown.closest('.dropdown');
|
||||
const $fieldInput = $(`input[name="${$dropdown.data('fieldName')}"]`, $dropdownContainer);
|
||||
const $filterInput = $('input[type="search"]', $dropdownContainer);
|
||||
|
@ -63,7 +64,11 @@ export default function initCompareAutocomplete(limitTo = null, clickHandler = (
|
|||
return $el.attr('data-ref');
|
||||
},
|
||||
toggleLabel(obj, $el) {
|
||||
return $el.text().trim();
|
||||
if ($el.hasClass('is-active')) {
|
||||
return $el.text().trim();
|
||||
}
|
||||
|
||||
return defaultText;
|
||||
},
|
||||
clicked: () => clickHandler($dropdown),
|
||||
});
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
<script>
|
||||
import { GlTooltipDirective } from '@gitlab/ui';
|
||||
import { GlBadge, GlTab, GlTooltipDirective } from '@gitlab/ui';
|
||||
import { createAlert, VARIANT_SUCCESS } from '~/flash';
|
||||
import { TYPE_CI_RUNNER } from '~/graphql_shared/constants';
|
||||
import { convertToGraphQLId } from '~/graphql_shared/utils';
|
||||
import { redirectTo } from '~/lib/utils/url_utility';
|
||||
import { formatJobCount } from '../utils';
|
||||
import RunnerDeleteButton from '../components/runner_delete_button.vue';
|
||||
import RunnerEditButton from '../components/runner_edit_button.vue';
|
||||
import RunnerPauseButton from '../components/runner_pause_button.vue';
|
||||
import RunnerHeader from '../components/runner_header.vue';
|
||||
import RunnerDetails from '../components/runner_details.vue';
|
||||
import RunnerJobs from '../components/runner_jobs.vue';
|
||||
import { I18N_FETCH_ERROR } from '../constants';
|
||||
import runnerQuery from '../graphql/show/runner.query.graphql';
|
||||
import { captureException } from '../sentry_utils';
|
||||
|
@ -17,11 +19,14 @@ import { saveAlertToLocalStorage } from '../local_storage_alert/save_alert_to_lo
|
|||
export default {
|
||||
name: 'AdminRunnerShowApp',
|
||||
components: {
|
||||
GlBadge,
|
||||
GlTab,
|
||||
RunnerDeleteButton,
|
||||
RunnerEditButton,
|
||||
RunnerPauseButton,
|
||||
RunnerHeader,
|
||||
RunnerDetails,
|
||||
RunnerJobs,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
|
@ -63,6 +68,9 @@ export default {
|
|||
canDelete() {
|
||||
return this.runner.userPermissions?.deleteRunner;
|
||||
},
|
||||
jobCount() {
|
||||
return formatJobCount(this.runner?.jobCount);
|
||||
},
|
||||
},
|
||||
errorCaptured(error) {
|
||||
this.reportToSentry(error);
|
||||
|
@ -88,6 +96,24 @@ export default {
|
|||
</template>
|
||||
</runner-header>
|
||||
|
||||
<runner-details :runner="runner" />
|
||||
<runner-details :runner="runner">
|
||||
<template #jobs-tab>
|
||||
<gl-tab>
|
||||
<template #title>
|
||||
{{ s__('Runners|Jobs') }}
|
||||
<gl-badge
|
||||
v-if="jobCount"
|
||||
data-testid="job-count-badge"
|
||||
class="gl-tab-counter-badge"
|
||||
size="sm"
|
||||
>
|
||||
{{ jobCount }}
|
||||
</gl-badge>
|
||||
</template>
|
||||
|
||||
<runner-jobs v-if="runner" :runner="runner" />
|
||||
</gl-tab>
|
||||
</template>
|
||||
</runner-details>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -1,19 +1,16 @@
|
|||
<script>
|
||||
import { GlBadge, GlTabs, GlTab, GlIntersperse } from '@gitlab/ui';
|
||||
import { GlTabs, GlTab, GlIntersperse } from '@gitlab/ui';
|
||||
import { s__ } from '~/locale';
|
||||
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
|
||||
import { timeIntervalInWords } from '~/lib/utils/datetime_utility';
|
||||
import { ACCESS_LEVEL_REF_PROTECTED, GROUP_TYPE, PROJECT_TYPE } from '../constants';
|
||||
import { formatJobCount } from '../utils';
|
||||
import RunnerDetail from './runner_detail.vue';
|
||||
import RunnerGroups from './runner_groups.vue';
|
||||
import RunnerProjects from './runner_projects.vue';
|
||||
import RunnerJobs from './runner_jobs.vue';
|
||||
import RunnerTags from './runner_tags.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlBadge,
|
||||
GlTabs,
|
||||
GlTab,
|
||||
GlIntersperse,
|
||||
|
@ -22,7 +19,6 @@ export default {
|
|||
import('ee_component/runner/components/runner_maintenance_note_detail.vue'),
|
||||
RunnerGroups,
|
||||
RunnerProjects,
|
||||
RunnerJobs,
|
||||
RunnerTags,
|
||||
TimeAgo,
|
||||
},
|
||||
|
@ -59,9 +55,6 @@ export default {
|
|||
isProjectRunner() {
|
||||
return this.runner?.runnerType === PROJECT_TYPE;
|
||||
},
|
||||
jobCount() {
|
||||
return formatJobCount(this.runner?.jobCount);
|
||||
},
|
||||
},
|
||||
ACCESS_LEVEL_REF_PROTECTED,
|
||||
};
|
||||
|
@ -120,15 +113,6 @@ export default {
|
|||
<runner-projects v-if="isProjectRunner" :runner="runner" />
|
||||
</template>
|
||||
</gl-tab>
|
||||
<gl-tab>
|
||||
<template #title>
|
||||
{{ s__('Runners|Jobs') }}
|
||||
<gl-badge v-if="jobCount" data-testid="job-count-badge" class="gl-ml-1" size="sm">
|
||||
{{ jobCount }}
|
||||
</gl-badge>
|
||||
</template>
|
||||
|
||||
<runner-jobs v-if="runner" :runner="runner" />
|
||||
</gl-tab>
|
||||
<slot name="jobs-tab"></slot>
|
||||
</gl-tabs>
|
||||
</template>
|
||||
|
|
|
@ -175,10 +175,6 @@
|
|||
@include btn-outline($white, $red-500, $red-500, $red-100, $red-700, $red-500, $red-200, $red-600, $red-800);
|
||||
}
|
||||
|
||||
&.btn-warning {
|
||||
@include btn-outline($white, $orange-500, $orange-500, $orange-50, $orange-600, $orange-600, $orange-100, $orange-700, $orange-700);
|
||||
}
|
||||
|
||||
&.btn-primary,
|
||||
&.btn-info {
|
||||
@include btn-outline($white, $blue-500, $blue-500, $blue-100, $blue-700, $blue-500, $blue-200, $blue-600, $blue-800);
|
||||
|
@ -190,10 +186,6 @@
|
|||
@include btn-blue;
|
||||
}
|
||||
|
||||
&.btn-warning {
|
||||
@include btn-orange;
|
||||
}
|
||||
|
||||
&.btn-danger {
|
||||
@include btn-red;
|
||||
}
|
||||
|
|
|
@ -273,6 +273,18 @@
|
|||
@include scrolling-links();
|
||||
}
|
||||
|
||||
.fade-left::after,
|
||||
.fade-right::after {
|
||||
content: '';
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
display: block;
|
||||
width: 16px;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.fade-right {
|
||||
@include fade(left, $gray-light);
|
||||
right: -5px;
|
||||
|
@ -280,6 +292,11 @@
|
|||
svg {
|
||||
right: -7px;
|
||||
}
|
||||
|
||||
&::after {
|
||||
right: 0;
|
||||
background: linear-gradient(270deg, $white, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
.fade-left {
|
||||
|
@ -290,6 +307,11 @@
|
|||
svg {
|
||||
left: -7px;
|
||||
}
|
||||
|
||||
&::after {
|
||||
left: 0;
|
||||
background: linear-gradient(90deg, $white, transparent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -316,7 +338,6 @@
|
|||
|
||||
.fade-right,
|
||||
.fade-left {
|
||||
bottom: $gl-padding;
|
||||
top: auto;
|
||||
}
|
||||
|
||||
|
|
|
@ -428,7 +428,6 @@ $gl-padding-12: 12px;
|
|||
$gl-padding: 16px;
|
||||
$gl-padding-24: 24px;
|
||||
$gl-padding-32: 32px;
|
||||
$gl-padding-50: 50px;
|
||||
$gl-input-padding: 10px;
|
||||
$gl-vert-padding: 6px;
|
||||
$gl-padding-top: 10px;
|
||||
|
|
|
@ -315,7 +315,7 @@ $tabs-holder-z-index: 250;
|
|||
}
|
||||
|
||||
.mr-fast-forward-message {
|
||||
padding-left: $gl-padding-50;
|
||||
padding-left: $gl-spacing-scale-9;
|
||||
padding-bottom: $gl-padding;
|
||||
}
|
||||
|
||||
|
|
|
@ -40,8 +40,8 @@
|
|||
}
|
||||
|
||||
.save-group-loader {
|
||||
margin-top: $gl-padding-50;
|
||||
margin-bottom: $gl-padding-50;
|
||||
margin-top: $gl-spacing-scale-9;
|
||||
margin-bottom: $gl-spacing-scale-9;
|
||||
color: $gray-700;
|
||||
}
|
||||
|
||||
|
|
|
@ -937,3 +937,58 @@
|
|||
margin-right: -7px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.issuable-discussion.incident-timeline-events {
|
||||
.main-notes-list::before {
|
||||
content: none;
|
||||
}
|
||||
|
||||
.timeline-event-note {
|
||||
p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* We have a very specific design proposal where we cannot
|
||||
* use `vertical-line` mixin as it is and have to use
|
||||
* custom styles, see https://gitlab.com/gitlab-org/gitlab/-/merge_requests/81284#note_904867444
|
||||
*/
|
||||
.timeline-entry-vertical-line {
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
border-left: 2px solid $gray-50;
|
||||
position: absolute;
|
||||
left: 39px;
|
||||
height: 80%;
|
||||
}
|
||||
|
||||
&:first-child::before,
|
||||
&:last-child::after {
|
||||
content: none;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
&::after {
|
||||
top: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
&::before {
|
||||
bottom: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(:first-child):not(:last-child) {
|
||||
&::before {
|
||||
top: -10%;
|
||||
}
|
||||
|
||||
&::after {
|
||||
bottom: -10%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
*
|
||||
*/
|
||||
$tabs-holder-z-index: 250;
|
||||
$comparison-empty-state-height: 62px;
|
||||
|
||||
.space-children {
|
||||
@include clearfix;
|
||||
|
@ -70,6 +71,10 @@ $tabs-holder-z-index: 250;
|
|||
}
|
||||
}
|
||||
|
||||
.compare-commit-empty {
|
||||
min-height: $comparison-empty-state-height;
|
||||
}
|
||||
|
||||
.commits-empty {
|
||||
text-align: center;
|
||||
|
||||
|
|
|
@ -209,7 +209,6 @@ body.gl-dark {
|
|||
&.btn-info,
|
||||
&.btn-success,
|
||||
&.btn-danger,
|
||||
&.btn-warning,
|
||||
&.btn-confirm {
|
||||
&-tertiary {
|
||||
mix-blend-mode: screen;
|
||||
|
|
|
@ -28,6 +28,7 @@ class Key < ApplicationRecord
|
|||
|
||||
validate :key_meets_restrictions
|
||||
validate :expiration, on: :create
|
||||
validate :banned_key, if: :should_check_for_banned_key?
|
||||
|
||||
delegate :name, :email, to: :user, prefix: true
|
||||
|
||||
|
@ -142,6 +143,27 @@ class Key < ApplicationRecord
|
|||
end
|
||||
end
|
||||
|
||||
def should_check_for_banned_key?
|
||||
return false unless user
|
||||
|
||||
key_changed? && Feature.enabled?(:ssh_banned_key, user)
|
||||
end
|
||||
|
||||
def banned_key
|
||||
return unless public_key.banned?
|
||||
|
||||
help_page_url = Rails.application.routes.url_helpers.help_page_url(
|
||||
'security/ssh_keys_restrictions',
|
||||
anchor: 'block-banned-or-compromised-keys'
|
||||
)
|
||||
|
||||
errors.add(
|
||||
:key,
|
||||
_('cannot be used because it belongs to a compromised private key. Stop using this key and generate a new one.'),
|
||||
help_page_url: help_page_url
|
||||
)
|
||||
end
|
||||
|
||||
def forbidden_key_type_message
|
||||
allowed_types = Gitlab::CurrentSettings.allowed_key_types.map(&:upcase)
|
||||
|
||||
|
|
|
@ -2585,16 +2585,7 @@ class Project < ApplicationRecord
|
|||
end
|
||||
|
||||
def access_request_approvers_to_be_notified
|
||||
# For a personal project:
|
||||
# The creator is added as a member with `Owner` access level, starting from GitLab 14.8
|
||||
# The creator was added as a member with `Maintainer` access level, before GitLab 14.8
|
||||
# So, to make sure access requests for all personal projects work as expected,
|
||||
# we need to filter members with the scope `owners_and_maintainers`.
|
||||
access_request_approvers = if personal?
|
||||
members.owners_and_maintainers
|
||||
else
|
||||
members.maintainers
|
||||
end
|
||||
access_request_approvers = members.owners_and_maintainers
|
||||
|
||||
access_request_approvers.connected_to_user.order_recent_sign_in.limit(Member::ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT)
|
||||
end
|
||||
|
|
|
@ -6,6 +6,10 @@ class MetricImageUploader < GitlabUploader # rubocop:disable Gitlab/NamespacedCl
|
|||
prepend ObjectStorage::Extension::RecordsUploads
|
||||
include UploaderHelper
|
||||
|
||||
def self.workhorse_local_upload_path
|
||||
File.join(options.storage_path, 'uploads', TMP_UPLOAD_PATH)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def dynamic_segment
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
= gitlab_ui_form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-account-settings'), html: { class: 'fieldset-form', id: 'account-settings' } do |f|
|
||||
= form_errors(@application_setting)
|
||||
= form_errors(@application_setting, pajamas_alert: true)
|
||||
|
||||
%fieldset
|
||||
.form-group
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
.settings-content
|
||||
= gitlab_ui_form_for @application_setting, url: ci_cd_admin_application_settings_path(anchor: 'js-ci-cd-settings'), html: { class: 'fieldset-form' } do |f|
|
||||
= form_errors(@application_setting)
|
||||
= form_errors(@application_setting, pajamas_alert: true )
|
||||
|
||||
%fieldset
|
||||
.form-group
|
||||
|
@ -72,7 +72,7 @@
|
|||
- @plans.each_with_index do |plan, index|
|
||||
.tab-pane{ :id => "plan#{index}", class: index == 0 ? 'active': '' }
|
||||
= form_for plan.actual_limits, url: admin_plan_limits_path(anchor: 'js-ci-cd-settings'), html: { class: 'fieldset-form' }, method: :post do |f|
|
||||
= form_errors(plan)
|
||||
= form_errors(plan, pajamas_alert: true)
|
||||
%fieldset
|
||||
= f.hidden_field(:plan_id, value: plan.id)
|
||||
.form-group
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
|
||||
.settings-content
|
||||
= gitlab_ui_form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-floc-settings'), html: { class: 'fieldset-form', id: 'floc-settings' } do |f|
|
||||
= form_errors(@application_setting)
|
||||
= form_errors(@application_setting, pajamas_alert: true)
|
||||
|
||||
%fieldset
|
||||
.form-group
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
= link_to _('Learn more.'), help_page_path('administration/integration/kroki.md'), target: '_blank', rel: 'noopener noreferrer'
|
||||
.settings-content
|
||||
= gitlab_ui_form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-kroki-settings'), html: { class: 'fieldset-form', id: 'kroki-settings' } do |f|
|
||||
= form_errors(@application_setting) if expanded
|
||||
= form_errors(@application_setting, pajamas_alert: true) if expanded
|
||||
|
||||
%fieldset
|
||||
.form-group
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
- @plans.each_with_index do |plan, index|
|
||||
.tab-pane{ :id => "plan#{index}", class: index == 0 ? 'active': '' }
|
||||
= form_for plan.actual_limits, url: admin_plan_limits_path(anchor: 'js-package-settings'), html: { class: 'fieldset-form' }, method: :post do |f|
|
||||
= form_errors(plan)
|
||||
= form_errors(plan, pajamas_alert: true)
|
||||
%fieldset
|
||||
= f.hidden_field(:plan_id, value: plan.id)
|
||||
.form-group
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
= gitlab_ui_form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-signin-settings'), html: { class: 'fieldset-form', id: 'signin-settings' } do |f|
|
||||
= form_errors(@application_setting)
|
||||
= form_errors(@application_setting, pajamas_alert: true)
|
||||
|
||||
%fieldset
|
||||
.form-group
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
|
||||
.settings-content
|
||||
= gitlab_ui_form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-sourcegraph-settings'), html: { class: 'fieldset-form', id: 'sourcegraph-settings' } do |f|
|
||||
= form_errors(@application_setting)
|
||||
= form_errors(@application_setting, pajamas_alert: true)
|
||||
|
||||
%fieldset
|
||||
.form-group
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
= gitlab_ui_form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-terms-settings'), html: { class: 'fieldset-form', id: 'terms-settings' } do |f|
|
||||
= form_errors(@application_setting)
|
||||
= form_errors(@application_setting, pajamas_alert: true)
|
||||
|
||||
%fieldset
|
||||
.form-group
|
||||
|
|
|
@ -8,12 +8,12 @@
|
|||
%h3= name
|
||||
|
||||
- if @service_ping_data_present
|
||||
%button.gl-button.btn.btn-default.js-payload-preview-trigger{ type: 'button', data: { payload_selector: ".#{payload_class}" } }
|
||||
= gl_loading_icon(css_class: 'js-spinner gl-display-none gl-mr-2')
|
||||
.js-text.gl-display-inline= _('Preview payload')
|
||||
%button.gl-button.btn.btn-default.js-payload-download-trigger{ type: 'button', data: { endpoint: usage_data_admin_application_settings_path(format: :json) } }
|
||||
= gl_loading_icon(css_class: 'js-spinner gl-display-none gl-mr-2')
|
||||
.js-text.d-inline= _('Download payload')
|
||||
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-payload-preview-trigger gl-mr-2', data: { payload_selector: ".#{payload_class}" } } ) do
|
||||
= gl_loading_icon(css_class: 'js-spinner gl-display-none', inline: true)
|
||||
%span.js-text.gl-display-inline= _('Preview payload')
|
||||
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-payload-download-trigger gl-mr-2', data: { endpoint: usage_data_admin_application_settings_path(format: :json) } } ) do
|
||||
= gl_loading_icon(css_class: 'js-spinner gl-display-none', inline: true)
|
||||
%span.js-text.gl-display-inline= _('Download payload')
|
||||
%pre.js-syntax-highlight.code.highlight.gl-mt-2.gl-display-none{ class: payload_class, data: { endpoint: usage_data_admin_application_settings_path(format: :html) } }
|
||||
- else
|
||||
= render Pajamas::AlertComponent.new(variant: :warning,
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
- billable_users_url = help_page_path('subscriptions/self_managed/index', anchor: 'billable-users')
|
||||
- billable_users_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer nofollow">'.html_safe % { url: billable_users_url }
|
||||
|
||||
= render_if_exists 'shared/manual_renewal_banner'
|
||||
= render_if_exists 'shared/manual_quarterly_reconciliation_banner'
|
||||
= render_if_exists 'shared/submit_license_usage_data_banner'
|
||||
= render_if_exists 'shared/qrtly_reconciliation_alert'
|
||||
|
|
|
@ -4,14 +4,17 @@
|
|||
- breadcrumb_title _("Jobs")
|
||||
- page_title _("Jobs")
|
||||
|
||||
.top-area.scrolling-tabs-container.inner-page-scroll-tabs
|
||||
- build_path_proc = ->(scope) { admin_jobs_path(scope: scope) }
|
||||
= render "shared/builds/tabs", build_path_proc: build_path_proc, all_builds: @all_builds, scope: @scope
|
||||
.top-area
|
||||
.scrolling-tabs-container.inner-page-scroll-tabs.gl-flex-grow-1.gl-min-w-0.gl-w-full
|
||||
.fade-left= sprite_icon('chevron-lg-left', size: 12)
|
||||
.fade-right= sprite_icon('chevron-lg-right', size: 12)
|
||||
- build_path_proc = ->(scope) { admin_jobs_path(scope: scope) }
|
||||
= render "shared/builds/tabs", build_path_proc: build_path_proc, all_builds: @all_builds, scope: @scope
|
||||
|
||||
- if @all_builds.running_or_pending.any?
|
||||
#js-stop-jobs-modal
|
||||
.nav-controls
|
||||
%button#js-stop-jobs-button.btn.gl-button.btn-danger{ data: { url: cancel_all_admin_jobs_path } }
|
||||
= render Pajamas::ButtonComponent.new(variant: :danger, button_options: { id: 'js-stop-jobs-button', data: { url: cancel_all_admin_jobs_path } }) do
|
||||
= s_('AdminArea|Stop all jobs')
|
||||
|
||||
.row-content-block.second-block
|
||||
|
|
|
@ -1,14 +1,18 @@
|
|||
- page_title _('Projects')
|
||||
- params[:visibility_level] ||= []
|
||||
|
||||
.top-area.scrolling-tabs-container.inner-page-scroll-tabs
|
||||
= gl_tabs_nav({ class: 'gl-border-b-0 gl-overflow-x-auto gl-flex-grow-1 gl-flex-nowrap gl-webkit-scrollbar-display-none' }) do
|
||||
= gl_tab_link_to _('All'), admin_projects_path(visibility_level: nil), { item_active: params[:visibility_level].empty? }
|
||||
= gl_tab_link_to _('Private'), admin_projects_path(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
|
||||
= gl_tab_link_to _('Internal'), admin_projects_path(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
|
||||
= gl_tab_link_to _('Public'), admin_projects_path(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
|
||||
.top-area
|
||||
.scrolling-tabs-container.inner-page-scroll-tabs.gl-flex-grow-1.gl-min-w-0.gl-w-full
|
||||
.fade-left= sprite_icon('chevron-lg-left', size: 12)
|
||||
.fade-right= sprite_icon('chevron-lg-right', size: 12)
|
||||
= gl_tabs_nav({ class: 'scrolling-tabs nav-links gl-display-flex gl-flex-grow-1 gl-w-full nav gl-tabs-nav nav gl-tabs-nav' }) do
|
||||
= gl_tab_link_to _('All'), admin_projects_path(visibility_level: nil), { item_active: params[:visibility_level].empty? }
|
||||
= gl_tab_link_to _('Private'), admin_projects_path(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
|
||||
= gl_tab_link_to _('Internal'), admin_projects_path(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
|
||||
= gl_tab_link_to _('Public'), admin_projects_path(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
|
||||
|
||||
.nav-controls
|
||||
|
||||
.nav-controls.gl-pl-2
|
||||
.search-holder
|
||||
= render 'shared/projects/search_form', autofocus: true, admin_view: true
|
||||
- current_namespace = _('Namespace')
|
||||
|
|
|
@ -11,10 +11,11 @@
|
|||
.page-title-controls
|
||||
= link_to _("New project"), new_project_path, class: "gl-button btn btn-confirm", data: { qa_selector: 'new_project_button' }
|
||||
|
||||
.top-area.scrolling-tabs-container.inner-page-scroll-tabs
|
||||
.fade-left= sprite_icon('chevron-lg-left', size: 12)
|
||||
.fade-right= sprite_icon('chevron-lg-right', size: 12)
|
||||
= render 'dashboard/projects_nav'
|
||||
.top-area
|
||||
.scrolling-tabs-container.inner-page-scroll-tabs.gl-flex-grow-1.gl-min-w-0.gl-w-full
|
||||
.fade-left= sprite_icon('chevron-lg-left', size: 12)
|
||||
.fade-right= sprite_icon('chevron-lg-right', size: 12)
|
||||
= render 'dashboard/projects_nav'
|
||||
- unless feature_project_list_filter_bar
|
||||
.nav-controls
|
||||
= render 'shared/projects/search_form'
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
- is_your_projects_path = current_page?(dashboard_projects_path) || current_page?(root_path)
|
||||
- is_explore_projects_path = current_page?(explore_root_path) || current_page?(trending_explore_projects_path) || current_page?(starred_explore_projects_path) || current_page?(explore_projects_path)
|
||||
|
||||
= gl_tabs_nav({ class: 'scrolling-tabs nav-links gl-display-flex gl-flex-grow-1 gl-flex-nowrap gl-border-0' }) do
|
||||
= gl_tabs_nav({ class: 'scrolling-tabs nav-links gl-display-flex gl-flex-grow-1 gl-w-full nav gl-tabs-nav' }) do
|
||||
= gl_tab_link_to dashboard_projects_path, { item_active: is_your_projects_path, class: 'shortcuts-activity', data: { placement: 'right' } } do
|
||||
= _("Your projects")
|
||||
= gl_tab_counter_badge(limited_counter_with_delimiter(@total_user_projects_count))
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
.page-title-holder.d-flex.align-items-center
|
||||
%h1.page-title.gl-font-size-h-display= _('Projects')
|
||||
|
||||
.top-area.scrolling-tabs-container.inner-page-scroll-tabs
|
||||
.fade-left= sprite_icon('chevron-lg-left', size: 12)
|
||||
.fade-right= sprite_icon('chevron-lg-right', size: 12)
|
||||
= render 'dashboard/projects_nav'
|
||||
.top-area
|
||||
.scrolling-tabs-container.inner-page-scroll-tabs.gl-flex-grow-1.gl-min-w-0.gl-w-full
|
||||
.fade-left= sprite_icon('chevron-lg-left', size: 12)
|
||||
.fade-right= sprite_icon('chevron-lg-right', size: 12)
|
||||
= render 'dashboard/projects_nav'
|
||||
.nav-controls
|
||||
= render 'shared/topics/search_form'
|
||||
|
|
|
@ -11,4 +11,4 @@
|
|||
%li.page-item.js-next-button{ class: ('disabled' if current_page.last?) }
|
||||
= link_to page_url, rel: 'next', remote: remote, class: 'page-link' do
|
||||
= s_('Pagination|Next')
|
||||
= sprite_icon('angle-right', size: 8)
|
||||
= sprite_icon('chevron-lg-right', size: 8)
|
||||
|
|
|
@ -10,5 +10,5 @@
|
|||
|
||||
%li.page-item.js-previous-button{ class: ('disabled' if current_page.first?) }
|
||||
= link_to page_url, rel: 'prev', remote: remote, class: 'page-link' do
|
||||
= sprite_icon('angle-left', size: 8)
|
||||
= sprite_icon('chevron-lg-left', size: 8)
|
||||
= s_('Pagination|Prev')
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
%li.commit-header.js-commit-header
|
||||
%span.font-weight-bold= n_("%d previously merged commit", "%d previously merged commits", context_commits.count) % context_commits.count
|
||||
- if can_update_merge_request
|
||||
%button.gl-button.btn.btn-default.ml-3.add-review-item-modal-trigger{ type: "button", data: { context_commits_empty: 'false' } }
|
||||
= render Pajamas::ButtonComponent.new(button_options: { class: 'gl-ml-3 add-review-item-modal-trigger', data: { context_commits_empty: 'false' } }) do
|
||||
= _('Add/remove')
|
||||
|
||||
%li.commits-row
|
||||
|
@ -41,7 +41,7 @@
|
|||
= n_('%s additional commit has been omitted to prevent performance issues.', '%s additional commits have been omitted to prevent performance issues.', hidden) % number_with_delimiter(hidden)
|
||||
|
||||
- if can_update_merge_request && context_commits&.empty?
|
||||
%button.gl-button.btn.btn-default.mt-3.add-review-item-modal-trigger{ type: "button", data: { context_commits_empty: 'true' } }
|
||||
= render Pajamas::ButtonComponent.new(button_options: { class: 'gl-mt-5', data: { context_commits_empty: 'true' } }) do
|
||||
= _('Add previously merged commits')
|
||||
|
||||
- if commits.size == 0 && context_commits.nil?
|
||||
|
|
|
@ -12,24 +12,27 @@
|
|||
.clearfix
|
||||
.merge-request-select.dropdown
|
||||
= f.hidden_field :source_project_id
|
||||
= dropdown_toggle @merge_request.source_project_path, { toggle: "dropdown", 'field-name': "#{f.object_name}[source_project_id]", disabled: @merge_request.persisted? }, { toggle_class: "js-compare-dropdown js-source-project" }
|
||||
= dropdown_toggle @merge_request.source_project_path, { toggle: "dropdown", 'field-name': "#{f.object_name}[source_project_id]", disabled: @merge_request.persisted?, default_text: _("Select source project") }, { toggle_class: "js-compare-dropdown js-source-project" }
|
||||
.dropdown-menu.dropdown-menu-selectable.dropdown-source-project
|
||||
= dropdown_title("Select source project")
|
||||
= dropdown_filter("Search projects")
|
||||
= dropdown_title(_("Select source project"))
|
||||
= dropdown_filter(_("Search projects"))
|
||||
= dropdown_content do
|
||||
= render 'projects/merge_requests/dropdowns/project',
|
||||
projects: [@merge_request.source_project],
|
||||
selected: f.object.source_project_id
|
||||
.merge-request-select.dropdown
|
||||
= f.hidden_field :source_branch
|
||||
= dropdown_toggle f.object.source_branch.presence || _("Select source branch"), { toggle: "dropdown", 'field-name': "#{f.object_name}[source_branch]", 'refs-url': refs_project_path(@source_project), selected: f.object.source_branch, qa_selector: "source_branch_dropdown" }, { toggle_class: "js-compare-dropdown js-source-branch monospace" }
|
||||
= dropdown_toggle f.object.source_branch.presence || _("Select source branch"), { toggle: "dropdown", 'field-name': "#{f.object_name}[source_branch]", 'refs-url': refs_project_path(@source_project), selected: f.object.source_branch, default_text: _("Select target branch"), qa_selector: "source_branch_dropdown" }, { toggle_class: "js-compare-dropdown js-source-branch monospace" }
|
||||
.dropdown-menu.dropdown-menu-selectable.js-source-branch-dropdown.git-revision-dropdown
|
||||
= dropdown_title(_("Select source branch"))
|
||||
= dropdown_filter(_("Search branches"))
|
||||
= dropdown_content
|
||||
= dropdown_loading
|
||||
.gl-bg-gray-50.gl-rounded-base.gl-mx-2.gl-my-4
|
||||
= gl_loading_icon(css_class: 'js-source-loading gl-my-3')
|
||||
.compare-commit-empty.js-source-commit-empty.gl-display-flex.gl-align-items-center.gl-p-5{ style: 'display: none;' }
|
||||
= sprite_icon('branch', size: 16, css_class: 'gl-mr-3')
|
||||
= _('Select a branch to compare')
|
||||
= gl_loading_icon(css_class: 'js-source-loading gl-py-3')
|
||||
%ul.list-unstyled.mr_source_commit
|
||||
|
||||
.col-lg-6
|
||||
|
@ -40,24 +43,27 @@
|
|||
- projects = target_projects(@project)
|
||||
.merge-request-select.dropdown
|
||||
= f.hidden_field :target_project_id
|
||||
= dropdown_toggle f.object.target_project.full_path, { toggle: "dropdown", 'field-name': "#{f.object_name}[target_project_id]", disabled: @merge_request.persisted? }, { toggle_class: "js-compare-dropdown js-target-project" }
|
||||
= dropdown_toggle f.object.target_project.full_path, { toggle: "dropdown", 'field-name': "#{f.object_name}[target_project_id]", disabled: @merge_request.persisted?, default_text: _("Select target project") }, { toggle_class: "js-compare-dropdown js-target-project" }
|
||||
.dropdown-menu.dropdown-menu-selectable.dropdown-target-project
|
||||
= dropdown_title("Select target project")
|
||||
= dropdown_filter("Search projects")
|
||||
= dropdown_title(_("Select target project"))
|
||||
= dropdown_filter(_("Search projects"))
|
||||
= dropdown_content do
|
||||
= render 'projects/merge_requests/dropdowns/project',
|
||||
projects: projects,
|
||||
selected: f.object.target_project_id
|
||||
.merge-request-select.dropdown
|
||||
= f.hidden_field :target_branch
|
||||
= dropdown_toggle f.object.target_branch.presence || _("Select target branch"), { toggle: "dropdown", 'field-name': "#{f.object_name}[target_branch]", 'refs-url': refs_project_path(f.object.target_project), selected: f.object.target_branch }, { toggle_class: "js-compare-dropdown js-target-branch monospace" }
|
||||
= dropdown_toggle f.object.target_branch.presence || _("Select target branch"), { toggle: "dropdown", 'field-name': "#{f.object_name}[target_branch]", 'refs-url': refs_project_path(f.object.target_project), selected: f.object.target_branch, default_text: _("Select target branch") }, { toggle_class: "js-compare-dropdown js-target-branch monospace" }
|
||||
.dropdown-menu.dropdown-menu-selectable.js-target-branch-dropdown.git-revision-dropdown
|
||||
= dropdown_title(_("Select target branch"))
|
||||
= dropdown_filter(_("Search branches"))
|
||||
= dropdown_content
|
||||
= dropdown_loading
|
||||
.gl-bg-gray-50.gl-rounded-base.gl-mx-2.gl-my-4
|
||||
= gl_loading_icon(css_class: 'js-target-loading gl-my-3')
|
||||
.compare-commit-empty.js-target-commit-empty.gl-display-flex.gl-align-items-center.gl-p-5{ style: 'display: none;' }
|
||||
= sprite_icon('branch', size: 16, css_class: 'gl-mr-3')
|
||||
= _('Select a branch to compare')
|
||||
= gl_loading_icon(css_class: 'js-target-loading gl-py-3')
|
||||
%ul.list-unstyled.mr_target_commit
|
||||
|
||||
- if @merge_request.errors.any?
|
||||
|
|
|
@ -14,8 +14,9 @@
|
|||
dom_id: dom_id(label), type: label.type } }
|
||||
%button.add-priority.btn.gl-button.btn-default-tertiary.btn-sm.has-tooltip{ title: _('Prioritize'), type: 'button', data: { placement: 'bottom' }, aria_label: _('Prioritize label') }
|
||||
= sprite_icon('star-o')
|
||||
%button.remove-priority.btn.gl-button.btn-default-tertiary.btn-sm.has-tooltip{ title: _('Remove priority'), type: 'button', data: { placement: 'bottom' }, aria_label: _('Deprioritize label') }
|
||||
= sprite_icon('star')
|
||||
= render Pajamas::ButtonComponent.new(category: :tertiary,
|
||||
icon: 'star',
|
||||
button_options: { class: 'remove-priority has-tooltip', 'title': _('Remove priority'), 'aria_label': _('Deprioritize label'), data: { placement: 'bottom' } })
|
||||
- if can?(current_user, :admin_label, label)
|
||||
%li.gl-display-inline-block
|
||||
= link_to label.edit_path, class: 'btn gl-button btn-default-tertiary btn-sm edit has-tooltip', title: _('Edit'), data: { placement: 'bottom' }, aria_label: _('Edit') do
|
||||
|
@ -23,8 +24,9 @@
|
|||
- if can?(current_user, :admin_label, label)
|
||||
%li.gl-display-inline-block
|
||||
.dropdown
|
||||
%button{ type: 'button', class: 'btn gl-button btn-default-tertiary btn-sm js-label-options-dropdown', data: { toggle: 'dropdown' }, aria_label: _('Label actions dropdown') }
|
||||
= sprite_icon('ellipsis_v')
|
||||
= render Pajamas::ButtonComponent.new(category: :tertiary,
|
||||
icon: 'ellipsis_v',
|
||||
button_options: { class: 'js-label-options-dropdown', 'aria_label': _('Label actions dropdown'), data: { toggle: 'dropdown' } })
|
||||
.dropdown-menu.dropdown-open-left
|
||||
%ul
|
||||
- if label.project_label? && label.project.group && can?(current_user, :admin_label, label.project.group)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
- count_badge_classes = 'gl-display-none gl-sm-display-inline-flex'
|
||||
|
||||
= gl_tabs_nav( {class: 'gl-border-b-0 gl-flex-grow-1', data: { testid: 'jobs-tabs' } } ) do
|
||||
= gl_tabs_nav( {class: 'scrolling-tabs nav-links gl-display-flex gl-flex-grow-1 gl-w-full nav gl-border-b-0', data: { testid: 'jobs-tabs' } } ) do
|
||||
= gl_tab_link_to build_path_proc.call(nil), { item_active: scope.nil? } do
|
||||
= _('All')
|
||||
= gl_tab_counter_badge(limited_counter_with_delimiter(all_builds), { class: count_badge_classes })
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
.issue-details.issuable-details.js-issue-details
|
||||
.detail-page-description.content-block.js-detail-page-description
|
||||
#js-issuable-app{ data: { initial: issuable_initial_data(issuable).to_json, full_path: @project.full_path } }
|
||||
#js-issuable-app{ data: { initial: issuable_initial_data(issuable).to_json, issuable_id: issuable.id, full_path: @project.full_path } }
|
||||
.title-container
|
||||
%h1.title.page-title.gl-font-size-h-display= markdown_field(issuable, :title)
|
||||
- if issuable.description.present?
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: ssh_banned_key
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/87541
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/363410
|
||||
milestone: '15.1'
|
||||
type: development
|
||||
group: 'group::source code'
|
||||
default_enabled: false
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: use_primary_and_secondary_stores_for_duplicate_jobs
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/
|
||||
rollout_issue_url:
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/85740
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/364381
|
||||
milestone:
|
||||
type: development
|
||||
group: group::scalability
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: use_primary_and_secondary_stores_for_sidekiq_status
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/
|
||||
rollout_issue_url:
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/89561
|
||||
rollout_issue_url: https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/924
|
||||
milestone:
|
||||
type: development
|
||||
group: group::scalability
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: use_primary_store_as_default_for_duplicate_jobs
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/
|
||||
rollout_issue_url:
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/85740
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/364381
|
||||
milestone:
|
||||
type: development
|
||||
group: group::scalability
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: use_primary_store_as_default_for_sidekiq_status
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/
|
||||
rollout_issue_url:
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/89561
|
||||
rollout_issue_url: https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/924
|
||||
milestone:
|
||||
type: development
|
||||
group: group::scalability
|
||||
|
|
|
@ -54,6 +54,13 @@ module Mail
|
|||
decoded = dec.decode(raw_source)
|
||||
|
||||
if defined?(Encoding) && charset && charset != "US-ASCII"
|
||||
# Sometimes, the decoded string is frozen. Encoders in
|
||||
# Mail::Encodings behave differently in this case. Unlike the
|
||||
# original implementation which does not modify this string, we
|
||||
# enforce the encoding below. That may lead to FrozenError.
|
||||
# Issue: https://gitlab.com/gitlab-org/gitlab/-/issues/364619
|
||||
decoded = decoded.dup if decoded.frozen?
|
||||
|
||||
# PATCH
|
||||
# We need to force the encoding: in the case of quoted-printable
|
||||
# this will throw an exception otherwise, because `decoded` will have
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
table_name: audit_events_streaming_headers
|
||||
classes:
|
||||
- AuditEvents::Streaming::Header
|
||||
feature_categories:
|
||||
- audit_events
|
||||
description: Represents a HTTP header sent with streaming audit events
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/88063
|
||||
milestone: '15.1'
|
|
@ -4,6 +4,6 @@ classes:
|
|||
- Ci::JobVariable
|
||||
feature_categories:
|
||||
- pipeline_authoring
|
||||
description: TODO
|
||||
description: CI/CD variables set to a job when running it manually.
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/14784
|
||||
milestone: '12.2'
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class CreateAuditEventsStreamingHeaders < Gitlab::Database::Migration[2.0]
|
||||
INDEX_NAME = 'idx_streaming_headers_on_external_audit_event_destination_id'
|
||||
UNIQ_INDEX_NAME = 'idx_external_audit_event_destination_id_key_uniq'
|
||||
|
||||
def change
|
||||
create_table :audit_events_streaming_headers do |t|
|
||||
t.timestamps_with_timezone null: false
|
||||
t.references :external_audit_event_destination,
|
||||
null: false,
|
||||
index: { name: INDEX_NAME },
|
||||
foreign_key: { to_table: 'audit_events_external_audit_event_destinations', on_delete: :cascade }
|
||||
t.text :key, null: false, limit: 255
|
||||
t.text :value, null: false, limit: 255
|
||||
|
||||
t.index [:key, :external_audit_event_destination_id], unique: true, name: UNIQ_INDEX_NAME
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1 @@
|
|||
9dddbbdb3e72763cc331b5690536312970c92c64d66d7cb2efc118c107ae204c
|
|
@ -11655,6 +11655,26 @@ CREATE SEQUENCE audit_events_id_seq
|
|||
|
||||
ALTER SEQUENCE audit_events_id_seq OWNED BY audit_events.id;
|
||||
|
||||
CREATE TABLE audit_events_streaming_headers (
|
||||
id bigint NOT NULL,
|
||||
created_at timestamp with time zone NOT NULL,
|
||||
updated_at timestamp with time zone NOT NULL,
|
||||
external_audit_event_destination_id bigint NOT NULL,
|
||||
key text NOT NULL,
|
||||
value text NOT NULL,
|
||||
CONSTRAINT check_53c3152034 CHECK ((char_length(key) <= 255)),
|
||||
CONSTRAINT check_ac213cca22 CHECK ((char_length(value) <= 255))
|
||||
);
|
||||
|
||||
CREATE SEQUENCE audit_events_streaming_headers_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1;
|
||||
|
||||
ALTER SEQUENCE audit_events_streaming_headers_id_seq OWNED BY audit_events_streaming_headers.id;
|
||||
|
||||
CREATE TABLE authentication_events (
|
||||
id bigint NOT NULL,
|
||||
created_at timestamp with time zone NOT NULL,
|
||||
|
@ -22545,6 +22565,8 @@ ALTER TABLE ONLY audit_events ALTER COLUMN id SET DEFAULT nextval('audit_events_
|
|||
|
||||
ALTER TABLE ONLY audit_events_external_audit_event_destinations ALTER COLUMN id SET DEFAULT nextval('audit_events_external_audit_event_destinations_id_seq'::regclass);
|
||||
|
||||
ALTER TABLE ONLY audit_events_streaming_headers ALTER COLUMN id SET DEFAULT nextval('audit_events_streaming_headers_id_seq'::regclass);
|
||||
|
||||
ALTER TABLE ONLY authentication_events ALTER COLUMN id SET DEFAULT nextval('authentication_events_id_seq'::regclass);
|
||||
|
||||
ALTER TABLE ONLY award_emoji ALTER COLUMN id SET DEFAULT nextval('award_emoji_id_seq'::regclass);
|
||||
|
@ -24166,6 +24188,9 @@ ALTER TABLE ONLY audit_events_external_audit_event_destinations
|
|||
ALTER TABLE ONLY audit_events
|
||||
ADD CONSTRAINT audit_events_pkey PRIMARY KEY (id, created_at);
|
||||
|
||||
ALTER TABLE ONLY audit_events_streaming_headers
|
||||
ADD CONSTRAINT audit_events_streaming_headers_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY authentication_events
|
||||
ADD CONSTRAINT authentication_events_pkey PRIMARY KEY (id);
|
||||
|
||||
|
@ -26782,6 +26807,8 @@ CREATE INDEX idx_elastic_reindexing_slices_on_elastic_reindexing_subtask_id ON e
|
|||
|
||||
CREATE UNIQUE INDEX idx_environment_merge_requests_unique_index ON deployment_merge_requests USING btree (environment_id, merge_request_id);
|
||||
|
||||
CREATE UNIQUE INDEX idx_external_audit_event_destination_id_key_uniq ON audit_events_streaming_headers USING btree (key, external_audit_event_destination_id);
|
||||
|
||||
CREATE INDEX idx_geo_con_rep_updated_events_on_container_repository_id ON geo_container_repository_updated_events USING btree (container_repository_id);
|
||||
|
||||
CREATE INDEX idx_installable_helm_pkgs_on_project_id_id ON packages_packages USING btree (project_id, id);
|
||||
|
@ -26892,6 +26919,8 @@ CREATE INDEX idx_security_scans_on_scan_type ON security_scans USING btree (scan
|
|||
|
||||
CREATE UNIQUE INDEX idx_serverless_domain_cluster_on_clusters_applications_knative ON serverless_domain_cluster USING btree (clusters_applications_knative_id);
|
||||
|
||||
CREATE INDEX idx_streaming_headers_on_external_audit_event_destination_id ON audit_events_streaming_headers USING btree (external_audit_event_destination_id);
|
||||
|
||||
CREATE INDEX idx_user_details_on_provisioned_by_group_id_user_id ON user_details USING btree (provisioned_by_group_id, user_id);
|
||||
|
||||
CREATE UNIQUE INDEX idx_vuln_signatures_on_occurrences_id_and_signature_sha ON vulnerability_finding_signatures USING btree (finding_id, signature_sha);
|
||||
|
@ -32289,6 +32318,9 @@ ALTER TABLE ONLY vulnerability_exports
|
|||
ALTER TABLE ONLY prometheus_alert_events
|
||||
ADD CONSTRAINT fk_rails_106f901176 FOREIGN KEY (prometheus_alert_id) REFERENCES prometheus_alerts(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY audit_events_streaming_headers
|
||||
ADD CONSTRAINT fk_rails_109fcf96e2 FOREIGN KEY (external_audit_event_destination_id) REFERENCES audit_events_external_audit_event_destinations(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY ci_sources_projects
|
||||
ADD CONSTRAINT fk_rails_10a1eb379a FOREIGN KEY (pipeline_id) REFERENCES ci_pipelines(id) ON DELETE CASCADE;
|
||||
|
||||
|
|
|
@ -143,6 +143,31 @@ Destination is deleted if:
|
|||
- The returned `errors` object is empty.
|
||||
- The API responds with `200 OK`.
|
||||
|
||||
## Custom HTTP header values
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/361216) in GitLab 15.1 [with a flag](feature_flags.md) named `streaming_audit_event_headers`. Disabled by default.
|
||||
|
||||
FLAG:
|
||||
On self-managed GitLab, by default this feature is not available. To make it available per group, ask an administrator to [enable the feature flag](../administration/feature_flags.md) named `streaming_audit_event_headers`.
|
||||
On GitLab.com, this feature is not available.
|
||||
The feature is not ready for production use.
|
||||
|
||||
Each streaming destination can have up to 20 custom HTTP headers included with each streamed event.
|
||||
|
||||
### Add with the API
|
||||
|
||||
Group owners can add a HTTP header using the GraphQL `auditEventsStreamingHeadersCreate` mutation.
|
||||
|
||||
```graphql
|
||||
mutation {
|
||||
auditEventsStreamingHeadersCreate(input: { destinationId: "gid://gitlab/AuditEvents::ExternalAuditEventDestination/24601", key: "foo", value: "bar" }) {
|
||||
errors
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The header is created if the returned `errors` object is empty.
|
||||
|
||||
## Verify event authenticity
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/345424) in GitLab 14.8.
|
||||
|
|
|
@ -717,6 +717,27 @@ Input type: `ApiFuzzingCiConfigurationCreateInput`
|
|||
| <a id="mutationapifuzzingciconfigurationcreateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
|
||||
| <a id="mutationapifuzzingciconfigurationcreategitlabciyamleditpath"></a>`gitlabCiYamlEditPath` **{warning-solid}** | [`String`](#string) | **Deprecated:** The configuration snippet is now generated client-side. Deprecated in 14.6. |
|
||||
|
||||
### `Mutation.auditEventsStreamingHeadersCreate`
|
||||
|
||||
Input type: `AuditEventsStreamingHeadersCreateInput`
|
||||
|
||||
#### Arguments
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="mutationauditeventsstreamingheaderscreateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
|
||||
| <a id="mutationauditeventsstreamingheaderscreatedestinationid"></a>`destinationId` | [`AuditEventsExternalAuditEventDestinationID!`](#auditeventsexternalauditeventdestinationid) | Destination to associate header with. |
|
||||
| <a id="mutationauditeventsstreamingheaderscreatekey"></a>`key` | [`String!`](#string) | Header key. |
|
||||
| <a id="mutationauditeventsstreamingheaderscreatevalue"></a>`value` | [`String!`](#string) | Header value. |
|
||||
|
||||
#### Fields
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="mutationauditeventsstreamingheaderscreateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
|
||||
| <a id="mutationauditeventsstreamingheaderscreateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
|
||||
| <a id="mutationauditeventsstreamingheaderscreateheader"></a>`header` | [`AuditEventStreamingHeader`](#auditeventstreamingheader) | Created header. |
|
||||
|
||||
### `Mutation.awardEmojiAdd`
|
||||
|
||||
Input type: `AwardEmojiAddInput`
|
||||
|
@ -5679,6 +5700,29 @@ The edge type for [`AlertManagementIntegration`](#alertmanagementintegration).
|
|||
| <a id="alertmanagementintegrationedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
|
||||
| <a id="alertmanagementintegrationedgenode"></a>`node` | [`AlertManagementIntegration`](#alertmanagementintegration) | The item at the end of the edge. |
|
||||
|
||||
#### `AuditEventStreamingHeaderConnection`
|
||||
|
||||
The connection type for [`AuditEventStreamingHeader`](#auditeventstreamingheader).
|
||||
|
||||
##### Fields
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="auditeventstreamingheaderconnectionedges"></a>`edges` | [`[AuditEventStreamingHeaderEdge]`](#auditeventstreamingheaderedge) | A list of edges. |
|
||||
| <a id="auditeventstreamingheaderconnectionnodes"></a>`nodes` | [`[AuditEventStreamingHeader]`](#auditeventstreamingheader) | A list of nodes. |
|
||||
| <a id="auditeventstreamingheaderconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
|
||||
|
||||
#### `AuditEventStreamingHeaderEdge`
|
||||
|
||||
The edge type for [`AuditEventStreamingHeader`](#auditeventstreamingheader).
|
||||
|
||||
##### Fields
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="auditeventstreamingheaderedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
|
||||
| <a id="auditeventstreamingheaderedgenode"></a>`node` | [`AuditEventStreamingHeader`](#auditeventstreamingheader) | The item at the end of the edge. |
|
||||
|
||||
#### `AwardEmojiConnection`
|
||||
|
||||
The connection type for [`AwardEmoji`](#awardemoji).
|
||||
|
@ -9137,6 +9181,18 @@ Represents a vulnerability asset type.
|
|||
| <a id="assettypetype"></a>`type` | [`String!`](#string) | Type of the asset. |
|
||||
| <a id="assettypeurl"></a>`url` | [`String!`](#string) | URL of the asset. |
|
||||
|
||||
### `AuditEventStreamingHeader`
|
||||
|
||||
Represents a HTTP header key/value that belongs to an audit streaming destination.
|
||||
|
||||
#### Fields
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="auditeventstreamingheaderid"></a>`id` | [`ID!`](#id) | ID of the header. |
|
||||
| <a id="auditeventstreamingheaderkey"></a>`key` | [`String!`](#string) | Key of the header. |
|
||||
| <a id="auditeventstreamingheadervalue"></a>`value` | [`String!`](#string) | Value of the header. |
|
||||
|
||||
### `AwardEmoji`
|
||||
|
||||
An emoji awarded by a user.
|
||||
|
@ -11293,6 +11349,7 @@ Represents an external resource to send audit events to.
|
|||
| ---- | ---- | ----------- |
|
||||
| <a id="externalauditeventdestinationdestinationurl"></a>`destinationUrl` | [`String!`](#string) | External destination to send audit events to. |
|
||||
| <a id="externalauditeventdestinationgroup"></a>`group` | [`Group!`](#group) | Group the destination belongs to. |
|
||||
| <a id="externalauditeventdestinationheaders"></a>`headers` | [`AuditEventStreamingHeaderConnection!`](#auditeventstreamingheaderconnection) | List of additional HTTP headers sent with each event. Available only when feature flag `streaming_audit_event_headers` is enabled. This flag is disabled by default, because the feature is experimental and is subject to change without notice. (see [Connections](#connections)) |
|
||||
| <a id="externalauditeventdestinationid"></a>`id` | [`ID!`](#id) | ID of the destination. |
|
||||
| <a id="externalauditeventdestinationverificationtoken"></a>`verificationToken` | [`String!`](#string) | Verification token to validate source of event. |
|
||||
|
||||
|
|
|
@ -228,17 +228,23 @@ Having this configuration in place:
|
|||
|
||||
### Protect critical environments under a group
|
||||
|
||||
To protect a group-level environment:
|
||||
To protect a group-level environment, make sure your environments have the correct
|
||||
[`deployment_tier`](index.md#deployment-tier-of-environments) defined in `.gitlab-ci.yml`.
|
||||
|
||||
1. Make sure your environments have the correct
|
||||
[`deployment_tier`](index.md#deployment-tier-of-environments) defined in
|
||||
`.gitlab-ci.yml`.
|
||||
1. Configure the group-level protected environments by using the
|
||||
[REST API](../../api/group_protected_environments.md).
|
||||
#### Using the UI
|
||||
|
||||
NOTE:
|
||||
Configuration [with the UI](https://gitlab.com/gitlab-org/gitlab/-/issues/325249)
|
||||
is scheduled for a later release.
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/325249) in GitLab 15.1 with a flag named `group_level_protected_environment`. Disabled by default.
|
||||
|
||||
1. On the top bar, select **Menu > Groups** and find your group.
|
||||
1. On the left sidebar, select **Settings > CI/CD**.
|
||||
1. Expand **Protected environments**.
|
||||
1. From the **Environment** list, select the [deployment tier of environments](index.md#deployment-tier-of-environments) you want to protect.
|
||||
1. In the **Allowed to deploy** list, select the [subgroups](../../user/group/subgroups/index.md) you want to give deploy access to.
|
||||
1. Select **Protect**.
|
||||
|
||||
#### Using the API
|
||||
|
||||
Configure the group-level protected environments by using the [REST API](../../api/group_protected_environments.md).
|
||||
|
||||
## Deployment approvals
|
||||
|
||||
|
|
|
@ -293,16 +293,18 @@ When authenticating using LDAP, the user's name and email are always synced.
|
|||
|
||||
> Introduced in GitLab 12.3.
|
||||
|
||||
With certain OmniAuth providers, users can sign in without
|
||||
using two-factor authentication.
|
||||
With certain OmniAuth providers, users can sign in without using two-factor authentication (2FA).
|
||||
|
||||
To bypass two-factor authentication, you can either:
|
||||
Because of a [known issue](https://gitlab.com/gitlab-org/gitlab/-/issues/196131) users must
|
||||
[set up 2FA](../user/profile/account/two_factor_authentication.md#enable-two-factor-authentication) on their GitLab
|
||||
account to bypass 2FA. Otherwise, they are prompted to set up 2FA when they sign in to GitLab.
|
||||
|
||||
To bypass 2FA, you can either:
|
||||
|
||||
- Define the allowed providers using an array (for example, `['twitter', 'google_oauth2']`).
|
||||
- Specify `true` to allow all providers, or `false` to allow none.
|
||||
|
||||
This option should be configured only for providers that already have
|
||||
two-factor authentication. The default is `false`.
|
||||
This option should be configured only for providers that already have 2FA. The default is `false`.
|
||||
|
||||
This configuration doesn't apply to SAML.
|
||||
|
||||
|
|
|
@ -48,6 +48,26 @@ By default, the GitLab.com and self-managed settings for the
|
|||
- ECDSA_SK SSH keys are allowed (GitLab 14.8 and later).
|
||||
- ED25519_SK SSH keys are allowed (GitLab 14.8 and later).
|
||||
|
||||
### Block banned or compromised keys **(FREE)**
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/24614) in GitLab 15.1 [with a flag](../administration/feature_flags.md) named `ssh_banned_key`. Disabled by default.
|
||||
|
||||
FLAG:
|
||||
On self-managed GitLab, by default this feature is not available. To make it available,
|
||||
ask an administrator to [enable the feature flag](../administration/feature_flags.md) named `ssh_banned_key`.
|
||||
On GitLab.com, this feature is available but can be configured by GitLab.com administrators only.
|
||||
The feature is not ready for production use.
|
||||
|
||||
When users attempt to [add a new SSH key](../user/ssh.md#add-an-ssh-key-to-your-gitlab-account)
|
||||
to GitLab accounts, the key is checked against a list of SSH keys which are known
|
||||
to be compromised. Users can't add keys from this list to any GitLab account.
|
||||
This restriction cannot be configured. This restriction exists because the private
|
||||
keys associated with the key pair are publicly known, and can be used to access
|
||||
accounts using the key pair.
|
||||
|
||||
If your key is disallowed by this restriction, [generate a new SSH key pair](../user/ssh.md#generate-an-ssh-key-pair)
|
||||
to use instead.
|
||||
|
||||
<!-- ## Troubleshooting
|
||||
|
||||
Include any troubleshooting steps that you can foresee. If you know beforehand what issues
|
||||
|
|
|
@ -52,6 +52,7 @@ By default both administrators and anyone with the **Owner** role can delete a p
|
|||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/255449) in GitLab 14.2 for groups created after August 12, 2021.
|
||||
> - [Renamed](https://gitlab.com/gitlab-org/gitlab/-/issues/352960) from default delayed project deletion in GitLab 15.1.
|
||||
> - [Enabled for projects in personal namespaces](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/89466) in GitLab 15.1.
|
||||
|
||||
Instance-level protection against accidental deletion of groups and projects.
|
||||
|
||||
|
|
|
@ -455,7 +455,9 @@ in GitLab 12.6, and then to [immediate deletion](https://gitlab.com/gitlab-org/g
|
|||
|
||||
### Delayed project deletion **(PREMIUM)**
|
||||
|
||||
Projects in a group (not a personal namespace) can be deleted after a delay period. Multiple settings can affect whether
|
||||
> [Enabled for projects in personal namespaces](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/89466) in GitLab 15.1.
|
||||
|
||||
Projects can be deleted after a delay period. Multiple settings can affect whether
|
||||
delayed project deletion is enabled for a particular project:
|
||||
|
||||
- Self-managed instance [settings](../../admin_area/settings/visibility_and_access_controls.md#deletion-protection).
|
||||
|
|
|
@ -39,6 +39,7 @@ ar_internal_metadata: :gitlab_shared
|
|||
atlassian_identities: :gitlab_main
|
||||
audit_events_external_audit_event_destinations: :gitlab_main
|
||||
audit_events: :gitlab_main
|
||||
audit_events_streaming_headers: :gitlab_main
|
||||
authentication_events: :gitlab_main
|
||||
award_emoji: :gitlab_main
|
||||
aws_roles: :gitlab_main
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Signature verification with ed25519 keys
|
||||
# requires this gem to be loaded.
|
||||
require 'ed25519'
|
||||
|
||||
module Gitlab
|
||||
module Ssh
|
||||
class Signature
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
def initialize(signature_text, signed_text, committer_email)
|
||||
@signature_text = signature_text
|
||||
@signed_text = signed_text
|
||||
@committer_email = committer_email
|
||||
end
|
||||
|
||||
def verification_status
|
||||
strong_memoize(:verification_status) do
|
||||
next :unverified unless all_attributes_present?
|
||||
next :unverified unless valid_signature_blob? && committer
|
||||
next :unknown_key unless signed_by_key
|
||||
next :other_user unless signed_by_key.user == committer
|
||||
|
||||
:verified
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def all_attributes_present?
|
||||
# Signing an empty string is valid, but signature_text and committer_email
|
||||
# must be non-empty.
|
||||
@signed_text && @signature_text.present? && @committer_email.present?
|
||||
end
|
||||
|
||||
# Verifies the signature using the public key embedded in the blob.
|
||||
# This proves that the signed_text was signed by the private key
|
||||
# of the public key identified by `key_fingerprint`. Afterwards, we
|
||||
# still need to check that the key belongs to the committer.
|
||||
def valid_signature_blob?
|
||||
return false unless signature
|
||||
|
||||
signature.verify(@signed_text)
|
||||
end
|
||||
|
||||
def committer
|
||||
# Lookup by email because users can push verified commits that were made
|
||||
# by someone else. For example: Doing a rebase.
|
||||
strong_memoize(:committer) { User.find_by_any_email(@committer_email, confirmed: true) }
|
||||
end
|
||||
|
||||
def signature
|
||||
strong_memoize(:signature) do
|
||||
::SSHData::Signature.parse_pem(@signature_text)
|
||||
rescue SSHData::DecodeError
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def key_fingerprint
|
||||
strong_memoize(:key_fingerprint) { signature&.public_key&.fingerprint }
|
||||
end
|
||||
|
||||
def signed_by_key
|
||||
strong_memoize(:signed_by_key) do
|
||||
next unless key_fingerprint
|
||||
|
||||
Key.find_by_fingerprint_sha256(key_fingerprint)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -15,6 +15,29 @@ module Gitlab
|
|||
Technology.new(:ed25519_sk, SSHData::PublicKey::SKED25519, [256], %w(sk-ssh-ed25519@openssh.com))
|
||||
].freeze
|
||||
|
||||
BANNED_SSH_KEY_FINGERPRINTS = [
|
||||
# https://github.com/rapid7/ssh-badkeys/tree/master/authorized
|
||||
# banned ssh rsa keys
|
||||
"SHA256:Z+q4XhSwWY7q0BIDVPR1v/S306FjGBsid7tLq/8kIxM",
|
||||
"SHA256:uy5wXyEgbRCGsk23+J6f85om7G55Cu3UIPwC7oMZhNQ",
|
||||
"SHA256:9prMbqhS4QteoFQ1ZRJDqSBLWoHXPyKB0iWR05Ghro4",
|
||||
"SHA256:1M4RzhMyWuFS/86uPY/ce2prh/dVTHW7iD2RhpquOZA",
|
||||
|
||||
# banned ssh dsa keys
|
||||
"SHA256:/JLp6z6uGE3BPcs70RQob6QOdEWQ6nDC0xY7ejPOCc0",
|
||||
"SHA256:whDP3xjKBEettbDuecxtGsfWBST+78gb6McdB9P7jCU",
|
||||
"SHA256:MEc4HfsOlMqJ3/9QMTmrKn5Xj/yfnMITMW8EwfUfTww",
|
||||
"SHA256:aPoYT2nPIfhqv6BIlbCCpbDjirBxaDFOtPfZ2K20uWw",
|
||||
"SHA256:VtjqZ5fiaeoZ3mXOYi49Lk9aO31iT4pahKFP9JPiQPc",
|
||||
|
||||
# other banned ssh keys
|
||||
# https://github.com/BenBE/kompromat/commit/c8d9a05ea155a1ed609c617d4516f0ac978e8559
|
||||
"SHA256:Z+q4XhSwWY7q0BIDVPR1v/S306FjGBsid7tLq/8kIxM",
|
||||
|
||||
# https://www.ctrlu.net/vuln/0006.html
|
||||
"SHA256:2ewGtK7Dc8XpnfNKShczdc8HSgoEGpoX+MiJkfH2p5I"
|
||||
].to_set.freeze
|
||||
|
||||
def self.technologies
|
||||
if Gitlab::FIPS.enabled?
|
||||
Gitlab::FIPS::SSH_KEY_TECHNOLOGIES
|
||||
|
@ -115,6 +138,10 @@ module Gitlab
|
|||
end
|
||||
end
|
||||
|
||||
def banned?
|
||||
BANNED_SSH_KEY_FINGERPRINTS.include?(fingerprint_sha256)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def technology
|
||||
|
|
|
@ -1045,6 +1045,9 @@ msgstr ""
|
|||
msgid "%{timebox_type} must have a start and due date"
|
||||
msgstr ""
|
||||
|
||||
msgid "%{time} UTC"
|
||||
msgstr ""
|
||||
|
||||
msgid "%{title} %{operator} %{threshold}"
|
||||
msgstr ""
|
||||
|
||||
|
@ -20261,9 +20264,6 @@ msgstr ""
|
|||
msgid "Incidents|Must start with http or https"
|
||||
msgstr ""
|
||||
|
||||
msgid "Incident|Add new timeline event"
|
||||
msgstr ""
|
||||
|
||||
msgid "Incident|Alert details"
|
||||
msgstr ""
|
||||
|
||||
|
@ -20288,6 +20288,9 @@ msgstr ""
|
|||
msgid "Incident|No timeline items have been added yet."
|
||||
msgstr ""
|
||||
|
||||
msgid "Incident|Something went wrong while fetching incident timeline events."
|
||||
msgstr ""
|
||||
|
||||
msgid "Incident|Summary"
|
||||
msgstr ""
|
||||
|
||||
|
@ -34605,6 +34608,9 @@ msgstr ""
|
|||
msgid "Select a branch"
|
||||
msgstr ""
|
||||
|
||||
msgid "Select a branch to compare"
|
||||
msgstr ""
|
||||
|
||||
msgid "Select a compliance framework to apply to this project. %{linkStart}How are these added?%{linkEnd}"
|
||||
msgstr ""
|
||||
|
||||
|
@ -34719,6 +34725,9 @@ msgstr ""
|
|||
msgid "Select source branch"
|
||||
msgstr ""
|
||||
|
||||
msgid "Select source project"
|
||||
msgstr ""
|
||||
|
||||
msgid "Select start date"
|
||||
msgstr ""
|
||||
|
||||
|
@ -34740,6 +34749,9 @@ msgstr ""
|
|||
msgid "Select target branch or tag"
|
||||
msgstr ""
|
||||
|
||||
msgid "Select target project"
|
||||
msgstr ""
|
||||
|
||||
msgid "Select timezone"
|
||||
msgstr ""
|
||||
|
||||
|
@ -39781,9 +39793,6 @@ msgstr ""
|
|||
msgid "To reactivate your account, sign in to GitLab at %{gitlab_url}."
|
||||
msgstr ""
|
||||
|
||||
msgid "To renew, export your license usage file and email it to %{renewal_service_email}. A new license will be emailed to the email address registered in the %{customers_dot}. You can add this license to your instance."
|
||||
msgstr ""
|
||||
|
||||
msgid "To resolve this, try to:"
|
||||
msgstr ""
|
||||
|
||||
|
@ -43992,12 +44001,6 @@ msgstr ""
|
|||
msgid "Your %{host} account was signed in to from a new location"
|
||||
msgstr ""
|
||||
|
||||
msgid "Your %{plan} subscription expired on %{expiry_date}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Your %{plan} subscription expires on %{expiry_date}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Your %{spammable_entity_type} has been recognized as spam and has been discarded."
|
||||
msgstr ""
|
||||
|
||||
|
@ -44300,9 +44303,6 @@ msgid_plural "Your subscription has %{remaining_seat_count} out of %{total_seat_
|
|||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "Your subscription is now expired. To renew, export your license usage file and email it to %{renewal_service_email}. A new license will be emailed to the email address registered in the %{customers_dot}. You can add this license to your instance. To use Free tier, remove your current license."
|
||||
msgstr ""
|
||||
|
||||
msgid "Your subscription will expire in %{remaining_days} day."
|
||||
msgid_plural "Your subscription will expire in %{remaining_days} days."
|
||||
msgstr[0] ""
|
||||
|
@ -44548,6 +44548,9 @@ msgstr ""
|
|||
msgid "cannot be enabled until a valid credit card is on file"
|
||||
msgstr ""
|
||||
|
||||
msgid "cannot be used because it belongs to a compromised private key. Stop using this key and generate a new one."
|
||||
msgstr ""
|
||||
|
||||
msgid "cannot be used for user namespace"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ module QA
|
|||
|
||||
module_function
|
||||
|
||||
def shell(command, stdin_data: nil, fail_on_exception: true, stream_progress: true) # rubocop:disable Metrics/CyclomaticComplexity
|
||||
def shell(command, stdin_data: nil, fail_on_exception: true, stream_progress: true, mask_secrets: []) # rubocop:disable Metrics/CyclomaticComplexity
|
||||
cmd_string = Array(command).join(' ')
|
||||
|
||||
QA::Runtime::Logger.info("Executing: `#{cmd_string.cyan}`")
|
||||
|
@ -24,6 +24,8 @@ module QA
|
|||
cmd_output = ''
|
||||
|
||||
out.each do |line|
|
||||
line = mask_secrets_on_string(line, mask_secrets)
|
||||
|
||||
cmd_output += line
|
||||
yield line if block_given?
|
||||
|
||||
|
@ -36,7 +38,7 @@ module QA
|
|||
|
||||
if wait.value.exited? && wait.value.exitstatus.nonzero? && fail_on_exception
|
||||
Runtime::Logger.error("Command output:\n#{cmd_output.strip}") unless cmd_output.empty?
|
||||
raise CommandError, "Command: `#{cmd_string}` failed! ✘"
|
||||
raise CommandError, "Command: `#{mask_secrets_on_string(cmd_string, mask_secrets)}` failed! ✘"
|
||||
end
|
||||
|
||||
Runtime::Logger.debug("Command output:\n#{cmd_output.strip}") unless cmd_output.empty?
|
||||
|
@ -66,6 +68,10 @@ module QA
|
|||
line =~ regex
|
||||
end
|
||||
end
|
||||
|
||||
def mask_secrets_on_string(str, secrets)
|
||||
secrets.reduce(str) { |s, secret| s.gsub(secret, '****') }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module QA
|
||||
RSpec.describe Service::Shellout do
|
||||
let(:wait_thread) { instance_double('Thread') }
|
||||
let(:errored_wait) { instance_double('Process::Status', exited?: true, exitstatus: 1) }
|
||||
let(:non_errored_wait) { instance_double('Process::Status', exited?: true, exitstatus: 0) }
|
||||
let(:stdin) { StringIO.new }
|
||||
let(:stdout) { [+'logged in as user with password secret'] }
|
||||
|
||||
context 'when masking secrets' do
|
||||
before do
|
||||
allow(Open3).to receive(:popen2e).and_yield(stdin, stdout, wait_thread)
|
||||
end
|
||||
|
||||
it 'masks command secrets on CommandError' do
|
||||
expect(wait_thread).to receive(:value).twice.and_return(errored_wait)
|
||||
|
||||
expect { subject.shell('docker login -u user -p secret', mask_secrets: %w[secret user]) }
|
||||
.to raise_error(Service::Shellout::CommandError) do |error|
|
||||
expect(error.to_s).to include('Command: `docker login -u **** -p ****` failed')
|
||||
end
|
||||
end
|
||||
|
||||
it 'masking secrets is optional' do
|
||||
expect(wait_thread).to receive(:value).twice.and_return(errored_wait)
|
||||
|
||||
expect { subject.shell('docker pull ruby:3') }.to raise_error(Service::Shellout::CommandError) do |error|
|
||||
expect(error.to_s).to include('Command: `docker pull ruby:3` failed')
|
||||
end
|
||||
end
|
||||
|
||||
it 'masks secrets when yielding output' do
|
||||
expect(wait_thread).to receive(:value).twice.and_return(non_errored_wait)
|
||||
|
||||
subject.shell('docker login -u user -p secret', mask_secrets: %w[secret user]) do |output|
|
||||
expect(output).not_to be(nil)
|
||||
expect(output).to eql('logged in as **** with password ****')
|
||||
end
|
||||
end
|
||||
|
||||
it 'masks secrets in debug logs' do
|
||||
expect(Runtime::Logger).to receive(:debug).with(/logged in as \*\*\*\* with password \*\*\*\*/)
|
||||
expect(wait_thread).to receive(:value).twice.and_return(non_errored_wait)
|
||||
|
||||
subject.shell('docker login -u user -p secret', mask_secrets: %w[secret user])
|
||||
end
|
||||
|
||||
it 'masks secrets in error logs' do
|
||||
expect(Runtime::Logger).to receive(:error).with(/logged in as \*\*\*\* with password \*\*\*\*/)
|
||||
expect(wait_thread).to receive(:value).twice.and_return(errored_wait)
|
||||
|
||||
expect { subject.shell('docker login -u user -p secret', mask_secrets: %w[secret user]) }
|
||||
.to raise_error(Service::Shellout::CommandError)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -2,63 +2,118 @@
|
|||
|
||||
module RuboCop
|
||||
module Cop
|
||||
# This cop flags translation definitions in static scopes because changing
|
||||
# locales has no effect and won't translate this text again.
|
||||
#
|
||||
# See https://docs.gitlab.com/ee/development/i18n/externalization.html#keep-translations-dynamic
|
||||
#
|
||||
# @example
|
||||
#
|
||||
# # bad
|
||||
# class MyExample
|
||||
# # Constant
|
||||
# Translation = _('A translation.')
|
||||
#
|
||||
# # Class scope
|
||||
# field :foo, title: _('A title')
|
||||
#
|
||||
# validates :title, :presence, message: _('is missing')
|
||||
#
|
||||
# # Memoized
|
||||
# def self.translations
|
||||
# @cached ||= { text: _('A translation.') }
|
||||
# end
|
||||
#
|
||||
# included do # or prepended or class_methods
|
||||
# self.error_message = _('Something went wrong.')
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# # good
|
||||
# class MyExample
|
||||
# # Keep translations dynamic.
|
||||
# Translation = -> { _('A translation.') }
|
||||
# # OR
|
||||
# def translation
|
||||
# _('A translation.')
|
||||
# end
|
||||
#
|
||||
# field :foo, title: -> { _('A title') }
|
||||
#
|
||||
# validates :title, :presence, message: -> { _('is missing') }
|
||||
#
|
||||
# def self.translations
|
||||
# { text: _('A translation.') }
|
||||
# end
|
||||
#
|
||||
# included do # or prepended or class_methods
|
||||
# self.error_message = -> { _('Something went wrong.') }
|
||||
# end
|
||||
# end
|
||||
#
|
||||
class StaticTranslationDefinition < RuboCop::Cop::Cop
|
||||
MSG = "The text you're translating will be already in the translated form when it's assigned to the constant. When a users changes the locale, these texts won't be translated again. Consider moving the translation logic to a method."
|
||||
MSG = <<~TEXT.tr("\n", ' ')
|
||||
Translation is defined in static scope.
|
||||
Keep translations dynamic. See https://docs.gitlab.com/ee/development/i18n/externalization.html#keep-translations-dynamic
|
||||
TEXT
|
||||
|
||||
TRANSLATION_METHODS = %i[_ s_ n_].freeze
|
||||
RESTRICT_ON_SEND = %i[_ s_ n_].freeze
|
||||
|
||||
# List of method names which are not considered real method definitions.
|
||||
# See https://api.rubyonrails.org/classes/ActiveSupport/Concern.html
|
||||
NON_METHOD_DEFINITIONS = %i[class_methods included prepended].to_set.freeze
|
||||
|
||||
def_node_matcher :translation_method?, <<~PATTERN
|
||||
(send _ _ str*)
|
||||
PATTERN
|
||||
|
||||
def_node_matcher :lambda_node?, <<~PATTERN
|
||||
(send _ :lambda)
|
||||
PATTERN
|
||||
|
||||
def_node_matcher :struct_constant_assignment?, <<~PATTERN
|
||||
(casgn _ _ `(const _ :Struct))
|
||||
(send _ {#{RESTRICT_ON_SEND.map(&:inspect).join(' ')}} str*)
|
||||
PATTERN
|
||||
|
||||
def on_send(node)
|
||||
return unless translation_method?(node)
|
||||
|
||||
method_name = node.children[1]
|
||||
return unless TRANSLATION_METHODS.include?(method_name)
|
||||
|
||||
translation_memoized = false
|
||||
static = true
|
||||
memoized = false
|
||||
|
||||
node.each_ancestor do |ancestor|
|
||||
receiver, _ = *ancestor
|
||||
break if lambda_node?(receiver) # translations defined in lambda nodes should be allowed
|
||||
|
||||
if constant_assignment?(ancestor) && !struct_constant_assignment?(ancestor)
|
||||
add_offense(node, location: :expression)
|
||||
|
||||
break
|
||||
end
|
||||
|
||||
translation_memoized = true if memoization?(ancestor)
|
||||
|
||||
if translation_memoized && class_method_definition?(ancestor)
|
||||
add_offense(node, location: :expression)
|
||||
memoized = true if memoized?(ancestor)
|
||||
|
||||
if dynamic?(ancestor, memoized)
|
||||
static = false
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
add_offense(node) if static
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def constant_assignment?(node)
|
||||
node.type == :casgn
|
||||
end
|
||||
|
||||
def memoization?(node)
|
||||
def memoized?(node)
|
||||
node.type == :or_asgn
|
||||
end
|
||||
|
||||
def class_method_definition?(node)
|
||||
node.type == :defs
|
||||
def dynamic?(node, memoized)
|
||||
lambda_or_proc?(node) ||
|
||||
named_block?(node) ||
|
||||
instance_method_definition?(node) ||
|
||||
unmemoized_class_method_definition?(node, memoized)
|
||||
end
|
||||
|
||||
def lambda_or_proc?(node)
|
||||
node.lambda_or_proc?
|
||||
end
|
||||
|
||||
def named_block?(node)
|
||||
return unless node.block_type?
|
||||
|
||||
!NON_METHOD_DEFINITIONS.include?(node.method_name) # rubocop:disable Rails/NegateInclude
|
||||
end
|
||||
|
||||
def instance_method_definition?(node)
|
||||
node.type == :def
|
||||
end
|
||||
|
||||
def unmemoized_class_method_definition?(node, memoized)
|
||||
node.type == :defs && !memoized
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -6,10 +6,11 @@ FactoryBot.define do
|
|||
source { association(:project) }
|
||||
maintainer
|
||||
|
||||
trait(:guest) { access_level { ProjectMember::GUEST } }
|
||||
trait(:reporter) { access_level { ProjectMember::REPORTER } }
|
||||
trait(:guest) { access_level { ProjectMember::GUEST } }
|
||||
trait(:reporter) { access_level { ProjectMember::REPORTER } }
|
||||
trait(:developer) { access_level { ProjectMember::DEVELOPER } }
|
||||
trait(:maintainer) { access_level { ProjectMember::MAINTAINER } }
|
||||
trait(:owner) { access_level { ProjectMember::OWNER } }
|
||||
trait(:access_request) { requested_at { Time.now } }
|
||||
|
||||
trait(:invited) do
|
||||
|
|
|
@ -77,7 +77,7 @@ describe('fetchBoard', () => {
|
|||
},
|
||||
};
|
||||
|
||||
it('should commit mutation RECEIVE_BOARD_SUCCESS and dispatch setBoardConfig on success', async () => {
|
||||
it('should commit mutation REQUEST_CURRENT_BOARD and dispatch setBoard on success', async () => {
|
||||
jest.spyOn(gqlClient, 'query').mockResolvedValue(queryResponse);
|
||||
|
||||
await testAction({
|
||||
|
@ -87,12 +87,8 @@ describe('fetchBoard', () => {
|
|||
{
|
||||
type: types.REQUEST_CURRENT_BOARD,
|
||||
},
|
||||
{
|
||||
type: types.RECEIVE_BOARD_SUCCESS,
|
||||
payload: mockBoard,
|
||||
},
|
||||
],
|
||||
expectedActions: [{ type: 'setBoardConfig', payload: mockBoard }],
|
||||
expectedActions: [{ type: 'setBoard', payload: mockBoard }],
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -978,10 +974,6 @@ describe('fetchItemsForList', () => {
|
|||
{ listId },
|
||||
state,
|
||||
[
|
||||
{
|
||||
type: types.RESET_ITEMS_FOR_LIST,
|
||||
payload: listId,
|
||||
},
|
||||
{
|
||||
type: types.REQUEST_ITEMS_FOR_LIST,
|
||||
payload: { listId, fetchNext: false },
|
||||
|
@ -1003,10 +995,6 @@ describe('fetchItemsForList', () => {
|
|||
{ listId },
|
||||
state,
|
||||
[
|
||||
{
|
||||
type: types.RESET_ITEMS_FOR_LIST,
|
||||
payload: listId,
|
||||
},
|
||||
{
|
||||
type: types.REQUEST_ITEMS_FOR_LIST,
|
||||
payload: { listId, fetchNext: false },
|
||||
|
|
|
@ -300,24 +300,6 @@ describe('Board Store Mutations', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('RESET_ITEMS_FOR_LIST', () => {
|
||||
it('should remove issues from boardItemsByListId state', () => {
|
||||
const listId = 'gid://gitlab/List/1';
|
||||
const boardItemsByListId = {
|
||||
[listId]: [mockIssue.id],
|
||||
};
|
||||
|
||||
state = {
|
||||
...state,
|
||||
boardItemsByListId,
|
||||
};
|
||||
|
||||
mutations[types.RESET_ITEMS_FOR_LIST](state, listId);
|
||||
|
||||
expect(state.boardItemsByListId[listId]).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('REQUEST_ITEMS_FOR_LIST', () => {
|
||||
const listId = 'gid://gitlab/List/1';
|
||||
const boardItemsByListId = {
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
trackCheckout,
|
||||
trackTransaction,
|
||||
trackAddToCartUsageTab,
|
||||
getNamespaceId,
|
||||
} from '~/google_tag_manager';
|
||||
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
|
||||
import { logError } from '~/lib/logger';
|
||||
|
@ -401,6 +402,7 @@ describe('~/google_tag_manager/index', () => {
|
|||
{
|
||||
brand: 'GitLab',
|
||||
category: 'DevOps',
|
||||
dimension36: 'not available',
|
||||
id,
|
||||
name,
|
||||
price: revenue.toString(),
|
||||
|
@ -478,4 +480,26 @@ describe('~/google_tag_manager/index', () => {
|
|||
resetHTMLFixture();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when getting the namespace_id from Snowplow standard context', () => {
|
||||
describe('when window.gl.snowplowStandardContext.data.namespace_id has a value', () => {
|
||||
beforeEach(() => {
|
||||
window.gl = { snowplowStandardContext: { data: { namespace_id: '321' } } };
|
||||
});
|
||||
|
||||
it('returns the value', () => {
|
||||
expect(getNamespaceId()).toBe('321');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when window.gl.snowplowStandardContext.data.namespace_id is undefined', () => {
|
||||
beforeEach(() => {
|
||||
window.gl = {};
|
||||
});
|
||||
|
||||
it('returns a placeholder value', () => {
|
||||
expect(getNamespaceId()).toBe('not available');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -57,12 +57,12 @@ exports[`Alert integration settings form should match the default snapshot 1`] =
|
|||
</gl-button-stub>
|
||||
|
||||
<gl-modal-stub
|
||||
actioncancel="[object Object]"
|
||||
actionprimary="[object Object]"
|
||||
arialabel=""
|
||||
dismisslabel="Close"
|
||||
modalclass=""
|
||||
modalid="resetWebhookModal"
|
||||
ok-title="Reset webhook URL"
|
||||
ok-variant="danger"
|
||||
size="md"
|
||||
title="Reset webhook URL"
|
||||
titletag="h4"
|
||||
|
|
|
@ -47,7 +47,7 @@ describe('Alert integration settings form', () => {
|
|||
resetWebhookUrl.mockResolvedValueOnce({
|
||||
data: { pagerduty_webhook_url: newWebhookUrl },
|
||||
});
|
||||
findModal().vm.$emit('ok');
|
||||
findModal().vm.$emit('primary');
|
||||
await waitForPromises();
|
||||
expect(resetWebhookUrl).toHaveBeenCalled();
|
||||
expect(findWebhookInput().attributes('value')).toBe(newWebhookUrl);
|
||||
|
@ -56,7 +56,7 @@ describe('Alert integration settings form', () => {
|
|||
|
||||
it('should show error message and NOT reset webhook url', async () => {
|
||||
resetWebhookUrl.mockRejectedValueOnce();
|
||||
findModal().vm.$emit('ok');
|
||||
findModal().vm.$emit('primary');
|
||||
await waitForPromises();
|
||||
expect(findAlert().attributes('variant')).toBe('danger');
|
||||
});
|
||||
|
|
|
@ -36,6 +36,7 @@ describe('Incident Tabs component', () => {
|
|||
fullPath: '',
|
||||
iid: '',
|
||||
projectId: '',
|
||||
issuableId: '',
|
||||
uploadMetricsFeatureAvailable: true,
|
||||
glFeatures: { incidentTimeline: true },
|
||||
},
|
||||
|
@ -48,6 +49,9 @@ describe('Incident Tabs component', () => {
|
|||
alert: {
|
||||
loading: true,
|
||||
},
|
||||
timelineEvents: {
|
||||
loading: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
export const mockEvents = [
|
||||
{
|
||||
action: 'comment',
|
||||
author: {
|
||||
__typename: 'UserCore',
|
||||
id: 'gid://gitlab/User/1',
|
||||
name: 'Administrator',
|
||||
username: 'root',
|
||||
},
|
||||
createdAt: '2022-03-22T15:59:08Z',
|
||||
id: 'gid://gitlab/IncidentManagement::TimelineEvent/132',
|
||||
note: 'Dummy event 1',
|
||||
noteHtml: '<p>Dummy event 1</p>',
|
||||
occurredAt: '2022-03-22T15:59:00Z',
|
||||
updatedAt: '2022-03-22T15:59:08Z',
|
||||
__typename: 'TimelineEventType',
|
||||
},
|
||||
{
|
||||
action: 'comment',
|
||||
author: {
|
||||
__typename: 'UserCore',
|
||||
id: 'gid://gitlab/User/1',
|
||||
name: 'Administrator',
|
||||
username: 'root',
|
||||
},
|
||||
createdAt: '2022-03-23T14:57:08Z',
|
||||
id: 'gid://gitlab/IncidentManagement::TimelineEvent/131',
|
||||
note: 'Dummy event 2',
|
||||
noteHtml: '<p>Dummy event 2</p>',
|
||||
occurredAt: '2022-03-23T14:57:00Z',
|
||||
updatedAt: '2022-03-23T14:57:08Z',
|
||||
__typename: 'TimelineEventType',
|
||||
},
|
||||
{
|
||||
action: 'comment',
|
||||
author: {
|
||||
__typename: 'UserCore',
|
||||
id: 'gid://gitlab/User/1',
|
||||
name: 'Administrator',
|
||||
username: 'root',
|
||||
},
|
||||
createdAt: '2022-03-23T15:59:08Z',
|
||||
id: 'gid://gitlab/IncidentManagement::TimelineEvent/132',
|
||||
note: 'Dummy event 3',
|
||||
noteHtml: '<p>Dummy event 3</p>',
|
||||
occurredAt: '2022-03-23T15:59:00Z',
|
||||
updatedAt: '2022-03-23T15:59:08Z',
|
||||
__typename: 'TimelineEventType',
|
||||
},
|
||||
];
|
||||
|
||||
export const timelineEventsQueryListResponse = {
|
||||
data: {
|
||||
project: {
|
||||
id: 'gid://gitlab/Project/8',
|
||||
incidentManagementTimelineEvents: {
|
||||
nodes: mockEvents,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const timelineEventsQueryEmptyResponse = {
|
||||
data: {
|
||||
project: {
|
||||
id: 'gid://gitlab/Project/8',
|
||||
incidentManagementTimelineEvents: {
|
||||
nodes: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
|
@ -0,0 +1,87 @@
|
|||
import timezoneMock from 'timezone-mock';
|
||||
import merge from 'lodash/merge';
|
||||
import { GlIcon } from '@gitlab/ui';
|
||||
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import IncidentTimelineEventListItem from '~/issues/show/components/incidents/timeline_events_list_item.vue';
|
||||
import { mockEvents } from './mock_data';
|
||||
|
||||
describe('IncidentTimelineEventList', () => {
|
||||
let wrapper;
|
||||
|
||||
const mountComponent = (propsData) => {
|
||||
const { action, noteHtml, occurredAt } = mockEvents[0];
|
||||
wrapper = mountExtended(
|
||||
IncidentTimelineEventListItem,
|
||||
merge({
|
||||
propsData: {
|
||||
action,
|
||||
noteHtml,
|
||||
occurredAt,
|
||||
isLastItem: false,
|
||||
...propsData,
|
||||
},
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const findCommentIcon = () => wrapper.findComponent(GlIcon);
|
||||
const findTextContainer = () => wrapper.findByTestId('event-text-container');
|
||||
const findEventTime = () => wrapper.findByTestId('event-time');
|
||||
|
||||
describe('template', () => {
|
||||
it('shows comment icon', () => {
|
||||
mountComponent();
|
||||
|
||||
expect(findCommentIcon().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('sets correct props for icon', () => {
|
||||
mountComponent();
|
||||
|
||||
expect(findCommentIcon().props('name')).toBe(mockEvents[0].action);
|
||||
});
|
||||
|
||||
it('displays the correct time', () => {
|
||||
mountComponent();
|
||||
|
||||
expect(findEventTime().text()).toBe('15:59 UTC');
|
||||
});
|
||||
|
||||
describe('last item in list', () => {
|
||||
it('shows a bottom border when not the last item', () => {
|
||||
mountComponent();
|
||||
|
||||
expect(findTextContainer().classes()).toContain('gl-border-1');
|
||||
});
|
||||
|
||||
it('does not show a bottom border when the last item', () => {
|
||||
mountComponent({ isLastItem: true });
|
||||
|
||||
expect(wrapper.classes()).not.toContain('gl-border-1');
|
||||
});
|
||||
});
|
||||
|
||||
describe.each`
|
||||
timezone
|
||||
${'Europe/London'}
|
||||
${'US/Pacific'}
|
||||
${'Australia/Adelaide'}
|
||||
`('when viewing in timezone', ({ timezone }) => {
|
||||
describe(timezone, () => {
|
||||
beforeEach(() => {
|
||||
timezoneMock.register(timezone);
|
||||
|
||||
mountComponent();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
timezoneMock.unregister();
|
||||
});
|
||||
|
||||
it('displays the correct time', () => {
|
||||
expect(findEventTime().text()).toBe('15:59 UTC');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,87 @@
|
|||
import timezoneMock from 'timezone-mock';
|
||||
import merge from 'lodash/merge';
|
||||
import { shallowMountExtended, extendedWrapper } from 'helpers/vue_test_utils_helper';
|
||||
import IncidentTimelineEventList from '~/issues/show/components/incidents/timeline_events_list.vue';
|
||||
import { mockEvents } from './mock_data';
|
||||
|
||||
describe('IncidentTimelineEventList', () => {
|
||||
let wrapper;
|
||||
|
||||
const mountComponent = () => {
|
||||
wrapper = shallowMountExtended(
|
||||
IncidentTimelineEventList,
|
||||
merge({
|
||||
provide: {
|
||||
fullPath: 'group/project',
|
||||
issuableId: '1',
|
||||
},
|
||||
propsData: {
|
||||
timelineEvents: mockEvents,
|
||||
},
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const findGroups = () => wrapper.findAllByTestId('timeline-group');
|
||||
const findItems = (base = wrapper) => base.findAllByTestId('timeline-event');
|
||||
const findFirstGroup = () => extendedWrapper(findGroups().at(0));
|
||||
const findSecondGroup = () => extendedWrapper(findGroups().at(1));
|
||||
const findDates = () => wrapper.findAllByTestId('event-date');
|
||||
|
||||
describe('template', () => {
|
||||
it('groups items correctly', () => {
|
||||
mountComponent();
|
||||
|
||||
expect(findGroups()).toHaveLength(2);
|
||||
|
||||
expect(findItems(findFirstGroup())).toHaveLength(1);
|
||||
expect(findItems(findSecondGroup())).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('sets the isLastItem prop correctly', () => {
|
||||
mountComponent();
|
||||
|
||||
expect(findItems().at(0).props('isLastItem')).toBe(false);
|
||||
expect(findItems().at(1).props('isLastItem')).toBe(false);
|
||||
expect(findItems().at(2).props('isLastItem')).toBe(true);
|
||||
});
|
||||
|
||||
it('sets the event props correctly', () => {
|
||||
mountComponent();
|
||||
|
||||
expect(findItems().at(1).props('occurredAt')).toBe(mockEvents[1].occurredAt);
|
||||
expect(findItems().at(1).props('action')).toBe(mockEvents[1].action);
|
||||
expect(findItems().at(1).props('noteHtml')).toBe(mockEvents[1].noteHtml);
|
||||
});
|
||||
|
||||
it('formats dates correctly', () => {
|
||||
mountComponent();
|
||||
|
||||
expect(findDates().at(0).text()).toBe('2022-03-22');
|
||||
expect(findDates().at(1).text()).toBe('2022-03-23');
|
||||
});
|
||||
|
||||
describe.each`
|
||||
timezone
|
||||
${'Europe/London'}
|
||||
${'US/Pacific'}
|
||||
${'Australia/Adelaide'}
|
||||
`('when viewing in timezone', ({ timezone }) => {
|
||||
describe(timezone, () => {
|
||||
beforeEach(() => {
|
||||
timezoneMock.register(timezone);
|
||||
|
||||
mountComponent();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
timezoneMock.unregister();
|
||||
});
|
||||
|
||||
it('displays the correct time', () => {
|
||||
expect(findDates().at(0).text()).toBe('2022-03-22');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue