Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-10-15 18:08:43 +00:00
parent d9e71b0d41
commit 316fbf9f95
129 changed files with 2676 additions and 1736 deletions

View file

@ -2,6 +2,18 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
## 13.4.4 (2020-10-15)
### Fixed (2 changes)
- Fix rollback portion of migration that adds temporary index for container scanning findings. !44593
- Improve merge error when pre-receive hooks fail in fast-forward merge. !44843
### Other (1 change)
- Revert 42465 and 42343: Expanded collapsed diff files. !43361
## 13.4.3 (2020-10-06)
### Fixed (3 changes)

View file

@ -33,30 +33,13 @@ export default {
query: alertsHelpUrlQuery,
},
},
props: {
enableAlertManagementPath: {
type: String,
required: true,
},
userCanEnableAlertManagement: {
type: Boolean,
required: true,
},
emptyAlertSvgPath: {
type: String,
required: true,
},
opsgenieMvcEnabled: {
type: Boolean,
required: false,
default: false,
},
opsgenieMvcTargetUrl: {
type: String,
required: false,
default: '',
},
},
inject: [
'enableAlertManagementPath',
'userCanEnableAlertManagement',
'emptyAlertSvgPath',
'opsgenieMvcEnabled',
'opsgenieMvcTargetUrl',
],
data() {
return {
alertsHelpUrl: '',

View file

@ -1,6 +1,4 @@
<script>
import Tracking from '~/tracking';
import { trackAlertListViewsOptions } from '../constants';
import AlertManagementEmptyState from './alert_management_empty_state.vue';
import AlertManagementTable from './alert_management_table.vue';
@ -9,67 +7,12 @@ export default {
AlertManagementEmptyState,
AlertManagementTable,
},
props: {
projectPath: {
type: String,
required: true,
},
alertManagementEnabled: {
type: Boolean,
required: true,
},
enableAlertManagementPath: {
type: String,
required: true,
},
populatingAlertsHelpUrl: {
type: String,
required: true,
},
userCanEnableAlertManagement: {
type: Boolean,
required: true,
},
emptyAlertSvgPath: {
type: String,
required: true,
},
opsgenieMvcEnabled: {
type: Boolean,
required: false,
default: false,
},
opsgenieMvcTargetUrl: {
type: String,
required: false,
default: '',
},
},
mounted() {
this.trackPageViews();
},
methods: {
trackPageViews() {
const { category, action } = trackAlertListViewsOptions;
Tracking.event(category, action);
},
},
inject: ['alertManagementEnabled'],
};
</script>
<template>
<div>
<alert-management-table
v-if="alertManagementEnabled"
:populating-alerts-help-url="populatingAlertsHelpUrl"
:project-path="projectPath"
/>
<alert-management-empty-state
v-else
:empty-alert-svg-path="emptyAlertSvgPath"
:enable-alert-management-path="enableAlertManagementPath"
:user-can-enable-alert-management="userCanEnableAlertManagement"
:opsgenie-mvc-enabled="opsgenieMvcEnabled"
:opsgenie-mvc-target-url="opsgenieMvcTargetUrl"
/>
<alert-management-table v-if="alertManagementEnabled" />
<alert-management-empty-state v-else />
</div>
</template>

View file

@ -1,58 +1,43 @@
<script>
/* eslint-disable vue/no-v-html */
import {
GlAlert,
GlLoadingIcon,
GlTable,
GlAlert,
GlAvatarsInline,
GlAvatarLink,
GlAvatar,
GlIcon,
GlLink,
GlTabs,
GlTab,
GlBadge,
GlPagination,
GlSearchBoxByType,
GlSprintf,
GlTooltipDirective,
} from '@gitlab/ui';
import { debounce, trim } from 'lodash';
import { __, s__ } from '~/locale';
import { joinPaths, visitUrl } from '~/lib/utils/url_utility';
import { s__, __ } from '~/locale';
import { fetchPolicies } from '~/lib/graphql';
import { joinPaths, visitUrl } from '~/lib/utils/url_utility';
import PaginatedTableWithSearchAndTabs from '~/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue';
import {
tdClass,
thClass,
bodyTrClass,
initialPaginationState,
} from '~/vue_shared/components/paginated_table_with_search_and_tabs/constants';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import { convertToSnakeCase } from '~/lib/utils/text_utility';
import Tracking from '~/tracking';
import getAlerts from '../graphql/queries/get_alerts.query.graphql';
import getAlertsCountByStatus from '../graphql/queries/get_count_by_status.query.graphql';
import {
ALERTS_STATUS_TABS,
ALERTS_SEVERITY_LABELS,
DEFAULT_PAGE_SIZE,
trackAlertListViewsOptions,
trackAlertStatusUpdateOptions,
} from '../constants';
import AlertStatus from './alert_status.vue';
const tdClass =
'table-col gl-display-flex d-md-table-cell gl-align-items-center gl-white-space-nowrap';
const thClass = 'gl-hover-bg-blue-50';
const bodyTrClass =
'gl-border-1 gl-border-t-solid gl-border-gray-100 gl-hover-bg-blue-50 gl-hover-cursor-pointer gl-hover-border-b-solid gl-hover-border-blue-200';
const TH_TEST_ID = { 'data-testid': 'alert-management-severity-sort' };
const initialPaginationState = {
currentPage: 1,
prevPageCursor: '',
nextPageCursor: '',
firstPageSize: DEFAULT_PAGE_SIZE,
lastPageSize: null,
};
const TWELVE_HOURS_IN_MS = 12 * 60 * 60 * 1000;
export default {
trackAlertListViewsOptions,
i18n: {
noAlertsMsg: s__(
'AlertManagement|No alerts available to display. See %{linkStart}enabling alert management%{linkEnd} for more information on adding alerts to the list.',
@ -60,7 +45,6 @@ export default {
errorMsg: s__(
"AlertManagement|There was an error displaying the alerts. Confirm your endpoint's configuration details to ensure alerts appear.",
),
searchPlaceholder: __('Search or filter results...'),
unassigned: __('Unassigned'),
},
fields: [
@ -115,36 +99,23 @@ export default {
severityLabels: ALERTS_SEVERITY_LABELS,
statusTabs: ALERTS_STATUS_TABS,
components: {
GlAlert,
GlLoadingIcon,
GlTable,
GlAlert,
GlAvatarsInline,
GlAvatarLink,
GlAvatar,
TimeAgo,
GlIcon,
GlLink,
GlTabs,
GlTab,
GlBadge,
GlPagination,
GlSearchBoxByType,
GlSprintf,
AlertStatus,
PaginatedTableWithSearchAndTabs,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
projectPath: {
type: String,
required: true,
},
populatingAlertsHelpUrl: {
type: String,
required: true,
},
},
inject: ['projectPath', 'textQuery', 'assigneeUsernameQuery', 'populatingAlertsHelpUrl'],
apollo: {
alerts: {
fetchPolicy: fetchPolicies.CACHE_AND_NETWORK,
@ -152,6 +123,7 @@ export default {
variables() {
return {
searchTerm: this.searchTerm,
assigneeUsername: this.assigneeUsername,
projectPath: this.projectPath,
statuses: this.statusFilter,
sort: this.sort,
@ -182,14 +154,16 @@ export default {
};
},
error() {
this.hasError = true;
this.errored = true;
},
},
alertsCount: {
fetchPolicy: fetchPolicies.CACHE_AND_NETWORK,
query: getAlertsCountByStatus,
variables() {
return {
searchTerm: this.searchTerm,
assigneeUsername: this.assigneeUsername,
projectPath: this.projectPath,
};
},
@ -200,288 +174,234 @@ export default {
},
data() {
return {
searchTerm: '',
hasError: false,
errorMessage: '',
isAlertDismissed: false,
errored: false,
serverErrorMessage: '',
isErrorAlertDismissed: false,
sort: 'STARTED_AT_DESC',
statusFilter: [],
filteredByStatus: '',
pagination: initialPaginationState,
alerts: {},
alertsCount: {},
sortBy: 'startedAt',
sortDesc: true,
sortDirection: 'desc',
searchTerm: this.textQuery,
assigneeUsername: this.assigneeUsernameQuery,
pagination: initialPaginationState,
};
},
computed: {
showErrorMsg() {
return this.errored && !this.isErrorAlertDismissed;
},
showNoAlertsMsg() {
return (
!this.hasError &&
!this.errored &&
!this.loading &&
this.alertsCount?.all === 0 &&
!this.searchTerm &&
!this.isAlertDismissed
!this.assigneeUsername &&
!this.isErrorAlertDismissed
);
},
loading() {
return this.$apollo.queries.alerts.loading;
},
hasAlerts() {
return this.alerts?.list?.length;
isEmpty() {
return !this.alerts?.list?.length;
},
showPaginationControls() {
return Boolean(this.prevPage || this.nextPage);
},
alertsForCurrentTab() {
return this.alertsCount ? this.alertsCount[this.filteredByStatus.toLowerCase()] : 0;
},
prevPage() {
return Math.max(this.pagination.currentPage - 1, 0);
},
nextPage() {
const nextPage = this.pagination.currentPage + 1;
return nextPage > Math.ceil(this.alertsForCurrentTab / DEFAULT_PAGE_SIZE) ? null : nextPage;
},
},
mounted() {
this.trackPageViews();
},
methods: {
filterAlertsByStatus(tabIndex) {
this.resetPagination();
const { filters, status } = this.$options.statusTabs[tabIndex];
this.statusFilter = filters;
this.filteredByStatus = status;
},
fetchSortedData({ sortBy, sortDesc }) {
const sortingDirection = sortDesc ? 'DESC' : 'ASC';
const sortingColumn = convertToSnakeCase(sortBy).toUpperCase();
this.resetPagination();
this.pagination = initialPaginationState;
this.sort = `${sortingColumn}_${sortingDirection}`;
},
onInputChange: debounce(function debounceSearch(input) {
const trimmedInput = trim(input);
if (trimmedInput !== this.searchTerm) {
this.resetPagination();
this.searchTerm = trimmedInput;
}
}, 500),
navigateToAlertDetails({ iid }, index, { metaKey }) {
return visitUrl(joinPaths(window.location.pathname, iid, 'details'), metaKey);
},
trackPageViews() {
const { category, action } = trackAlertListViewsOptions;
Tracking.event(category, action);
},
trackStatusUpdate(status) {
const { category, action, label } = trackAlertStatusUpdateOptions;
Tracking.event(category, action, { label, property: status });
},
hasAssignees(assignees) {
return Boolean(assignees.nodes?.length);
},
getIssueLink(item) {
return joinPaths('/', this.projectPath, '-', 'issues', item.issueIid);
},
handlePageChange(page) {
const { startCursor, endCursor } = this.alerts.pageInfo;
if (page > this.pagination.currentPage) {
this.pagination = {
...initialPaginationState,
nextPageCursor: endCursor,
currentPage: page,
};
} else {
this.pagination = {
lastPageSize: DEFAULT_PAGE_SIZE,
firstPageSize: null,
prevPageCursor: startCursor,
nextPageCursor: '',
currentPage: page,
};
}
},
resetPagination() {
this.pagination = initialPaginationState;
},
tbodyTrClass(item) {
return {
[bodyTrClass]: !this.loading && this.hasAlerts,
[bodyTrClass]: !this.loading && !this.isEmpty,
'new-alert': item?.isNew,
};
},
handleAlertError(errorMessage) {
this.hasError = true;
this.errorMessage = errorMessage;
this.errored = true;
this.serverErrorMessage = errorMessage;
},
dismissError() {
this.hasError = false;
this.errorMessage = '';
handleStatusUpdate() {
this.$apollo.queries.alerts.refetch();
this.$apollo.queries.alertsCount.refetch();
},
pageChanged(pagination) {
this.pagination = pagination;
},
statusChanged({ filters, status }) {
this.statusFilter = filters;
this.filteredByStatus = status;
},
filtersChanged({ searchTerm, assigneeUsername }) {
this.searchTerm = searchTerm;
this.assigneeUsername = assigneeUsername;
},
errorAlertDismissed() {
this.errored = false;
this.serverErrorMessage = '';
this.isErrorAlertDismissed = true;
},
},
};
</script>
<template>
<div>
<div class="incident-management-list">
<gl-alert v-if="showNoAlertsMsg" @dismiss="isAlertDismissed = true">
<gl-sprintf :message="$options.i18n.noAlertsMsg">
<template #link="{ content }">
<gl-link
class="gl-display-inline-block"
:href="populatingAlertsHelpUrl"
target="_blank"
>
{{ content }}
</gl-link>
</template>
</gl-sprintf>
</gl-alert>
<gl-alert v-if="hasError" variant="danger" data-testid="alert-error" @dismiss="dismissError">
<p v-html="errorMessage || $options.i18n.errorMsg"></p>
</gl-alert>
<gl-tabs
content-class="gl-p-0 gl-border-b-solid gl-border-b-1 gl-border-gray-100"
@input="filterAlertsByStatus"
>
<gl-tab v-for="tab in $options.statusTabs" :key="tab.status">
<template slot="title">
<span>{{ tab.title }}</span>
<gl-badge v-if="alertsCount" pill size="sm" class="gl-tab-counter-badge">
{{ alertsCount[tab.status.toLowerCase()] }}
</gl-badge>
</template>
</gl-tab>
</gl-tabs>
<div class="gl-bg-gray-10 gl-p-5 gl-border-b-solid gl-border-b-1 gl-border-gray-100">
<gl-search-box-by-type
class="gl-bg-white"
:placeholder="$options.i18n.searchPlaceholder"
@input="onInputChange"
/>
</div>
<h4 class="d-block d-md-none my-3">
{{ s__('AlertManagement|Alerts') }}
</h4>
<gl-table
class="alert-management-table"
:items="alerts ? alerts.list : []"
:fields="$options.fields"
:show-empty="true"
:busy="loading"
stacked="md"
:tbody-tr-class="tbodyTrClass"
:no-local-sorting="true"
:sort-direction="sortDirection"
:sort-desc.sync="sortDesc"
:sort-by.sync="sortBy"
sort-icon-left
fixed
@row-clicked="navigateToAlertDetails"
@sort-changed="fetchSortedData"
>
<template #cell(severity)="{ item }">
<div
class="d-inline-flex align-items-center justify-content-between"
data-testid="severityField"
>
<gl-icon
class="mr-2"
:size="12"
:name="`severity-${item.severity.toLowerCase()}`"
:class="`icon-${item.severity.toLowerCase()}`"
/>
{{ $options.severityLabels[item.severity] }}
</div>
</template>
<template #cell(startedAt)="{ item }">
<time-ago v-if="item.startedAt" :time="item.startedAt" />
</template>
<template #cell(eventCount)="{ item }">
{{ item.eventCount }}
</template>
<template #cell(alertLabel)="{ item }">
<div
class="gl-max-w-full text-truncate"
:title="`${item.iid} - ${item.title}`"
data-testid="idField"
>
#{{ item.iid }} {{ item.title }}
</div>
</template>
<template #cell(issue)="{ item }">
<gl-link v-if="item.issueIid" data-testid="issueField" :href="getIssueLink(item)">
#{{ item.issueIid }}
<gl-alert v-if="showNoAlertsMsg" @dismiss="errorAlertDismissed">
<gl-sprintf :message="$options.i18n.noAlertsMsg">
<template #link="{ content }">
<gl-link class="gl-display-inline-block" :href="populatingAlertsHelpUrl" target="_blank">
{{ content }}
</gl-link>
<div v-else data-testid="issueField">{{ s__('AlertManagement|None') }}</div>
</template>
</gl-sprintf>
</gl-alert>
<template #cell(assignees)="{ item }">
<div data-testid="assigneesField">
<template v-if="hasAssignees(item.assignees)">
<gl-avatars-inline
:avatars="item.assignees.nodes"
:collapsed="true"
:max-visible="4"
:avatar-size="24"
badge-tooltip-prop="name"
:badge-tooltip-max-chars="100"
>
<template #avatar="{ avatar }">
<gl-avatar-link
:key="avatar.username"
v-gl-tooltip
target="_blank"
:href="avatar.webUrl"
:title="avatar.name"
>
<gl-avatar :src="avatar.avatarUrl" :label="avatar.name" :size="24" />
</gl-avatar-link>
</template>
</gl-avatars-inline>
</template>
<template v-else>
{{ $options.i18n.unassigned }}
</template>
</div>
</template>
<paginated-table-with-search-and-tabs
:show-error-msg="showErrorMsg"
:i18n="$options.i18n"
:items="alerts.list || []"
:page-info="alerts.pageInfo"
:items-count="alertsCount"
:status-tabs="$options.statusTabs"
:track-views-options="$options.trackAlertListViewsOptions"
:server-error-message="serverErrorMessage"
:filter-search-tokens="['assignee_username']"
filter-search-key="alerts"
@page-changed="pageChanged"
@tabs-changed="statusChanged"
@filters-changed="filtersChanged"
@error-alert-dismissed="errorAlertDismissed"
>
<template #header-actions></template>
<template #cell(status)="{ item }">
<alert-status
:alert="item"
:project-path="projectPath"
:is-sidebar="false"
@alert-error="handleAlertError"
/>
</template>
<template #title>
{{ s__('AlertManagement|Alerts') }}
</template>
<template #empty>
{{ s__('AlertManagement|No alerts to display.') }}
</template>
<template #table>
<gl-table
class="alert-management-table"
:items="alerts ? alerts.list : []"
:fields="$options.fields"
:show-empty="true"
:busy="loading"
stacked="md"
:tbody-tr-class="tbodyTrClass"
:no-local-sorting="true"
:sort-direction="sortDirection"
:sort-desc.sync="sortDesc"
:sort-by.sync="sortBy"
sort-icon-left
fixed
@row-clicked="navigateToAlertDetails"
@sort-changed="fetchSortedData"
>
<template #cell(severity)="{ item }">
<div
class="d-inline-flex align-items-center justify-content-between"
data-testid="severityField"
>
<gl-icon
class="mr-2"
:size="12"
:name="`severity-${item.severity.toLowerCase()}`"
:class="`icon-${item.severity.toLowerCase()}`"
/>
{{ $options.severityLabels[item.severity] }}
</div>
</template>
<template #table-busy>
<gl-loading-icon size="lg" color="dark" class="mt-3" />
</template>
</gl-table>
<template #cell(startedAt)="{ item }">
<time-ago v-if="item.startedAt" :time="item.startedAt" />
</template>
<gl-pagination
v-if="showPaginationControls"
:value="pagination.currentPage"
:prev-page="prevPage"
:next-page="nextPage"
align="center"
class="gl-pagination gl-mt-3"
@input="handlePageChange"
/>
</div>
<template #cell(eventCount)="{ item }">
{{ item.eventCount }}
</template>
<template #cell(alertLabel)="{ item }">
<div
class="gl-max-w-full text-truncate"
:title="`${item.iid} - ${item.title}`"
data-testid="idField"
>
#{{ item.iid }} {{ item.title }}
</div>
</template>
<template #cell(issue)="{ item }">
<gl-link v-if="item.issueIid" data-testid="issueField" :href="getIssueLink(item)">
#{{ item.issueIid }}
</gl-link>
<div v-else data-testid="issueField">{{ s__('AlertManagement|None') }}</div>
</template>
<template #cell(assignees)="{ item }">
<div data-testid="assigneesField">
<template v-if="hasAssignees(item.assignees)">
<gl-avatars-inline
:avatars="item.assignees.nodes"
:collapsed="true"
:max-visible="4"
:avatar-size="24"
badge-tooltip-prop="name"
:badge-tooltip-max-chars="100"
>
<template #avatar="{ avatar }">
<gl-avatar-link
:key="avatar.username"
v-gl-tooltip
target="_blank"
:href="avatar.webUrl"
:title="avatar.name"
>
<gl-avatar :src="avatar.avatarUrl" :label="avatar.name" :size="24" />
</gl-avatar-link>
</template>
</gl-avatars-inline>
</template>
<template v-else>
{{ $options.i18n.unassigned }}
</template>
</div>
</template>
<template #cell(status)="{ item }">
<alert-status
:alert="item"
:project-path="projectPath"
:is-sidebar="false"
@alert-error="handleAlertError"
@hide-dropdown="handleStatusUpdate"
/>
</template>
<template #empty>
{{ s__('AlertManagement|No alerts to display.') }}
</template>
<template #table-busy>
<gl-loading-icon size="lg" color="dark" class="mt-3" />
</template>
</gl-table>
</template>
</paginated-table-with-search-and-tabs>
</div>
</template>

View file

@ -3,7 +3,7 @@ import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { s__ } from '~/locale';
import Tracking from '~/tracking';
import { trackAlertStatusUpdateOptions } from '../constants';
import updateAlertStatus from '../graphql/mutations/update_alert_status.mutation.graphql';
import updateAlertStatusMutation from '../graphql/mutations/update_alert_status.mutation.graphql';
export default {
i18n: {
@ -50,7 +50,7 @@ export default {
this.$emit('handle-updating', true);
this.$apollo
.mutate({
mutation: updateAlertStatus,
mutation: updateAlertStatusMutation,
variables: {
iid: this.alert.iid,
status: status.toUpperCase(),
@ -59,8 +59,6 @@ export default {
})
.then(resp => {
this.trackStatusUpdate(status);
this.$emit('hide-dropdown');
const errors = resp.data?.updateAlertStatus?.errors || [];
if (errors[0]) {
@ -69,6 +67,8 @@ export default {
`${this.$options.i18n.UPDATE_ALERT_STATUS_ERROR} ${errors[0]}`,
);
}
this.$emit('hide-dropdown');
})
.catch(() => {
this.$emit(

View file

@ -229,11 +229,7 @@ export default {
<p class="gl-new-dropdown-header-top">
{{ __('Assign To') }}
</p>
<gl-search-box-by-type
v-model.trim="search"
class="m-2"
:placeholder="__('Search users')"
/>
<gl-search-box-by-type v-model.trim="search" :placeholder="__('Search users')" />
<div class="dropdown-content dropdown-body">
<template v-if="userListValid">
<gl-dropdown-item

View file

@ -63,5 +63,3 @@ export const trackAlertStatusUpdateOptions = {
action: 'update_alert_status',
label: 'Status',
};
export const DEFAULT_PAGE_SIZE = 20;

View file

@ -1,7 +1,6 @@
#import "../fragments/list_item.fragment.graphql"
query getAlerts(
$searchTerm: String
$projectPath: ID!
$statuses: [AlertManagementStatus!]
$sort: AlertManagementAlertSort
@ -9,10 +8,13 @@ query getAlerts(
$lastPageSize: Int
$prevPageCursor: String = ""
$nextPageCursor: String = ""
$searchTerm: String = ""
$assigneeUsername: String = ""
) {
project(fullPath: $projectPath) {
alertManagementAlerts(
search: $searchTerm
assigneeUsername: $assigneeUsername
statuses: $statuses
sort: $sort
first: $firstPageSize

View file

@ -1,6 +1,6 @@
query getAlertsCount($searchTerm: String, $projectPath: ID!) {
query getAlertsCount($searchTerm: String, $projectPath: ID!, $assigneeUsername: String = "") {
project(fullPath: $projectPath) {
alertManagementAlertStatusCounts(search: $searchTerm) {
alertManagementAlertStatusCounts(search: $searchTerm, assigneeUsername: $assigneeUsername) {
all
open
acknowledged

View file

@ -18,12 +18,12 @@ export default () => {
populatingAlertsHelpUrl,
alertsHelpUrl,
opsgenieMvcTargetUrl,
textQuery,
assigneeUsernameQuery,
alertManagementEnabled,
userCanEnableAlertManagement,
opsgenieMvcEnabled,
} = domEl.dataset;
let { alertManagementEnabled, userCanEnableAlertManagement, opsgenieMvcEnabled } = domEl.dataset;
alertManagementEnabled = parseBoolean(alertManagementEnabled);
userCanEnableAlertManagement = parseBoolean(userCanEnableAlertManagement);
opsgenieMvcEnabled = parseBoolean(opsgenieMvcEnabled);
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(
@ -50,23 +50,24 @@ export default () => {
return new Vue({
el: selector,
provide: {
projectPath,
textQuery,
assigneeUsernameQuery,
enableAlertManagementPath,
populatingAlertsHelpUrl,
emptyAlertSvgPath,
opsgenieMvcTargetUrl,
alertManagementEnabled: parseBoolean(alertManagementEnabled),
userCanEnableAlertManagement: parseBoolean(userCanEnableAlertManagement),
opsgenieMvcEnabled: parseBoolean(opsgenieMvcEnabled),
},
apolloProvider,
components: {
AlertManagementList,
},
render(createElement) {
return createElement('alert-management-list', {
props: {
projectPath,
enableAlertManagementPath,
populatingAlertsHelpUrl,
emptyAlertSvgPath,
alertManagementEnabled,
userCanEnableAlertManagement,
opsgenieMvcTargetUrl,
opsgenieMvcEnabled,
},
});
return createElement('alert-management-list');
},
});
};

View file

@ -388,7 +388,7 @@ export default {
<gl-form @submit.prevent="onSubmit" @reset.prevent="onReset">
<h5 class="gl-font-lg">{{ $options.i18n.integrationsLabel }}</h5>
<gl-form-group label-for="integrations" label-class="gl-font-weight-bold">
<gl-form-group label-for="integrations">
<div data-testid="alert-settings-description" class="gl-mt-5">
<p v-for="section in sections" :key="section.text">
<gl-sprintf :message="section.text">
@ -417,11 +417,7 @@ export default {
</gl-sprintf>
</span>
</gl-form-group>
<gl-form-group
:label="$options.i18n.activeLabel"
label-for="activated"
label-class="gl-font-weight-bold"
>
<gl-form-group :label="$options.i18n.activeLabel" label-for="activated">
<toggle-button
id="activated"
:disabled-input="loading"
@ -434,7 +430,6 @@ export default {
v-if="isOpsgenie || isPrometheus"
:label="$options.i18n.apiBaseUrlLabel"
label-for="api-url"
label-class="gl-font-weight-bold"
>
<gl-form-input
id="api-url"
@ -448,11 +443,7 @@ export default {
</span>
</gl-form-group>
<template v-if="!isOpsgenie">
<gl-form-group
:label="$options.i18n.urlLabel"
label-for="url"
label-class="gl-font-weight-bold"
>
<gl-form-group :label="$options.i18n.urlLabel" label-for="url">
<gl-form-input-group id="url" readonly :value="selectedService.url">
<template #append>
<clipboard-button
@ -466,11 +457,7 @@ export default {
{{ prometheusInfo }}
</span>
</gl-form-group>
<gl-form-group
:label="$options.i18n.authKeyLabel"
label-for="authorization-key"
label-class="gl-font-weight-bold"
>
<gl-form-group :label="$options.i18n.authKeyLabel" label-for="authorization-key">
<gl-form-input-group id="authorization-key" class="gl-mb-2" readonly :value="authKey">
<template #append>
<clipboard-button
@ -496,7 +483,6 @@ export default {
<gl-form-group
:label="$options.i18n.alertJson"
label-for="alert-json"
label-class="gl-font-weight-bold"
:invalid-feedback="testAlert.error"
>
<gl-form-textarea

View file

@ -0,0 +1,120 @@
<script>
import { mapGetters, mapActions } from 'vuex';
import { GlLabel } from '@gitlab/ui';
import LabelsSelect from '~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue';
import { isScopedLabel } from '~/lib/utils/common_utils';
import createFlash from '~/flash';
import { __ } from '~/locale';
export default {
components: {
BoardEditableItem,
LabelsSelect,
GlLabel,
},
data() {
return {
loading: false,
};
},
inject: ['labelsFetchPath', 'labelsManagePath', 'labelsFilterBasePath'],
computed: {
...mapGetters({ issue: 'getActiveIssue' }),
selectedLabels() {
const { labels = [] } = this.issue;
return labels.map(label => ({
...label,
id: getIdFromGraphQLId(label.id),
}));
},
issueLabels() {
const { labels = [] } = this.issue;
return labels.map(label => ({
...label,
scoped: isScopedLabel(label),
}));
},
projectPath() {
const { referencePath = '' } = this.issue;
return referencePath.slice(0, referencePath.indexOf('#'));
},
},
methods: {
...mapActions(['setActiveIssueLabels']),
async setLabels(payload) {
this.loading = true;
this.$refs.sidebarItem.collapse();
try {
const addLabelIds = payload.filter(label => label.set).map(label => label.id);
const removeLabelIds = this.selectedLabels
.filter(label => !payload.find(selected => selected.id === label.id))
.map(label => label.id);
const input = { addLabelIds, removeLabelIds, projectPath: this.projectPath };
await this.setActiveIssueLabels(input);
} catch (e) {
createFlash({ message: __('An error occurred while updating labels.') });
} finally {
this.loading = false;
}
},
async removeLabel(id) {
this.loading = true;
try {
const removeLabelIds = [getIdFromGraphQLId(id)];
const input = { removeLabelIds, projectPath: this.projectPath };
await this.setActiveIssueLabels(input);
} catch (e) {
createFlash({ message: __('An error occurred when removing the label.') });
} finally {
this.loading = false;
}
},
},
};
</script>
<template>
<board-editable-item ref="sidebarItem" :title="__('Labels')" :loading="loading">
<template #collapsed>
<gl-label
v-for="label in issueLabels"
:key="label.id"
:background-color="label.color"
:title="label.title"
:description="label.description"
:scoped="label.scoped"
:show-close-button="true"
:disabled="loading"
class="gl-mr-2 gl-mb-2"
@close="removeLabel(label.id)"
/>
</template>
<template>
<labels-select
ref="labelsSelect"
:allow-label-edit="false"
:allow-label-create="false"
:allow-multiselect="true"
:allow-scoped-labels="true"
:selected-labels="selectedLabels"
:labels-fetch-path="labelsFetchPath"
:labels-manage-path="labelsManagePath"
:labels-filter-base-path="labelsFilterBasePath"
:labels-list-title="__('Select label')"
:dropdown-button-text="__('Choose labels')"
variant="embedded"
class="gl-display-block labels gl-w-full"
@updateSelectedLabels="setLabels"
>
{{ __('None') }}
</labels-select>
</template>
</board-editable-item>
</template>

View file

@ -87,6 +87,9 @@ export default () => {
groupId: Number($boardApp.dataset.groupId),
rootPath: $boardApp.dataset.rootPath,
canUpdate: $boardApp.dataset.canUpdate,
labelsFetchPath: $boardApp.dataset.labelsFetchPath,
labelsManagePath: $boardApp.dataset.labelsManagePath,
labelsFilterBasePath: $boardApp.dataset.labelsFilterBasePath,
},
store,
apolloProvider,
@ -369,6 +372,10 @@ export default () => {
toggleFocusMode(ModalStore, boardsStore);
toggleLabels();
toggleEpicsSwimlanes();
if (gon.features?.swimlanes) {
toggleEpicsSwimlanes();
}
mountMultipleBoardsSwitcher();
};

View file

@ -0,0 +1,15 @@
mutation issueSetLabels($input: UpdateIssueInput!) {
updateIssue(input: $input) {
issue {
labels {
nodes {
id
title
color
description
}
}
}
errors
}
}

View file

@ -19,6 +19,7 @@ import boardListsQuery from '../queries/board_lists.query.graphql';
import createBoardListMutation from '../queries/board_list_create.mutation.graphql';
import updateBoardListMutation from '../queries/board_list_update.mutation.graphql';
import issueMoveListMutation from '../queries/issue_move_list.mutation.graphql';
import issueSetLabels from '../queries/issue_set_labels.mutation.graphql';
const notImplemented = () => {
/* eslint-disable-next-line @gitlab/require-i18n-strings */
@ -281,6 +282,31 @@ export default {
commit(types.ADD_ISSUE_TO_LIST_FAILURE, { list, issue });
},
setActiveIssueLabels: async ({ commit, getters }, input) => {
const activeIssue = getters.getActiveIssue;
const { data } = await gqlClient.mutate({
mutation: issueSetLabels,
variables: {
input: {
iid: String(activeIssue.iid),
addLabelIds: input.addLabelIds ?? [],
removeLabelIds: input.removeLabelIds ?? [],
projectPath: input.projectPath,
},
},
});
if (data.updateIssue?.errors?.length > 0) {
throw new Error(data.updateIssue.errors);
}
commit(types.UPDATE_ISSUE_BY_ID, {
issueId: activeIssue.id,
prop: 'labels',
value: data.updateIssue.issue.labels.nodes,
});
},
fetchBacklog: () => {
notImplemented();
},

View file

@ -5,7 +5,7 @@ export default {
getLabelToggleState: state => (state.isShowingLabels ? 'on' : 'off'),
isSidebarOpen: state => state.activeId !== inactiveId,
isSwimlanesOn: state => {
if (!gon?.features?.boardsWithSwimlanes) {
if (!gon?.features?.boardsWithSwimlanes && !gon?.features?.swimlanes) {
return false;
}

View file

@ -60,7 +60,7 @@ export default {
</script>
<template>
<gl-dropdown :text="value">
<gl-search-box-by-type v-model.trim="searchTerm" class="gl-m-3" />
<gl-search-box-by-type v-model.trim="searchTerm" />
<gl-dropdown-item
v-for="environment in filteredResults"
:key="environment"

View file

@ -130,7 +130,6 @@ export default {
<gl-search-box-by-type
v-model.trim="searchQuery"
:placeholder="s__('ClusterIntegration|Search domains')"
class="gl-m-3"
/>
<gl-dropdown-item
v-for="domain in filteredDomains"

View file

@ -80,7 +80,6 @@ export default {
<gl-search-box-by-type
ref="searchBox"
v-model.trim="environmentSearch"
class="gl-m-3"
@focus="fetchEnvironments"
@keyup="fetchEnvironments"
/>

View file

@ -2,41 +2,32 @@
import {
GlLoadingIcon,
GlTable,
GlAlert,
GlAvatarsInline,
GlAvatarLink,
GlAvatar,
GlTooltipDirective,
GlButton,
GlIcon,
GlPagination,
GlTabs,
GlTab,
GlBadge,
GlEmptyState,
} from '@gitlab/ui';
import Api from '~/api';
import Tracking from '~/tracking';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue';
import { convertToSnakeCase } from '~/lib/utils/text_utility';
import { s__, __ } from '~/locale';
import { urlParamsToObject } from '~/lib/utils/common_utils';
import PaginatedTableWithSearchAndTabs from '~/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue';
import {
visitUrl,
mergeUrlParams,
joinPaths,
updateHistory,
setUrlParams,
} from '~/lib/utils/url_utility';
tdClass,
thClass,
bodyTrClass,
initialPaginationState,
} from '~/vue_shared/components/paginated_table_with_search_and_tabs/constants';
import { convertToSnakeCase } from '~/lib/utils/text_utility';
import { s__ } from '~/locale';
import { visitUrl, mergeUrlParams, joinPaths } from '~/lib/utils/url_utility';
import getIncidents from '../graphql/queries/get_incidents.query.graphql';
import getIncidentsCountByStatus from '../graphql/queries/get_count_by_status.query.graphql';
import SeverityToken from '~/sidebar/components/severity/severity.vue';
import { INCIDENT_SEVERITY } from '~/sidebar/components/severity/constants';
import {
I18N,
DEFAULT_PAGE_SIZE,
INCIDENT_STATUS_TABS,
TH_CREATED_AT_TEST_ID,
TH_INCIDENT_SLA_TEST_ID,
@ -44,24 +35,12 @@ import {
TH_PUBLISHED_TEST_ID,
INCIDENT_DETAILS_PATH,
trackIncidentCreateNewOptions,
trackIncidentListViewsOptions,
} from '../constants';
const tdClass =
'table-col gl-display-flex d-md-table-cell gl-align-items-center gl-white-space-nowrap';
const thClass = 'gl-hover-bg-blue-50';
const bodyTrClass =
'gl-border-1 gl-border-t-solid gl-border-gray-100 gl-hover-cursor-pointer gl-hover-bg-blue-50 gl-hover-border-b-solid gl-hover-border-blue-200';
const initialPaginationState = {
currentPage: 1,
prevPageCursor: '',
nextPageCursor: '',
firstPageSize: DEFAULT_PAGE_SIZE,
lastPageSize: null,
};
export default {
trackIncidentCreateNewOptions,
trackIncidentListViewsOptions,
i18n: I18N,
statusTabs: INCIDENT_STATUS_TABS,
fields: [
@ -112,23 +91,18 @@ export default {
components: {
GlLoadingIcon,
GlTable,
GlAlert,
GlAvatarsInline,
GlAvatarLink,
GlAvatar,
GlButton,
TimeAgoTooltip,
GlIcon,
GlPagination,
GlTabs,
GlTab,
PublishedCell: () => import('ee_component/incidents/components/published_cell.vue'),
ServiceLevelAgreementCell: () =>
import('ee_component/incidents/components/service_level_agreement_cell.vue'),
GlBadge,
GlEmptyState,
SeverityToken,
FilteredSearchBar,
PaginatedTableWithSearchAndTabs,
},
directives: {
GlTooltip: GlTooltipDirective,
@ -142,8 +116,8 @@ export default {
'publishedAvailable',
'emptyListSvgPath',
'textQuery',
'authorUsernamesQuery',
'assigneeUsernamesQuery',
'authorUsernameQuery',
'assigneeUsernameQuery',
'slaFeatureAvailable',
],
apollo: {
@ -152,16 +126,16 @@ export default {
variables() {
return {
searchTerm: this.searchTerm,
status: this.statusFilter,
authorUsername: this.authorUsername,
assigneeUsername: this.assigneeUsername,
projectPath: this.projectPath,
status: this.statusFilter,
issueTypes: ['INCIDENT'],
sort: this.sort,
firstPageSize: this.pagination.firstPageSize,
lastPageSize: this.pagination.lastPageSize,
prevPageCursor: this.pagination.prevPageCursor,
nextPageCursor: this.pagination.nextPageCursor,
authorUsername: this.authorUsername,
assigneeUsernames: this.assigneeUsernames,
};
},
update({ project: { issues: { nodes = [], pageInfo = {} } = {} } = {} }) {
@ -180,7 +154,7 @@ export default {
return {
searchTerm: this.searchTerm,
authorUsername: this.authorUsername,
assigneeUsernames: this.assigneeUsernames,
assigneeUsername: this.assigneeUsername,
projectPath: this.projectPath,
issueTypes: ['INCIDENT'],
};
@ -195,17 +169,17 @@ export default {
errored: false,
isErrorAlertDismissed: false,
redirecting: false,
searchTerm: this.textQuery,
pagination: initialPaginationState,
incidents: {},
incidentsCount: {},
sort: 'created_desc',
sortBy: 'createdAt',
sortDesc: true,
statusFilter: '',
filteredByStatus: '',
authorUsername: this.authorUsernamesQuery,
assigneeUsernames: this.assigneeUsernamesQuery,
filterParams: {},
searchTerm: this.textQuery,
authorUsername: this.authorUsernameQuery,
assigneeUsername: this.assigneeUsernameQuery,
pagination: initialPaginationState,
};
},
computed: {
@ -215,29 +189,15 @@ export default {
loading() {
return this.$apollo.queries.incidents.loading;
},
hasIncidents() {
return this.incidents?.list?.length;
isEmpty() {
return !this.incidents?.list?.length;
},
incidentsForCurrentTab() {
return this.incidentsCount?.[this.filteredByStatus.toLowerCase()] ?? 0;
},
showPaginationControls() {
return Boolean(
this.incidents?.pageInfo?.hasNextPage || this.incidents?.pageInfo?.hasPreviousPage,
);
},
prevPage() {
return Math.max(this.pagination.currentPage - 1, 0);
},
nextPage() {
const nextPage = this.pagination.currentPage + 1;
return nextPage > Math.ceil(this.incidentsForCurrentTab / DEFAULT_PAGE_SIZE)
? null
: nextPage;
showList() {
return !this.isEmpty || this.errored || this.loading;
},
tbodyTrClass() {
return {
[bodyTrClass]: !this.loading && this.hasIncidents,
[bodyTrClass]: !this.loading && !this.isEmpty,
};
},
newIncidentPath() {
@ -257,12 +217,6 @@ export default {
return this.$options.fields.filter(({ key }) => !isHidden[key]);
},
isEmpty() {
return !this.incidents.list?.length;
},
showList() {
return !this.isEmpty || this.errored || this.loading;
},
activeClosedTabHasNoIncidents() {
const { all, closed } = this.incidentsCount || {};
const isClosedTabActive = this.statusFilter === this.$options.statusTabs[1].filters;
@ -285,63 +239,8 @@ export default {
btnText: createIncidentBtnLabel,
};
},
filteredSearchTokens() {
return [
{
type: 'author_username',
icon: 'user',
title: __('Author'),
unique: true,
symbol: '@',
token: AuthorToken,
operators: [{ value: '=', description: __('is'), default: 'true' }],
fetchPath: this.projectPath,
fetchAuthors: Api.projectUsers.bind(Api),
},
{
type: 'assignee_username',
icon: 'user',
title: __('Assignees'),
unique: true,
symbol: '@',
token: AuthorToken,
operators: [{ value: '=', description: __('is'), default: 'true' }],
fetchPath: this.projectPath,
fetchAuthors: Api.projectUsers.bind(Api),
},
];
},
filteredSearchValue() {
const value = [];
if (this.authorUsername) {
value.push({
type: 'author_username',
value: { data: this.authorUsername },
});
}
if (this.assigneeUsernames) {
value.push({
type: 'assignee_username',
value: { data: this.assigneeUsernames },
});
}
if (this.searchTerm) {
value.push(this.searchTerm);
}
return value;
},
},
methods: {
filterIncidentsByStatus(tabIndex) {
this.resetPagination();
const { filters, status } = this.$options.statusTabs[tabIndex];
this.statusFilter = filters;
this.filteredByStatus = status;
},
hasAssignees(assignees) {
return Boolean(assignees.nodes?.length);
},
@ -353,255 +252,170 @@ export default {
Tracking.event(category, action);
this.redirecting = true;
},
handlePageChange(page) {
const { startCursor, endCursor } = this.incidents.pageInfo;
if (page > this.pagination.currentPage) {
this.pagination = {
...initialPaginationState,
nextPageCursor: endCursor,
currentPage: page,
};
} else {
this.pagination = {
lastPageSize: DEFAULT_PAGE_SIZE,
firstPageSize: null,
prevPageCursor: startCursor,
nextPageCursor: '',
currentPage: page,
};
}
},
resetPagination() {
this.pagination = initialPaginationState;
},
fetchSortedData({ sortBy, sortDesc }) {
const sortingDirection = sortDesc ? 'DESC' : 'ASC';
const sortingColumn = convertToSnakeCase(sortBy)
.replace(/_.*/, '')
.toUpperCase();
this.resetPagination();
this.pagination = initialPaginationState;
this.sort = `${sortingColumn}_${sortingDirection}`;
},
getSeverity(severity) {
return INCIDENT_SEVERITY[severity];
},
handleFilterIncidents(filters) {
this.resetPagination();
const filterParams = { authorUsername: '', assigneeUsername: '', search: '' };
filters.forEach(filter => {
if (typeof filter === 'object') {
switch (filter.type) {
case 'author_username':
filterParams.authorUsername = filter.value.data;
break;
case 'assignee_username':
filterParams.assigneeUsername = filter.value.data;
break;
case 'filtered-search-term':
if (filter.value.data !== '') filterParams.search = filter.value.data;
break;
default:
break;
}
}
});
this.filterParams = filterParams;
this.updateUrl();
this.searchTerm = filterParams?.search;
this.authorUsername = filterParams?.authorUsername;
this.assigneeUsernames = filterParams?.assigneeUsername;
pageChanged(pagination) {
this.pagination = pagination;
},
updateUrl() {
const queryParams = urlParamsToObject(window.location.search);
const { authorUsername, assigneeUsername, search } = this.filterParams || {};
if (authorUsername) {
queryParams.author_username = authorUsername;
} else {
delete queryParams.author_username;
}
if (assigneeUsername) {
queryParams.assignee_username = assigneeUsername;
} else {
delete queryParams.assignee_username;
}
if (search) {
queryParams.search = search;
} else {
delete queryParams.search;
}
updateHistory({
url: setUrlParams(queryParams, window.location.href, true),
title: document.title,
replace: true,
});
statusChanged({ filters, status }) {
this.statusFilter = filters;
this.filteredByStatus = status;
},
filtersChanged({ searchTerm, authorUsername, assigneeUsername }) {
this.searchTerm = searchTerm;
this.authorUsername = authorUsername;
this.assigneeUsername = assigneeUsername;
},
errorAlertDismissed() {
this.isErrorAlertDismissed = true;
},
},
};
</script>
<template>
<div class="incident-management-list">
<gl-alert v-if="showErrorMsg" variant="danger" @dismiss="isErrorAlertDismissed = true">
{{ $options.i18n.errorMsg }}
</gl-alert>
<div
class="incident-management-list-header gl-display-flex gl-justify-content-space-between gl-border-b-solid gl-border-b-1 gl-border-gray-100"
>
<gl-tabs content-class="gl-p-0" @input="filterIncidentsByStatus">
<gl-tab v-for="tab in $options.statusTabs" :key="tab.status" :data-testid="tab.status">
<template #title>
<span>{{ tab.title }}</span>
<gl-badge v-if="incidentsCount" pill size="sm" class="gl-tab-counter-badge">
{{ incidentsCount[tab.status.toLowerCase()] }}
</gl-badge>
</template>
</gl-tab>
</gl-tabs>
<gl-button
v-if="!isEmpty || activeClosedTabHasNoIncidents"
class="gl-my-3 gl-mr-5 create-incident-button"
data-testid="createIncidentBtn"
data-qa-selector="create_incident_button"
:loading="redirecting"
:disabled="redirecting"
category="primary"
variant="success"
:href="newIncidentPath"
@click="navigateToCreateNewIncident"
>
{{ $options.i18n.createIncidentBtnLabel }}
</gl-button>
</div>
<div class="filtered-search-wrapper">
<filtered-search-bar
:namespace="projectPath"
:search-input-placeholder="$options.i18n.searchPlaceholder"
:tokens="filteredSearchTokens"
:initial-filter-value="filteredSearchValue"
initial-sortby="created_desc"
recent-searches-storage-key="incidents"
class="row-content-block"
@onFilter="handleFilterIncidents"
/>
</div>
<h4 class="gl-display-block d-md-none my-3">
{{ s__('IncidentManagement|Incidents') }}
</h4>
<gl-table
v-if="showList"
<div>
<paginated-table-with-search-and-tabs
:show-items="showList"
:show-error-msg="showErrorMsg"
:i18n="$options.i18n"
:items="incidents.list || []"
:fields="availableFields"
:show-empty="true"
:busy="loading"
stacked="md"
:tbody-tr-class="tbodyTrClass"
:no-local-sorting="true"
:sort-direction="'desc'"
:sort-desc.sync="sortDesc"
:sort-by.sync="sortBy"
sort-icon-left
fixed
@row-clicked="navigateToIncidentDetails"
@sort-changed="fetchSortedData"
:page-info="incidents.pageInfo"
:items-count="incidentsCount"
:status-tabs="$options.statusTabs"
:track-views-options="$options.trackIncidentListViewsOptions"
filter-search-key="incidents"
@page-changed="pageChanged"
@tabs-changed="statusChanged"
@filters-changed="filtersChanged"
@error-alert-dismissed="errorAlertDismissed"
>
<template #cell(severity)="{ item }">
<severity-token :severity="getSeverity(item.severity)" />
<template #header-actions>
<gl-button
v-if="!isEmpty || activeClosedTabHasNoIncidents"
class="gl-my-3 gl-mr-5 create-incident-button"
data-testid="createIncidentBtn"
data-qa-selector="create_incident_button"
:loading="redirecting"
:disabled="redirecting"
category="primary"
variant="success"
:href="newIncidentPath"
@click="redirecting = true"
>
{{ $options.i18n.createIncidentBtnLabel }}
</gl-button>
</template>
<template #cell(title)="{ item }">
<div :class="{ 'gl-display-flex gl-align-items-center': item.state === 'closed' }">
<div class="gl-max-w-full text-truncate" :title="item.title">{{ item.title }}</div>
<gl-icon
v-if="item.state === 'closed'"
name="issue-close"
class="gl-mx-1 gl-fill-blue-500 gl-flex-shrink-0"
:size="16"
data-testid="incident-closed"
/>
</div>
<template #title>
{{ s__('IncidentManagement|Incidents') }}
</template>
<template #cell(createdAt)="{ item }">
<time-ago-tooltip :time="item.createdAt" />
</template>
<template #table>
<gl-table
:items="incidents.list || []"
:fields="availableFields"
:show-empty="true"
:busy="loading"
stacked="md"
:tbody-tr-class="tbodyTrClass"
:no-local-sorting="true"
:sort-direction="'desc'"
:sort-desc.sync="sortDesc"
:sort-by.sync="sortBy"
sort-icon-left
fixed
@row-clicked="navigateToIncidentDetails"
@sort-changed="fetchSortedData"
>
<template #cell(severity)="{ item }">
<severity-token :severity="getSeverity(item.severity)" />
</template>
<template v-if="slaFeatureAvailable" #cell(incidentSla)="{ item }">
<service-level-agreement-cell :sla-due-at="item.slaDueAt" data-testid="incident-sla" />
</template>
<template #cell(title)="{ item }">
<div :class="{ 'gl-display-flex gl-align-items-center': item.state === 'closed' }">
<div class="gl-max-w-full text-truncate" :title="item.title">{{ item.title }}</div>
<gl-icon
v-if="item.state === 'closed'"
name="issue-close"
class="gl-mx-1 gl-fill-blue-500 gl-flex-shrink-0"
:size="16"
data-testid="incident-closed"
/>
</div>
</template>
<template #cell(assignees)="{ item }">
<div data-testid="incident-assignees">
<template v-if="hasAssignees(item.assignees)">
<gl-avatars-inline
:avatars="item.assignees.nodes"
:collapsed="true"
:max-visible="4"
:avatar-size="24"
badge-tooltip-prop="name"
:badge-tooltip-max-chars="100"
>
<template #avatar="{ avatar }">
<gl-avatar-link
:key="avatar.username"
v-gl-tooltip
target="_blank"
:href="avatar.webUrl"
:title="avatar.name"
<template #cell(createdAt)="{ item }">
<time-ago-tooltip :time="item.createdAt" />
</template>
<template v-if="slaFeatureAvailable" #cell(incidentSla)="{ item }">
<service-level-agreement-cell :sla-due-at="item.slaDueAt" data-testid="incident-sla" />
</template>
<template #cell(assignees)="{ item }">
<div data-testid="incident-assignees">
<template v-if="hasAssignees(item.assignees)">
<gl-avatars-inline
:avatars="item.assignees.nodes"
:collapsed="true"
:max-visible="4"
:avatar-size="24"
badge-tooltip-prop="name"
:badge-tooltip-max-chars="100"
>
<gl-avatar :src="avatar.avatarUrl" :label="avatar.name" :size="24" />
</gl-avatar-link>
<template #avatar="{ avatar }">
<gl-avatar-link
:key="avatar.username"
v-gl-tooltip
target="_blank"
:href="avatar.webUrl"
:title="avatar.name"
>
<gl-avatar :src="avatar.avatarUrl" :label="avatar.name" :size="24" />
</gl-avatar-link>
</template>
</gl-avatars-inline>
</template>
</gl-avatars-inline>
<template v-else>
{{ $options.i18n.unassigned }}
</template>
</div>
</template>
<template v-else>
{{ $options.i18n.unassigned }}
</template>
</div>
</template>
<template v-if="publishedAvailable" #cell(published)="{ item }">
<published-cell
:status-page-published-incident="item.statusPagePublishedIncident"
:un-published="$options.i18n.unPublished"
<template v-if="publishedAvailable" #cell(published)="{ item }">
<published-cell
:status-page-published-incident="item.statusPagePublishedIncident"
:un-published="$options.i18n.unPublished"
/>
</template>
<template #table-busy>
<gl-loading-icon size="lg" color="dark" class="mt-3" />
</template>
<template v-if="errored" #empty>
{{ $options.i18n.noIncidents }}
</template>
</gl-table>
</template>
<template #emtpy-state>
<gl-empty-state
:title="emptyStateData.title"
:svg-path="emptyListSvgPath"
:description="emptyStateData.description"
:primary-button-link="emptyStateData.btnLink"
:primary-button-text="emptyStateData.btnText"
/>
</template>
<template #table-busy>
<gl-loading-icon size="lg" color="dark" class="mt-3" />
</template>
<template v-if="errored" #empty>
{{ $options.i18n.noIncidents }}
</template>
</gl-table>
<gl-empty-state
v-else
:title="emptyStateData.title"
:svg-path="emptyListSvgPath"
:description="emptyStateData.description"
:primary-button-link="emptyStateData.btnLink"
:primary-button-text="emptyStateData.btnText"
/>
<gl-pagination
v-if="showPaginationControls"
:value="pagination.currentPage"
:prev-page="prevPage"
:next-page="nextPage"
align="center"
class="gl-pagination gl-mt-3"
@input="handlePageChange"
/>
</paginated-table-with-search-and-tabs>
</div>
</template>

View file

@ -1,5 +1,5 @@
/* eslint-disable @gitlab/require-i18n-strings */
import { s__, __ } from '~/locale';
import { s__ } from '~/locale';
export const I18N = {
errorMsg: s__('IncidentManagement|There was an error displaying the incidents.'),
@ -7,7 +7,6 @@ export const I18N = {
unassigned: s__('IncidentManagement|Unassigned'),
createIncidentBtnLabel: s__('IncidentManagement|Create incident'),
unPublished: s__('IncidentManagement|Unpublished'),
searchPlaceholder: __('Search or filter results…'),
emptyState: {
title: s__('IncidentManagement|Display your incidents in a dedicated view'),
emptyClosedTabTitle: s__('IncidentManagement|There are no closed incidents'),
@ -43,6 +42,14 @@ export const trackIncidentCreateNewOptions = {
action: 'create_incident_button_clicks',
};
/**
* Tracks snowplow event when user views incident list
*/
export const trackIncidentListViewsOptions = {
category: 'Incident Management',
action: 'view_incidents_list',
};
export const DEFAULT_PAGE_SIZE = 20;
export const TH_CREATED_AT_TEST_ID = { 'data-testid': 'incident-management-created-at-sort' };
export const TH_SEVERITY_TEST_ID = { 'data-testid': 'incident-management-severity-sort' };

View file

@ -3,14 +3,14 @@ query getIncidentsCountByStatus(
$projectPath: ID!
$issueTypes: [IssueType!]
$authorUsername: String = ""
$assigneeUsernames: String = ""
$assigneeUsername: String = ""
) {
project(fullPath: $projectPath) {
issueStatusCounts(
search: $searchTerm
types: $issueTypes
authorUsername: $authorUsername
assigneeUsername: $assigneeUsernames
assigneeUsername: $assigneeUsername
) {
all
opened

View file

@ -11,7 +11,7 @@ query getIncidents(
$nextPageCursor: String = ""
$searchTerm: String = ""
$authorUsername: String = ""
$assigneeUsernames: String = ""
$assigneeUsername: String = ""
) {
project(fullPath: $projectPath) {
issues(
@ -20,7 +20,7 @@ query getIncidents(
sort: $sort
state: $status
authorUsername: $authorUsername
assigneeUsername: $assigneeUsernames
assigneeUsername: $assigneeUsername
first: $firstPageSize
last: $lastPageSize
after: $nextPageCursor

View file

@ -18,8 +18,8 @@ export default () => {
publishedAvailable,
emptyListSvgPath,
textQuery,
authorUsernamesQuery,
assigneeUsernamesQuery,
authorUsernameQuery,
assigneeUsernameQuery,
slaFeatureAvailable,
} = domEl.dataset;
@ -38,8 +38,8 @@ export default () => {
publishedAvailable: parseBoolean(publishedAvailable),
emptyListSvgPath,
textQuery,
authorUsernamesQuery,
assigneeUsernamesQuery,
authorUsernameQuery,
assigneeUsernameQuery,
slaFeatureAvailable: parseBoolean(slaFeatureAvailable),
},
apolloProvider,

View file

@ -124,7 +124,6 @@ export default {
class="col-8 col-md-9 gl-p-0"
:label="$options.i18n.webhookUrl.label"
label-for="url"
label-class="label-bold"
>
<gl-form-input-group id="url" data-testid="webhook-url" readonly :value="webhookUrl">
<template #append>

View file

@ -301,7 +301,7 @@ export default {
"
@hide="resetDropdown"
>
<gl-search-box-by-type v-model.trim="searchTerm" class="gl-m-3" />
<gl-search-box-by-type v-model.trim="searchTerm" />
<gl-loading-icon v-if="isFetching" />

View file

@ -205,7 +205,6 @@ export default {
<gl-search-box-by-type
ref="searchBox"
v-model.trim="searchQuery"
class="gl-m-3"
:placeholder="this.$options.translations.searchMilestones"
@input="onSearchBoxInput"
@keydown.enter.prevent="onSearchBoxEnter"

View file

@ -192,7 +192,7 @@ export default {
>
<div class="d-flex flex-column overflow-hidden">
<gl-dropdown-section-header>{{ __('Environment') }}</gl-dropdown-section-header>
<gl-search-box-by-type class="gl-m-3" @input="debouncedEnvironmentsSearch" />
<gl-search-box-by-type @input="debouncedEnvironmentsSearch" />
<gl-loading-icon v-if="environmentsLoading" :inline="true" />
<div v-else class="flex-fill overflow-auto">

View file

@ -80,11 +80,7 @@ export default {
>
<div class="d-flex flex-column overflow-hidden">
<gl-dropdown-section-header>{{ __('Dashboard') }}</gl-dropdown-section-header>
<gl-search-box-by-type
ref="monitorDashboardsDropdownSearch"
v-model="searchTerm"
class="gl-m-3"
/>
<gl-search-box-by-type ref="monitorDashboardsDropdownSearch" v-model="searchTerm" />
<div class="flex-fill overflow-auto">
<gl-dropdown-item

View file

@ -237,7 +237,6 @@ export default {
<gl-search-box-by-type
v-model.trim="searchTerm"
:placeholder="__('Search branches and tags')"
class="gl-p-2"
/>
<gl-dropdown-item
v-for="(ref, index) in filteredRefs"

View file

@ -119,7 +119,6 @@ export default {
<gl-dropdown-divider />
<gl-search-box-by-type
v-model.trim="authorInput"
class="gl-m-3"
:placeholder="__('Search')"
@input="searchAuthors"
/>

View file

@ -139,7 +139,6 @@ export default {
<gl-search-box-by-type
ref="searchBox"
v-model.trim="query"
class="gl-m-3"
:placeholder="i18n.searchPlaceholder"
@input="onSearchBoxInput"
@keydown.enter.prevent="onSearchBoxEnter"

View file

@ -123,7 +123,7 @@ export default {
};
</script>
<template>
<div class="form-group file-editor">
<div class="form-group">
<label :for="firstInputId">{{ s__('Snippets|Files') }}</label>
<snippet-blob-edit
v-for="(blobId, index) in blobIds"

View file

@ -42,6 +42,7 @@ const populateUserInfo = user => {
bio: userData.bio,
bioHtml: sanitize(userData.bio_html),
workInformation: userData.work_information,
websiteUrl: userData.website_url,
loaded: true,
});
}

View file

@ -72,12 +72,7 @@ export default {
css-class="deploy-link js-deploy-url inline"
/>
<gl-dropdown size="small" class="js-mr-wigdet-deployment-dropdown">
<gl-search-box-by-type
v-model.trim="searchTerm"
v-autofocusonshow
autofocus
class="gl-m-3"
/>
<gl-search-box-by-type v-model.trim="searchTerm" v-autofocusonshow autofocus />
<gl-dropdown-item
v-for="change in filteredChanges"
:key="change.path"

View file

@ -57,6 +57,9 @@ export default {
fileName(newVal) {
this.editor.updateModelLanguage(newVal);
},
value(newVal) {
this.editor.setValue(newVal);
},
},
mounted() {
this.editor = initEditorLite({
@ -83,9 +86,12 @@ export default {
};
</script>
<template>
<div class="file-content code">
<div id="editor" ref="editor" data-editor-loading @editor-ready="$emit('editor-ready')">
<pre class="editor-loading-content">{{ value }}</pre>
</div>
<div
:id="`editor-lite-${fileGlobalId}`"
ref="editor"
data-editor-loading
@editor-ready="$emit('editor-ready')"
>
<pre class="editor-loading-content">{{ value }}</pre>
</div>
</template>

View file

@ -0,0 +1,21 @@
import { __ } from '~/locale';
export const tdClass =
'table-col gl-display-flex d-md-table-cell gl-align-items-center gl-white-space-nowrap';
export const thClass = 'gl-hover-bg-blue-50';
export const bodyTrClass =
'gl-border-1 gl-border-t-solid gl-border-gray-100 gl-hover-cursor-pointer gl-hover-bg-blue-50 gl-hover-border-b-solid gl-hover-border-blue-200';
export const defaultPageSize = 20;
export const initialPaginationState = {
page: 1,
prevPageCursor: '',
nextPageCursor: '',
firstPageSize: defaultPageSize,
lastPageSize: null,
};
export const defaultI18n = {
searchPlaceholder: __('Search or filter results…'),
};

View file

@ -0,0 +1,313 @@
<script>
import { GlAlert, GlBadge, GlPagination, GlTab, GlTabs } from '@gitlab/ui';
import Api from '~/api';
import Tracking from '~/tracking';
import { __ } from '~/locale';
import { updateHistory, setUrlParams } from '~/lib/utils/url_utility';
import { initialPaginationState, defaultI18n, defaultPageSize } from './constants';
import { isAny } from './utils';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue';
export default {
defaultI18n,
components: {
GlAlert,
GlBadge,
GlPagination,
GlTabs,
GlTab,
FilteredSearchBar,
},
inject: {
projectPath: {
default: '',
},
textQuery: {
default: '',
},
assigneeUsernameQuery: {
default: '',
},
authorUsernameQuery: {
default: '',
},
},
props: {
items: {
type: Array,
required: true,
},
itemsCount: {
type: Object,
required: false,
default: () => {},
},
pageInfo: {
type: Object,
required: false,
default: () => {},
},
statusTabs: {
type: Array,
required: true,
},
showItems: {
type: Boolean,
required: false,
default: true,
},
showErrorMsg: {
type: Boolean,
required: true,
},
trackViewsOptions: {
type: Object,
required: true,
},
i18n: {
type: Object,
required: true,
},
serverErrorMessage: {
type: String,
required: false,
default: '',
},
filterSearchKey: {
type: String,
required: true,
},
filterSearchTokens: {
type: Array,
required: false,
default: () => ['author_username', 'assignee_username'],
},
},
data() {
return {
searchTerm: this.textQuery,
authorUsername: this.authorUsernameQuery,
assigneeUsername: this.assigneeUsernameQuery,
filterParams: {},
pagination: initialPaginationState,
filteredByStatus: '',
statusFilter: '',
};
},
computed: {
defaultTokens() {
return [
{
type: 'author_username',
icon: 'user',
title: __('Author'),
unique: true,
symbol: '@',
token: AuthorToken,
operators: [{ value: '=', description: __('is'), default: 'true' }],
fetchPath: this.projectPath,
fetchAuthors: Api.projectUsers.bind(Api),
},
{
type: 'assignee_username',
icon: 'user',
title: __('Assignee'),
unique: true,
symbol: '@',
token: AuthorToken,
operators: [{ value: '=', description: __('is'), default: 'true' }],
fetchPath: this.projectPath,
fetchAuthors: Api.projectUsers.bind(Api),
},
];
},
filteredSearchTokens() {
return this.defaultTokens.filter(({ type }) => this.filterSearchTokens.includes(type));
},
filteredSearchValue() {
const value = [];
if (this.authorUsername) {
value.push({
type: 'author_username',
value: { data: this.authorUsername },
});
}
if (this.assigneeUsername) {
value.push({
type: 'assignee_username',
value: { data: this.assigneeUsername },
});
}
if (this.searchTerm) {
value.push(this.searchTerm);
}
return value;
},
itemsForCurrentTab() {
return this.itemsCount?.[this.filteredByStatus.toLowerCase()] ?? 0;
},
showPaginationControls() {
return Boolean(this.pageInfo?.hasNextPage || this.pageInfo?.hasPreviousPage);
},
previousPage() {
return Math.max(this.pagination.page - 1, 0);
},
nextPage() {
const nextPage = this.pagination.page + 1;
return nextPage > Math.ceil(this.itemsForCurrentTab / defaultPageSize) ? null : nextPage;
},
},
mounted() {
this.trackPageViews();
},
methods: {
filterItemsByStatus(tabIndex) {
this.resetPagination();
const { filters, status } = this.statusTabs[tabIndex];
this.statusFilter = filters;
this.filteredByStatus = status;
this.$emit('tabs-changed', { filters, status });
},
handlePageChange(page) {
const { startCursor, endCursor } = this.pageInfo;
if (page > this.pagination.page) {
this.pagination = {
...initialPaginationState,
nextPageCursor: endCursor,
page,
};
} else {
this.pagination = {
lastPageSize: defaultPageSize,
firstPageSize: null,
prevPageCursor: startCursor,
nextPageCursor: '',
page,
};
}
this.$emit('page-changed', this.pagination);
},
resetPagination() {
this.pagination = initialPaginationState;
this.$emit('page-changed', this.pagination);
},
handleFilterItems(filters) {
this.resetPagination();
const filterParams = { authorUsername: '', assigneeUsername: '', search: '' };
filters.forEach(filter => {
if (typeof filter === 'object') {
switch (filter.type) {
case 'author_username':
filterParams.authorUsername = isAny(filter.value.data);
break;
case 'assignee_username':
filterParams.assigneeUsername = isAny(filter.value.data);
break;
case 'filtered-search-term':
if (filter.value.data !== '') filterParams.search = filter.value.data;
break;
default:
break;
}
}
});
this.filterParams = filterParams;
this.updateUrl();
this.searchTerm = filterParams?.search;
this.authorUsername = filterParams?.authorUsername;
this.assigneeUsername = filterParams?.assigneeUsername;
this.$emit('filters-changed', {
searchTerm: this.searchTerm,
authorUsername: this.authorUsername,
assigneeUsername: this.assigneeUsername,
});
},
updateUrl() {
const { authorUsername, assigneeUsername, search } = this.filterParams || {};
const params = {
...(authorUsername !== '' && { author_username: authorUsername }),
...(assigneeUsername !== '' && { assignee_username: assigneeUsername }),
...(search !== '' && { search }),
};
updateHistory({
url: setUrlParams(params, window.location.href, true),
title: document.title,
replace: true,
});
},
trackPageViews() {
const { category, action } = this.trackViewsOptions;
Tracking.event(category, action);
},
},
};
</script>
<template>
<div class="incident-management-list">
<gl-alert v-if="showErrorMsg" variant="danger" @dismiss="$emit('error-alert-dismissed')">
<!-- eslint-disable-next-line vue/no-v-html -->
<p v-html="serverErrorMessage || i18n.errorMsg"></p>
</gl-alert>
<div
class="list-header gl-display-flex gl-justify-content-space-between gl-border-b-solid gl-border-b-1 gl-border-gray-100"
>
<gl-tabs content-class="gl-p-0" @input="filterItemsByStatus">
<gl-tab v-for="tab in statusTabs" :key="tab.status" :data-testid="tab.status">
<template #title>
<span>{{ tab.title }}</span>
<gl-badge v-if="itemsCount" pill size="sm" class="gl-tab-counter-badge">
{{ itemsCount[tab.status.toLowerCase()] }}
</gl-badge>
</template>
</gl-tab>
</gl-tabs>
<slot name="header-actions"></slot>
</div>
<div class="filtered-search-wrapper">
<filtered-search-bar
:namespace="projectPath"
:search-input-placeholder="$options.defaultI18n.searchPlaceholder"
:tokens="filteredSearchTokens"
:initial-filter-value="filteredSearchValue"
initial-sortby="created_desc"
:recent-searches-storage-key="filterSearchKey"
class="row-content-block"
@onFilter="handleFilterItems"
/>
</div>
<h4 class="gl-display-block d-md-none my-3">
<slot name="title"></slot>
</h4>
<slot v-if="showItems" name="table"></slot>
<gl-pagination
v-if="showPaginationControls"
:value="pagination.page"
:prev-page="previousPage"
:next-page="nextPage"
align="center"
class="gl-pagination gl-mt-3"
@input="handlePageChange"
/>
<slot v-if="!showItems" name="emtpy-state"></slot>
</div>
</template>

View file

@ -0,0 +1,11 @@
import { __ } from '~/locale';
/**
* Return a empty string when passed a value of 'Any'
*
* @param {String} value
* @returns {String}
*/
export const isAny = value => {
return value === __('Any') ? '' : value;
};

View file

@ -82,7 +82,7 @@ export default {
<gl-icon name="chevron-down" />
</template>
<gl-search-box-by-type v-model.trim="searchTerm" v-autofocusonshow autofocus class="gl-m-3" />
<gl-search-box-by-type v-model.trim="searchTerm" v-autofocusonshow autofocus />
<gl-deprecated-dropdown-item
v-for="timezone in filteredResults"
:key="timezone.formattedTimezone"

View file

@ -1,16 +1,27 @@
<script>
/* eslint-disable vue/no-v-html */
import { GlPopover, GlDeprecatedSkeletonLoading as GlSkeletonLoading, GlIcon } from '@gitlab/ui';
import {
GlPopover,
GlLink,
GlDeprecatedSkeletonLoading as GlSkeletonLoading,
GlIcon,
} from '@gitlab/ui';
import UserAvatarImage from '../user_avatar/user_avatar_image.vue';
import { glEmojiTag } from '../../../emoji';
const MAX_SKELETON_LINES = 4;
const SECURITY_BOT_USER_DATA = {
username: 'GitLab-Security-Bot',
name: 'GitLab Security Bot',
};
export default {
name: 'UserPopover',
maxSkeletonLines: MAX_SKELETON_LINES,
components: {
GlIcon,
GlLink,
GlPopover,
GlSkeletonLoading,
UserAvatarImage,
@ -43,6 +54,15 @@ export default {
userIsLoading() {
return !this.user?.loaded;
},
isSecurityBot() {
const { username, name, websiteUrl = '' } = this.user;
return (
gon.features?.securityAutoFix &&
username === SECURITY_BOT_USER_DATA.username &&
name === SECURITY_BOT_USER_DATA.name &&
websiteUrl.length
);
},
},
};
</script>
@ -89,6 +109,12 @@ export default {
<div v-if="statusHtml" class="js-user-status gl-mt-3">
<span v-html="statusHtml"></span>
</div>
<div v-if="isSecurityBot" class="gl-text-blue-500">
<gl-icon name="question" />
<gl-link data-testid="user-popover-bot-docs-link" :href="user.websiteUrl">
{{ sprintf(__('Learn more about %{username}'), { username: user.name }) }}
</gl-link>
</div>
</template>
</div>
</div>

View file

@ -324,15 +324,8 @@ img.emoji {
}
.project-item-select-holder {
display: inline-block;
position: relative;
.project-item-select {
position: absolute;
top: 0;
right: 0;
min-width: 250px;
visibility: hidden;
}
}

View file

@ -1,5 +1,3 @@
.monaco-editor.gl-editor-lite {
.line-numbers {
@include gl-pt-0;
}
[id^='editor-lite-'] {
height: 500px;
}

View file

@ -376,33 +376,11 @@
}
.project-item-select-holder.btn-group {
display: flex;
overflow: hidden;
float: right;
.new-project-item-link {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.new-project-item-select-button {
width: 32px;
max-width: 44px;
}
}
.empty-state .project-item-select-holder.btn-group {
float: none;
justify-content: center;
.btn {
// overrides styles applied to plain `.empty-state .btn`
margin: 10px 0;
max-width: 300px;
width: auto;
@include media-breakpoint-down(xs) {
max-width: 250px;
}
}
max-width: 320px;
}

View file

@ -8,7 +8,7 @@
@include gl-text-gray-500;
tbody {
tr {
tr:not(.b-table-busy-slot) {
// TODO replace with gitlab/ui utilities: https://gitlab.com/gitlab-org/gitlab-ui/-/merge_requests/1791
&:hover {
border-top-style: double;
@ -132,7 +132,7 @@
}
@include media-breakpoint-down(xs) {
.incident-management-list-header {
.list-header {
flex-direction: column-reverse;
}

View file

@ -6,6 +6,6 @@ module ShowInheritedLabelsChecker
private
def show_inherited_labels?(include_ancestor_groups)
Feature.enabled?(:show_inherited_labels, @project || @group) || include_ancestor_groups # rubocop:disable Gitlab/ModuleWithInstanceVariables
Feature.enabled?(:show_inherited_labels, @project || @group, default_enabled: true) || include_ancestor_groups # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
end

View file

@ -18,7 +18,10 @@ module BoardsHelper
time_tracking_limit_to_hours: Gitlab::CurrentSettings.time_tracking_limit_to_hours.to_s,
recent_boards_endpoint: recent_boards_path,
parent: current_board_parent.model_name.param_key,
group_id: @group&.id
group_id: @group&.id,
labels_filter_base_path: build_issue_link_base,
labels_fetch_path: labels_fetch_path,
labels_manage_path: labels_manage_path
}
end
@ -38,6 +41,22 @@ module BoardsHelper
end
end
def labels_fetch_path
if board.group_board?
group_labels_path(@group, format: :json, only_group_labels: true, include_ancestor_groups: true)
else
project_labels_path(@project, format: :json, include_ancestor_groups: true)
end
end
def labels_manage_path
if board.group_board?
group_labels_path(@group)
else
project_labels_path(@project)
end
end
def board_base_url
if board.group_board?
group_boards_url(@group)

View file

@ -264,6 +264,10 @@ module LabelsHelper
['issues', 'merge requests']
end
def show_labels_full_path?(project, group)
project || group&.subgroup?
end
private
def render_label_link(label_html, link:, title:, dataset:)

View file

@ -9,7 +9,9 @@ module Projects::AlertManagementHelper
'populating-alerts-help-url' => help_page_url('operations/incident_management/index.md', anchor: 'enable-alert-management'),
'empty-alert-svg-path' => image_path('illustrations/alert-management-empty-state.svg'),
'user-can-enable-alert-management' => can?(current_user, :admin_operations, project).to_s,
'alert-management-enabled' => alert_management_enabled?(project).to_s
'alert-management-enabled' => alert_management_enabled?(project).to_s,
'text-query': params[:search],
'assignee-username-query': params[:assignee_username]
}
end

View file

@ -10,8 +10,8 @@ module Projects::IncidentsHelper
'issue-path' => project_issues_path(project),
'empty-list-svg-path' => image_path('illustrations/incident-empty-state.svg'),
'text-query': params[:search],
'author-usernames-query': params[:author_username],
'assignee-usernames-query': params[:assignee_username]
'author-username-query': params[:author_username],
'assignee-username-query': params[:assignee_username]
}
end
end

View file

@ -14,8 +14,8 @@ module Ci
Gitlab::Ci::Pipeline::Chain::Config::Process,
Gitlab::Ci::Pipeline::Chain::RemoveUnwantedChatJobs,
Gitlab::Ci::Pipeline::Chain::Skip,
Gitlab::Ci::Pipeline::Chain::EvaluateWorkflowRules,
Gitlab::Ci::Pipeline::Chain::Seed,
Gitlab::Ci::Pipeline::Chain::EvaluateWorkflowRules,
Gitlab::Ci::Pipeline::Chain::Limit::Size,
Gitlab::Ci::Pipeline::Chain::Validate::External,
Gitlab::Ci::Pipeline::Chain::Populate,

View file

@ -15,3 +15,5 @@
.row-second-line.str-truncated-100
= mail_to user.email, user.email, class: 'text-secondary'
- unless Feature.disabled?(:security_auto_fix) || !user.internal? || user.website_url.blank?
= link_to "(#{_('more information')})", user.website_url

View file

@ -22,13 +22,13 @@
.header-action-buttons
- if defined?(@notes_count) && @notes_count > 0
%span.btn.disabled.btn-grouped.d-none.d-sm-block.gl-mr-3.has-tooltip{ title: n_("%d comment on this commit", "%d comments on this commit", @notes_count) % @notes_count }
%span.btn.disabled.gl-button.btn-icon.d-none.d-sm-inline.gl-mr-3.has-tooltip{ title: n_("%d comment on this commit", "%d comments on this commit", @notes_count) % @notes_count }
= sprite_icon('comment')
= @notes_count
= link_to project_tree_path(@project, @commit), class: "btn btn-default gl-mr-3 d-none d-md-inline" do
= link_to project_tree_path(@project, @commit), class: "btn gl-button gl-mr-3 d-none d-md-inline" do
#{ _('Browse files') }
.dropdown.inline
%a.btn.btn-default.dropdown-toggle.qa-options-button.d-md-inline{ data: { toggle: "dropdown" } }
%a.btn.gl-button.dropdown-toggle.qa-options-button.d-md-inline{ data: { toggle: "dropdown" } }
%span= _('Options')
= icon('caret-down')
%ul.dropdown-menu.dropdown-menu-right

View file

@ -10,7 +10,7 @@
.description-text.gl-flex-grow-1.gl-overflow-hidden
- if label.description.present?
= markdown_field(label, :description)
- elsif @project
- elsif show_labels_full_path?(@project, @group)
= render 'shared/label_full_path', label: label
%ul.label-links.gl-m-0.gl-p-0.gl-white-space-nowrap
- if show_label_issues_link
@ -25,6 +25,6 @@
&middot;
%li.js-priority-badge.inline.gl-ml-3
.label-badge.gl-bg-blue-50= _('Prioritized label')
- if @project && label.description.present?
- if label.description.present? && show_labels_full_path?(@project, @group)
.gl-mt-3
= render 'shared/label_full_path', label: label

View file

@ -1,7 +1,7 @@
- if any_projects?(@projects)
.project-item-select-holder.btn-group
%a.btn.btn-success.new-project-item-link.qa-new-project-item-link{ href: '', data: { label: local_assigns[:label], type: local_assigns[:type] } }
.project-item-select-holder.btn-group.gl-ml-auto.gl-mr-auto.gl-py-3.gl-relative.gl-display-flex.gl-overflow-hidden
%a.btn.gl-button.btn-success.new-project-item-link.block-truncated.qa-new-project-item-link{ href: '', data: { label: local_assigns[:label], type: local_assigns[:type] }, class: "gl-m-0!" }
= loading_icon(color: 'light')
= project_select_tag :project_path, class: "project-item-select", data: { include_groups: local_assigns[:include_groups], order_by: 'last_activity_at', relative_path: local_assigns[:path], with_shared: local_assigns[:with_shared], include_projects_in_subgroups: local_assigns[:include_projects_in_subgroups] }, with_feature_enabled: local_assigns[:with_feature_enabled]
%button.btn.btn-success.new-project-item-select-button.qa-new-project-item-select-button.gl-p-0
= project_select_tag :project_path, class: "project-item-select gl-absolute gl-visibility-hidden", data: { include_groups: local_assigns[:include_groups], order_by: 'last_activity_at', relative_path: local_assigns[:path], with_shared: local_assigns[:with_shared], include_projects_in_subgroups: local_assigns[:include_projects_in_subgroups] }, with_feature_enabled: local_assigns[:with_feature_enabled]
%button.btn.dropdown-toggle.btn-success.btn-md.gl-button.gl-dropdown-toggle.dropdown-toggle-split.new-project-item-select-button.qa-new-project-item-select-button.gl-p-0.gl-w-100{ class: "gl-m-0!", 'aria-label': _('Toggle project select') }
= sprite_icon('chevron-down')

View file

@ -1,3 +1,5 @@
- activity_pane_class = Feature.enabled?(:security_auto_fix) && @user.bot? ? "col-12" : "col-md-12 col-lg-6"
.row
.col-12
.calendar-block.gl-mt-3.gl-mb-3
@ -6,25 +8,26 @@
.spinner.spinner-md
.user-calendar-activities.d-none.d-sm-block
.row
.col-md-12.col-lg-6
%div{ class: activity_pane_class }
- if can?(current_user, :read_cross_project)
.activities-block
.gl-mt-5
.d-flex.align-items-center.border-bottom
%h4.flex-grow
= s_('UserProfile|Activity')
.gl-display-flex.gl-align-items-center.gl-border-b-1.gl-border-b-gray-100.gl-border-b-solid
%h4.gl-flex-grow-1
= Feature.enabled?(:security_auto_fix) && @user.bot? ? s_('UserProfile|Bot activity') : s_('UserProfile|Activity')
= link_to s_('UserProfile|View all'), user_activity_path, class: "hide js-view-all"
.overview-content-list{ data: { href: user_path } }
.center.light.loading
.spinner.spinner-md
.col-md-12.col-lg-6
.projects-block
.gl-mt-5
.d-flex.align-items-center.border-bottom
%h4.flex-grow
= s_('UserProfile|Personal projects')
= link_to s_('UserProfile|View all'), user_projects_path, class: "hide js-view-all"
.overview-content-list{ data: { href: user_projects_path } }
.center.light.loading
.spinner.spinner-md
- unless Feature.enabled?(:security_auto_fix) && @user.bot?
.col-md-12.col-lg-6
.projects-block
.gl-mt-5
.gl-display-flex.gl-align-items-center.gl-border-b-1.gl-border-b-gray-100.gl-border-b-solid
%h4.gl-flex-grow-1
= s_('UserProfile|Personal projects')
= link_to s_('UserProfile|View all'), user_projects_path, class: "hide js-view-all"
.overview-content-list{ data: { href: user_projects_path } }
.center.light.loading
.spinner.spinner-md

View file

@ -78,6 +78,8 @@
= sprite_icon('twitter')
- unless @user.website_url.blank?
.profile-link-holder.middle-dot-divider-sm.d-block.d-sm-inline.mt-1.mt-sm-0
- if Feature.enabled?(:security_auto_fix) && @user.bot?
= sprite_icon('question', css_class: 'gl-text-blue-600')
= link_to @user.short_website_url, @user.full_website_url, class: 'text-link', target: '_blank', rel: 'me noopener noreferrer nofollow'
- unless @user.public_email.blank?
.profile-link-holder.middle-dot-divider-sm.d-block.d-sm-inline.mt-1.mt-sm-0
@ -101,26 +103,27 @@
%li.js-activity-tab
= link_to user_activity_path, data: { target: 'div#activity', action: 'activity', toggle: 'tab' } do
= s_('UserProfile|Activity')
- if profile_tab?(:groups)
%li.js-groups-tab
= link_to user_groups_path, data: { target: 'div#groups', action: 'groups', toggle: 'tab', endpoint: user_groups_path(format: :json) } do
= s_('UserProfile|Groups')
- if profile_tab?(:contributed)
%li.js-contributed-tab
= link_to user_contributed_projects_path, data: { target: 'div#contributed', action: 'contributed', toggle: 'tab', endpoint: user_contributed_projects_path(format: :json) } do
= s_('UserProfile|Contributed projects')
- if profile_tab?(:projects)
%li.js-projects-tab
= link_to user_projects_path, data: { target: 'div#projects', action: 'projects', toggle: 'tab', endpoint: user_projects_path(format: :json) } do
= s_('UserProfile|Personal projects')
- if profile_tab?(:starred)
%li.js-starred-tab
= link_to user_starred_projects_path, data: { target: 'div#starred', action: 'starred', toggle: 'tab', endpoint: user_starred_projects_path(format: :json) } do
= s_('UserProfile|Starred projects')
- if profile_tab?(:snippets)
%li.js-snippets-tab
= link_to user_snippets_path, data: { target: 'div#snippets', action: 'snippets', toggle: 'tab', endpoint: user_snippets_path(format: :json) } do
= s_('UserProfile|Snippets')
- unless Feature.enabled?(:security_auto_fix) && @user.bot?
- if profile_tab?(:groups)
%li.js-groups-tab
= link_to user_groups_path, data: { target: 'div#groups', action: 'groups', toggle: 'tab', endpoint: user_groups_path(format: :json) } do
= s_('UserProfile|Groups')
- if profile_tab?(:contributed)
%li.js-contributed-tab
= link_to user_contributed_projects_path, data: { target: 'div#contributed', action: 'contributed', toggle: 'tab', endpoint: user_contributed_projects_path(format: :json) } do
= s_('UserProfile|Contributed projects')
- if profile_tab?(:projects)
%li.js-projects-tab
= link_to user_projects_path, data: { target: 'div#projects', action: 'projects', toggle: 'tab', endpoint: user_projects_path(format: :json) } do
= s_('UserProfile|Personal projects')
- if profile_tab?(:starred)
%li.js-starred-tab
= link_to user_starred_projects_path, data: { target: 'div#starred', action: 'starred', toggle: 'tab', endpoint: user_starred_projects_path(format: :json) } do
= s_('UserProfile|Starred projects')
- if profile_tab?(:snippets)
%li.js-snippets-tab
= link_to user_snippets_path, data: { target: 'div#snippets', action: 'snippets', toggle: 'tab', endpoint: user_snippets_path(format: :json) } do
= s_('UserProfile|Snippets')
%div{ class: container_class }
.tab-content
@ -136,26 +139,26 @@
.content_list{ data: { href: user_path } }
.loading
.spinner.spinner-md
- unless @user.bot?
- if profile_tab?(:groups)
#groups.tab-pane
-# This tab is always loaded via AJAX
- if profile_tab?(:groups)
#groups.tab-pane
-# This tab is always loaded via AJAX
- if profile_tab?(:contributed)
#contributed.tab-pane
-# This tab is always loaded via AJAX
- if profile_tab?(:contributed)
#contributed.tab-pane
-# This tab is always loaded via AJAX
- if profile_tab?(:projects)
#projects.tab-pane
-# This tab is always loaded via AJAX
- if profile_tab?(:projects)
#projects.tab-pane
-# This tab is always loaded via AJAX
- if profile_tab?(:starred)
#starred.tab-pane
-# This tab is always loaded via AJAX
- if profile_tab?(:starred)
#starred.tab-pane
-# This tab is always loaded via AJAX
- if profile_tab?(:snippets)
#snippets.tab-pane
-# This tab is always loaded via AJAX
- if profile_tab?(:snippets)
#snippets.tab-pane
-# This tab is always loaded via AJAX
.loading.hide
.spinner.spinner-md

View file

@ -0,0 +1,5 @@
---
title: Fix workflow:rules not accessing passed-upstream and trigger variables
merge_request: 44935
author:
type: fixed

View file

@ -0,0 +1,5 @@
---
title: Show all inherited labels in projects and subgroups
merge_request: 45161
author:
type: added

View file

@ -0,0 +1,5 @@
---
title: Show origin path of labels on subgroup labels page
merge_request: 45040
author:
type: added

View file

@ -1,5 +0,0 @@
---
title: Fix rollback portion of migration that adds temporary index for container scanning findings
merge_request: 44593
author:
type: fixed

View file

@ -1,5 +0,0 @@
---
title: 'Revert 42465 and 42343: Expanded collapsed diff files'
merge_request: 43361
author:
type: other

View file

@ -1,5 +0,0 @@
---
title: Improve merge error when pre-receive hooks fail in fast-forward merge
merge_request: 44843
author:
type: fixed

View file

@ -49,7 +49,7 @@
- error_tracking
- feature_flags
- foundations
- fuzz-testing
- fuzz_testing
- gdk
- geo_replication
- git_lfs

View file

@ -0,0 +1,7 @@
---
name: security_auto_fix
introduced_by_url:
rollout_issue_url:
group: group::composition analysis
type: development
default_enabled: false

View file

@ -1,7 +1,7 @@
---
name: show_inherited_labels
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/42960
rollout_issue_url:
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/267547
group: group::project management
type: development
default_enabled: false
default_enabled: true

View file

@ -61,6 +61,7 @@ buildpacks
bundler
bundlers
burndown
burnup
cacheable
CAS
CentOS

View file

@ -374,7 +374,7 @@ default artifacts expiration setting, which you can find in the [CI/CD Admin set
> Introduced in GitLab 10.3.
To disable [the dependencies validation](../ci/yaml/README.md#when-a-dependent-job-will-fail),
To disable [the dependencies validation](../ci/yaml/README.md#when-a-dependent-job-fails),
you can enable the `ci_disable_validates_dependencies` feature flag from a Rails console.
**In Omnibus installations:**

View file

@ -181,7 +181,7 @@ To enable or disable the inheritance of all `variables:` or `default:` parameter
- `variables: true` or `variables: false`
To inherit only a subset of `default:` parameters or `variables:`, specify what
you wish to inherit, and any not listed will **not** be inherited. Use
you wish to inherit. Anything not listed is **not** inherited. Use
one of the following formats:
```yaml
@ -344,9 +344,9 @@ workflow:
This example never allows pipelines for schedules or `push` (branches and tags) pipelines,
but does allow pipelines in **all** other cases, *including* merge request pipelines.
As with `rules` defined in jobs, be careful not to use a configuration that allows
merge request pipelines and branch pipelines to run at the same time, or you could
have [duplicate pipelines](#prevent-duplicate-pipelines).
Be careful not to use a configuration that might run
merge request pipelines and branch pipelines at the same time. As with `rules` defined in jobs,
it can cause [duplicate pipelines](#prevent-duplicate-pipelines).
#### `workflow:rules` templates
@ -358,9 +358,9 @@ for common scenarios. These templates help prevent duplicate pipelines.
The [`Branch-Pipelines` template](https://gitlab.com/gitlab-org/gitlab/-/tree/master/lib/gitlab/ci/templates/Workflows/Branch-Pipelines.gitlab-ci.yml)
makes your pipelines run for branches and tags.
Branch pipeline status is displayed within merge requests that use that branch
as a source, but this pipeline type does not support any features offered by
[Merge Request Pipelines](../merge_request_pipelines/) like
Branch pipeline status is displayed within merge requests that use the branch
as a source. However, this pipeline type does not support any features offered by
[Merge Request Pipelines](../merge_request_pipelines/), like
[Pipelines for Merge Results](../merge_request_pipelines/#pipelines-for-merged-results)
or [Merge Trains](../merge_request_pipelines/pipelines_for_merged_results/merge_trains/).
Use this template if you are intentionally avoiding those features.
@ -1529,7 +1529,7 @@ docker build:
script: docker build -t my-image:$CI_COMMIT_REF_SLUG .
rules:
- if: '$VAR == "string value"'
changes: # Will include the job and set to when:manual if any of the follow paths match a modified file.
changes: # Include the job and set to when:manual if any of the follow paths match a modified file.
- Dockerfile
- docker/scripts/*
when: manual
@ -1730,7 +1730,7 @@ the pipeline if the following is true:
- `(any listed refs are true) AND (any listed variables are true) AND (any listed changes are true) AND (any chosen Kubernetes status matches)`
In the example below, the `test` job will `only` be created when **all** of the following are true:
In the example below, the `test` job is `only` created when **all** of the following are true:
- The pipeline has been [scheduled](../pipelines/schedules.md) **or** runs for `master`.
- The `variables` keyword matches.
@ -1889,7 +1889,7 @@ the `docker build` job is created, but only if changes were made to any of the f
CAUTION: **Warning:**
If you use `only:changes` with [only allow merge requests to be merged if the pipeline succeeds](../../user/project/merge_requests/merge_when_pipeline_succeeds.md#only-allow-merge-requests-to-be-merged-if-the-pipeline-succeeds),
undesired behavior can result if you don't [also use `only:merge_requests`](#using-onlychanges-with-pipelines-for-merge-requests).
you should [also use `only:merge_requests`](#using-onlychanges-with-pipelines-for-merge-requests). Otherwise it may not work as expected.
You can also use glob patterns to match multiple files in either the root directory
of the repository, or in _any_ directory within the repository. However, they must be wrapped
@ -1949,13 +1949,9 @@ docker build service one:
- service-one/**/*
```
In the scenario above, if a merge request is created or updated that changes
either files in `service-one` directory or the `Dockerfile`, GitLab creates
and triggers the `docker build service one` job.
Note that if [pipelines for merge requests](../merge_request_pipelines/index.md) is
combined with `only: [change]`, but `only: [merge_requests]` is omitted, there could be
unwanted behavior.
In this scenario, if a merge request changes
files in the `service-one` directory or the `Dockerfile`, GitLab creates
the `docker build service one` job.
For example:
@ -2776,8 +2772,8 @@ The cache is shared between jobs, so if you're using different
paths for different jobs, you should also set a different `cache:key`.
Otherwise cache content can be overwritten.
The `key` parameter defines the affinity of caching between jobs,
to have a single cache for all jobs, cache per-job, cache per-branch
The `key` parameter defines the affinity of caching between jobs.
You can have a single cache for all jobs, cache per-job, cache per-branch,
or any other way that fits your workflow. This way, you can fine tune caching,
including caching data between different jobs or even different branches.
@ -3381,7 +3377,7 @@ deploy:
script: make deploy
```
##### When a dependent job will fail
##### When a dependent job fails
> Introduced in GitLab 10.3.
@ -3621,6 +3617,10 @@ Job naming style [was improved](https://gitlab.com/gitlab-org/gitlab/-/issues/23
Use `trigger` to define a downstream pipeline trigger. When GitLab starts a job created
with a `trigger` definition, a downstream pipeline is created.
Jobs with `trigger` can only use a [limited set of keywords](../multi_project_pipelines.md#limitations).
For example, you can't run commands with [`script`](#script), [`before_script`](#before_script-and-after_script),
or [`after_script`](#before_script-and-after_script).
You can use this keyword to create two different types of downstream pipelines:
- [Multi-project pipelines](../multi_project_pipelines.md#creating-multi-project-pipelines-from-gitlab-ciyml)
@ -3772,10 +3772,10 @@ starting, which reduces parallelization.
#### Trigger a pipeline by API call
Triggers can be used to force a rebuild of a specific branch, tag or commit,
with an API call when a pipeline gets created using a trigger token.
To force a rebuild of a specific branch, tag, or commit, you can use an API call
with a trigger token.
Not to be confused with the [`trigger`](#trigger) parameter.
The trigger token is different than the [`trigger`](#trigger) parameter.
[Read more in the triggers documentation.](../triggers/README.md)
@ -3818,7 +3818,7 @@ step-2:
step-3:
stage: stage3
script:
- echo "Because step-2 can not be canceled, this step will never be canceled, even though set as interruptible."
- echo "Because step-2 can not be canceled, this step can never be canceled, even though it's set as interruptible."
interruptible: true
```
@ -3837,7 +3837,7 @@ Sometimes running multiple jobs or pipelines at the same time in an environment
can lead to errors during the deployment.
To avoid these errors, the `resource_group` attribute can be used to ensure that
the runner doesn't run certain jobs simultaneously. Resource groups behave similiar
the runner doesn't run certain jobs simultaneously. Resource groups behave similar
to semaphores in other programming languages.
When the `resource_group` key is defined for a job in `.gitlab-ci.yml`,
@ -4003,7 +4003,7 @@ tags. These options cannot be used together, so choose one:
- 'm1'
- 'm2'
- 'm3'
released_at: '2020-07-15T08:00:00Z' # Optional, will auto generate if not defined, or can use a variable.
released_at: '2020-07-15T08:00:00Z' # Optional, is auto generated if not defined, or can use a variable.
```
- To create a release automatically when commits are pushed or merged to the default branch,
@ -4049,7 +4049,7 @@ tags. These options cannot be used together, so choose one:
- 'm1'
- 'm2'
- 'm3'
released_at: '2020-07-15T08:00:00Z' # Optional, will auto generate if not defined, or can use a variable.
released_at: '2020-07-15T08:00:00Z' # Optional, is auto generated if not defined, or can use a variable.
```
#### Release assets as Generic packages

View file

@ -12,7 +12,7 @@ A branch is an independent line of development in a [project](../user/project/in
When you create a new branch (in your [terminal](start-using-git.md) or with
[the web interface](../user/project/repository/web_editor.md#create-a-new-branch)),
you are creating a snapshot of a certain branch, usually the main `master` branch,
at it's current state. From there, you can start to make your own changes without
at its current state. From there, you can start to make your own changes without
affecting the main codebase. The history of your changes will be tracked in your branch.
When your changes are ready, you then merge them into the rest of the codebase with a

View file

@ -67,7 +67,7 @@ The following languages and dependency managers are supported:
| [npm](https://www.npmjs.com/), [yarn](https://classic.yarnpkg.com/en/) | JavaScript | `package-lock.json`, `npm-shrinkwrap.json`, `yarn.lock` | [Gemnasium](https://gitlab.com/gitlab-org/security-products/gemnasium), [Retire.js](https://retirejs.github.io/retire.js/) |
| [NuGet](https://www.nuget.org/) 4.9+ | .NET, C# | [`packages.lock.json`](https://docs.microsoft.com/en-us/nuget/consume-packages/package-references-in-project-files#enabling-lock-file) | [Gemnasium](https://gitlab.com/gitlab-org/security-products/gemnasium) |
| [setuptools](https://setuptools.readthedocs.io/en/latest/), [pip](https://pip.pypa.io/en/stable/), [Pipenv](https://pipenv.pypa.io/en/latest/) | Python | `setup.py`, `requirements.txt`, `requirements.pip`, `requires.txt`, `Pipfile` | [Gemnasium](https://gitlab.com/gitlab-org/security-products/gemnasium) |
| [sbt](https://www.scala-sbt.org/) | Scala | `build.sbt` | [Gemnasium](https://gitlab.com/gitlab-org/security-products/gemnasium) |
| [sbt](https://www.scala-sbt.org/) 1.2 and below ([Ivy](http://ant.apache.org/ivy/)) | Scala | `build.sbt` | [Gemnasium](https://gitlab.com/gitlab-org/security-products/gemnasium) |
Plans are underway for supporting the following languages, dependency managers, and dependency files. For details, see the issue link for each.
@ -75,6 +75,7 @@ Plans are underway for supporting the following languages, dependency managers,
| ------------------- | --------- | --------------- | ------------ |
| [Pipenv](https://pipenv.pypa.io/en/latest/) | Python | `Pipfile.lock` | [Gemnasium](https://gitlab.com/gitlab-org/security-products/gemnasium) | [GitLab#11756](https://gitlab.com/gitlab-org/gitlab/-/issues/11756) |
| [Poetry](https://python-poetry.org/) | Python | `poetry.lock` | [Gemnasium](https://gitlab.com/gitlab-org/security-products/gemnasium) | [GitLab#7006](https://gitlab.com/gitlab-org/gitlab/-/issues/7006) |
| [sbt](https://www.scala-sbt.org/) 1.3+ ([Coursier](https://get-coursier.io/))| Scala | `build.sbt` | [Gemnasium](https://gitlab.com/gitlab-org/security-products/gemnasium) | [GitLab#249526](https://gitlab.com/gitlab-org/gitlab/-/issues/249526) |
## Contribute your scanner

View file

@ -66,7 +66,7 @@ Metrics contain both instance and node labels. The instance label will be deprec
- `container_gpu_allocation` - Average number of GPUs requested over the previous minute.
- `container_memory_allocation_bytes` - Average bytes of RAM requested/used over the previous minute.
- `pod_pvc_allocation` - Bytes provisioned for a PVC attached to a pod.
- `pv_hourly_cost` - Hourly cost per GP on a persistent volume.
- `pv_hourly_cost` - Hourly cost per GB on a persistent volume.
Some examples are provided in the
[`kubecost-cost-model` repository](https://gitlab.com/gitlab-examples/kubecost-cost-model/-/blob/master/PROMETHEUS.md#example-queries).

View file

@ -57,9 +57,11 @@ and edit labels.
### Project labels
> Showing all inherited labels [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/241990) in 13.5.
To view the project labels list, navigate to the project and click **Issues > Labels**.
The list includes all labels that are defined at the project level, as well as all
labels inherited from the immediate parent group.
labels defined by its ancestor groups.
For each label, you can see the project or group path from where it was created.
You can filter the list by entering a search query at the top and clicking search (**{search}**).

View file

@ -0,0 +1,142 @@
---
type: reference
stage: Plan
group: Project Management
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Burndown and burnup charts **(STARTER)**
[Burndown](#burndown-charts) and [burnup](#burnup-charts) charts show the progress of completing a milestone.
![burndown and burnup chart](img/burndown_and_burnup_charts_v13_5.png)
## Burndown charts
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/1540) in [GitLab Starter](https://about.gitlab.com/pricing/) 9.1 for project milestones.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/5354) in [GitLab Premium](https://about.gitlab.com/pricing/) 10.8 for group milestones.
> - [Added](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/6495) to [GitLab Starter](https://about.gitlab.com/pricing/) 11.2 for group milestones.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/6903) [fixed burndown charts](#fixed-burndown-charts) in [GitLab Starter](https://about.gitlab.com/pricing/) 13.5.
Burndown charts show the number of issues over the course of a milestone.
![burndown chart](img/burndown_chart_v13_5.png)
At a glance, you see the current state for the completion a given milestone.
Without them, you would have to organize the data from the milestone and plot it
yourself to have the same sense of progress.
GitLab plots it for you and presents it in a clear and beautiful chart.
<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
For an overview, check the video demonstration on [Mapping work versus time with burndown charts](https://www.youtube.com/watch?v=zJU2MuRChzs).
To view a project's burndown chart:
1. In a project, navigate to **Issues > Milestones**.
1. Select a milestone from the list.
To view a group's burndown chart:
1. In a group, navigate to **Issues > Milestones**.
1. Select a milestone from the list.
### Use cases for burndown charts
Burndown charts are generally used for tracking and analyzing the completion of
a milestone. Therefore, their use cases are tied to the
[use you are assigning your milestone to](index.md).
For example, suppose you lead a team of developers in a large company,
and you follow this workflow:
- Your company set the goal for the quarter to deliver 10 new features for your app
in the upcoming major release.
- You create a milestone, and remind your team to assign that milestone to every new issue
and merge request that's part of the launch of your app.
- Every week, you open the milestone, visualize the progress, identify the gaps,
and help your team to get their work done.
- Every month, you check in with your supervisor, and show the progress of that milestone
from the burndown chart.
- By the end of the quarter, your team successfully delivered 100% of that milestone, as
it was taken care of closely throughout the whole quarter.
### How burndown charts work
A burndown chart is available for every project or group milestone that has been attributed a **start
date** and a **due date**.
NOTE: **Note:**
You're able to [promote project](index.md#promoting-project-milestones-to-group-milestones) to group milestones and still see the **burndown chart** for them, respecting license limitations.
The chart indicates the project's progress throughout that milestone (for issues assigned to it).
In particular, it shows how many issues were or are still open for a given day in the
milestone's corresponding period.
The burndown chart can also be toggled to display the cumulative open issue
weight for a given day. When using this feature, make sure issue weights have
been properly assigned, since an open issue with no weight adds zero to the
cumulative value.
### Fixed burndown charts
For milestones created before GitLab 13.5, burndown charts have an additional toggle to
switch between Legacy and Fixed views.
| Legacy | Fixed |
| ----- | ----- |
| ![Legacy burndown chart, ](img/burndown_chart_legacy_v13_5.png) | ![Fixed burndown chart, showing a jump when a lot of issues were added to the milestone](img/burndown_chart_fixed_v13_5.png) |
**Fixed burndown** charts track the full history of milestone activity, from its creation until the
milestone expires. After the milestone due date passes, issues removed from the milestone no longer
affect the chart.
**Legacy burndown** charts track when issues were created and when they were last closed, not their
full history. For each day, a legacy burndown chart takes the number of open issues and the issues
created that day, and subtracts the number of issues closed that day.
Issues that were created and assigned a milestone before its start date (and remain open as of the
start date) are considered as having been opened on the start date.
Therefore, when the milestone start date is changed, the number of opened issues on each day may
change.
Reopened issues are considered as having been opened on the day after they were last closed.
## Burnup charts
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/6903) in [GitLab Starter](https://about.gitlab.com/pricing/) 13.5.
Burnup charts show the assigned and completed work for a milestone.
![burnup chart](img/burnup_chart_v13_5.png)
To view a project's burnup chart:
1. In a project, navigate to **Issues > Milestones**.
1. Select a milestone from the list.
To view a group's burnup chart:
1. In a group, navigate to **Issues > Milestones**.
1. Select a milestone from the list.
### How burnup charts work
Burnup charts have separate lines for total work and completed work. The total line
shows when scope is reduced or added to a milestone. The completed work is a count
of issues closed.
Burnup charts can show either the total number of issues or total weight for each
day of the milestone. Use the toggle above the charts to switch between total
and weight.
<!-- ## Troubleshooting
Include any troubleshooting steps that you can foresee. If you know beforehand what issues
one might have when setting this up, or when something is changed, or on upgrading, it's
important to describe those, too. Think of things that may go wrong and include them here.
This is important to minimize requests for support, and to avoid doc comments with
questions that you know someone might ask.
Each scenario can be a third-level heading, e.g. `### Getting error message X`.
If you have none to add when creating a doc, leave this section in place
but commented out to help encourage others to add to it in the future. -->

View file

@ -1,87 +1,5 @@
---
type: reference
stage: Plan
group: Project Management
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
redirect_to: './burndown_and_burnup_charts.md'
---
# Burndown Charts **(STARTER)**
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/1540) in [GitLab Starter](https://about.gitlab.com/pricing/) 9.1 for project milestones.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/5354) in [GitLab Premium](https://about.gitlab.com/pricing/) 10.8 for group milestones.
> - [Added](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/6495) to [GitLab Starter](https://about.gitlab.com/pricing/) 11.2 for group milestones.
> - Closed or reopened issues prior to GitLab 9.1 won't have a `closed_at`
> value, so the burndown chart considers them as closed on the milestone
> `start_date`. In that case, a warning will be displayed.
Burndown Charts are visual representations of the progress of completing a milestone.
![burndown chart](img/burndown_chart.png)
At a glance, you see the current state for the completion a given milestone.
Without them, you would have to organize the data from the milestone and plot it
yourself to have the same sense of progress.
GitLab Starter plots it for you and presents it in a clear and beautiful chart.
<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
For an overview, check the video demonstration on [Mapping work versus time with Burndown Charts](https://www.youtube.com/watch?v=zJU2MuRChzs).
## Use cases
Burndown Charts are generally used for tracking and analyzing the completion of
a milestone. Therefore, their use cases are tied to the
[use you are assigning your milestone to](index.md).
For example, suppose you lead a team of developers in a large company,
and you follow this workflow:
- Your company set the goal for the quarter to deliver 10 new features for your app
in the upcoming major release.
- You create a milestone, and remind your team to assign that milestone to every new issue
and merge request that's part of the launch of your app.
- Every week, you open the milestone, visualize the progress, identify the gaps,
and help your team to get their work done.
- Every month, you check in with your supervisor, and show the progress of that milestone
from the Burndown Chart.
- By the end of the quarter, your team successfully delivered 100% of that milestone, as
it was taken care of closely throughout the whole quarter.
## How it works
A Burndown Chart is available for every project or group milestone that has been attributed a **start
date** and a **due date**.
Find your project's **Burndown Chart** under **Project > Issues > Milestones**,
and select a milestone from your current ones, while for group's, access the **Groups** dashboard,
select a group, and go through **Issues > Milestones** on the sidebar.
NOTE: **Note:**
You're able to [promote project](index.md#promoting-project-milestones-to-group-milestones) to group milestones and still see the **Burndown Chart** for them, respecting license limitations.
The chart indicates the project's progress throughout that milestone (for issues assigned to it).
In particular, it shows how many issues were or are still open for a given day in the
milestone's corresponding period.
The Burndown Chart tracks when issues were created and when they were last closed—not their full history. For each day, it takes the number of issues still open and issues created that day and subtracts the number of issues closed that day.
**Issues that were created and assigned a milestone before its start date—and remain open as of the start date—are considered as having been opened on the start date**. Therefore, when the milestone start date is changed the number of opened issues on each day may change.
Reopened issues are
considered as having been opened on the day after they were last closed.
The Burndown Chart can also be toggled to display the cumulative open issue
weight for a given day. When using this feature, make sure issue weights have
been properly assigned, since an open issue with no weight adds zero to the
cumulative value.
<!-- ## Troubleshooting
Include any troubleshooting steps that you can foresee. If you know beforehand what issues
one might have when setting this up, or when something is changed, or on upgrading, it's
important to describe those, too. Think of things that may go wrong and include them here.
This is important to minimize requests for support, and to avoid doc comments with
questions that you know someone might ask.
Each scenario can be a third-level heading, e.g. `### Getting error message X`.
If you have none to add when creating a doc, leave this section in place
but commented out to help encourage others to add to it in the future. -->
This document was moved to [another location](./burndown_and_burnup_charts.md).

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View file

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View file

@ -150,7 +150,7 @@ There are also tabs below these that show the following:
For project milestones in [GitLab Starter](https://about.gitlab.com/pricing/), a [burndown chart](burndown_charts.md) is in the milestone view, showing the progress of completing a milestone.
![burndown chart](img/burndown_chart.png)
![burndown chart](img/burndown_chart_v13_5.png)
### Group Burndown Charts **(STARTER)**

View file

@ -46,6 +46,7 @@ module Gitlab
push_frontend_feature_flag(:webperf_experiment, default_enabled: false)
push_frontend_feature_flag(:snippets_binary_blob, default_enabled: false)
push_frontend_feature_flag(:usage_data_api, default_enabled: true)
push_frontend_feature_flag(:security_auto_fix, default_enabled: false)
# Startup CSS feature is a special one as it can be enabled by means of cookies and params
gon.push({ features: { 'startupCss' => use_startup_css? } }, true)

View file

@ -20,8 +20,8 @@ module Gitlab
/\Aremote_\w+_(url|urls|request_header)\Z/ # carrierwave automatically creates these attribute methods for uploads
).freeze
def self.clean(*args)
new(*args).clean
def self.clean(*args, **kwargs)
new(*args, **kwargs).clean
end
def initialize(relation_hash:, relation_class:, excluded_keys: [])

View file

@ -31,8 +31,8 @@ module Gitlab
TOKEN_RESET_MODELS = %i[Project Namespace Group Ci::Trigger Ci::Build Ci::Runner ProjectHook].freeze
def self.create(*args)
new(*args).create
def self.create(*args, **kwargs)
new(*args, **kwargs).create
end
def self.relation_class(relation_name)

View file

@ -10,8 +10,8 @@ module Gitlab
MAX_RETRIES = 8
IGNORED_FILENAMES = %w(. ..).freeze
def self.import(*args)
new(*args).import
def self.import(*args, **kwargs)
new(*args, **kwargs).import
end
def initialize(importable:, archive_file:, shared:)

View file

@ -36,7 +36,7 @@ module Gitlab
end
def exportable
@project.present(exportable_params)
@project.present(**exportable_params)
end
def exportable_params

View file

@ -5,8 +5,8 @@ module Gitlab
class Saver
include Gitlab::ImportExport::CommandLineUtil
def self.save(*args)
new(*args).save
def self.save(*args, **kwargs)
new(*args, **kwargs).save
end
def initialize(exportable:, shared:)

View file

@ -40,7 +40,7 @@ module Gitlab
def add_upload(upload)
uploader_context = FileUploader.extract_dynamic_path(upload).named_captures.symbolize_keys
UploadService.new(@project, File.open(upload, 'r'), FileUploader, uploader_context).execute.to_h
UploadService.new(@project, File.open(upload, 'r'), FileUploader, **uploader_context).execute.to_h
end
def copy_project_uploads

View file

@ -3,8 +3,8 @@
module Gitlab
module ImportExport
class VersionChecker
def self.check!(*args)
new(*args).check!
def self.check!(*args, **kwargs)
new(*args, **kwargs).check!
end
def initialize(shared:)

View file

@ -185,6 +185,11 @@ msgid_plural "%d failed"
msgstr[0] ""
msgstr[1] ""
msgid "%d failed security job"
msgid_plural "%d failed security jobs"
msgstr[0] ""
msgstr[1] ""
msgid "%d fixed test result"
msgid_plural "%d fixed test results"
msgstr[0] ""
@ -2815,6 +2820,9 @@ msgstr ""
msgid "An error occurred previewing the blob"
msgstr ""
msgid "An error occurred when removing the label."
msgstr ""
msgid "An error occurred when toggling the notification subscription"
msgstr ""
@ -15244,6 +15252,9 @@ msgstr ""
msgid "Learn more"
msgstr ""
msgid "Learn more about %{username}"
msgstr ""
msgid "Learn more about Auto DevOps"
msgstr ""
@ -27553,6 +27564,9 @@ msgstr ""
msgid "Toggle navigation"
msgstr ""
msgid "Toggle project select"
msgstr ""
msgid "Toggle sidebar"
msgstr ""
@ -27964,6 +27978,9 @@ msgstr ""
msgid "Unable to save your changes. Please try again."
msgstr ""
msgid "Unable to save your preference"
msgstr ""
msgid "Unable to schedule a pipeline to run immediately"
msgstr ""
@ -28600,6 +28617,9 @@ msgstr ""
msgid "UserProfile|Blocked user"
msgstr ""
msgid "UserProfile|Bot activity"
msgstr ""
msgid "UserProfile|Contributed projects"
msgstr ""
@ -31217,6 +31237,9 @@ msgstr ""
msgid "missing"
msgstr ""
msgid "more information"
msgstr ""
msgid "most recent deployment"
msgstr ""

View file

@ -43,7 +43,7 @@
"@babel/preset-env": "^7.10.1",
"@gitlab/at.js": "1.5.5",
"@gitlab/svgs": "1.171.0",
"@gitlab/ui": "21.28.0",
"@gitlab/ui": "21.30.1",
"@gitlab/visual-review-tools": "1.6.1",
"@rails/actioncable": "^6.0.3-3",
"@rails/ujs": "^6.0.3-2",

View file

@ -79,7 +79,7 @@ module QA
private
def text_area
find('#editor textarea', visible: false)
find('.monaco-editor textarea', visible: false)
end
end
end

View file

@ -139,7 +139,7 @@ module QA
end
def click_edit_button
click_element(:snippet_action_button, action: 'Edit')
click_element(:snippet_action_button, Page::Dashboard::Snippet::Edit, action: 'Edit')
end
def click_delete_button

View file

@ -6,11 +6,10 @@ module QA
module Snippet
class Edit < Page::Base
view 'app/assets/javascripts/snippets/components/edit.vue' do
element :submit_button
element :submit_button, required: true
end
def add_to_file_content(content)
finished_loading?
text_area.set content
text_area.has_text?(content) # wait for changes to take effect
end
@ -26,7 +25,7 @@ module QA
private
def text_area
find('#editor textarea', visible: false)
find('.monaco-editor textarea', visible: false)
end
end
end

View file

@ -4,6 +4,8 @@ require 'spec_helper'
RSpec.describe SnippetsController do
let_it_be(:user) { create(:user) }
let_it_be(:other_user) { create(:user) }
let_it_be(:public_snippet) { create(:personal_snippet, :public, :repository, author: user) }
describe 'GET #index' do
let(:base_params) { { username: user.username } }
@ -12,10 +14,6 @@ RSpec.describe SnippetsController do
it_behaves_like 'paginated collection' do
let(:collection) { Snippet.all }
let(:params) { { username: user.username } }
before do
create(:personal_snippet, :public, author: user)
end
end
it 'renders snippets of a user when username is present' do
@ -97,8 +95,7 @@ RSpec.describe SnippetsController do
end
context 'when signed in user is not the author' do
let(:other_author) { create(:author) }
let(:other_personal_snippet) { create(:personal_snippet, :private, author: other_author) }
let(:other_personal_snippet) { create(:personal_snippet, :private, author: other_user) }
it 'responds with status 404' do
get :show, params: { id: other_personal_snippet.to_param }
@ -158,7 +155,7 @@ RSpec.describe SnippetsController do
end
context 'when the personal snippet is public' do
let_it_be(:personal_snippet) { create(:personal_snippet, :public, :repository, author: user) }
let(:personal_snippet) { public_snippet }
context 'when signed in' do
before do
@ -166,22 +163,22 @@ RSpec.describe SnippetsController do
end
it_behaves_like 'successful response' do
subject { get :show, params: { id: personal_snippet.to_param } }
subject { get :show, params: { id: public_snippet.to_param } }
end
it 'responds with status 200 when embeddable content is requested' do
get :show, params: { id: personal_snippet.to_param }, format: :js
get :show, params: { id: public_snippet.to_param }, format: :js
expect(assigns(:snippet)).to eq(personal_snippet)
expect(assigns(:snippet)).to eq(public_snippet)
expect(response).to have_gitlab_http_status(:ok)
end
end
context 'when not signed in' do
it 'renders the snippet' do
get :show, params: { id: personal_snippet.to_param }
get :show, params: { id: public_snippet.to_param }
expect(assigns(:snippet)).to eq(personal_snippet)
expect(assigns(:snippet)).to eq(public_snippet)
expect(response).to have_gitlab_http_status(:ok)
end
end
@ -211,37 +208,34 @@ RSpec.describe SnippetsController do
context 'when requesting JSON' do
it 'renders the blob from the repository' do
personal_snippet = create(:personal_snippet, :public, :repository, author: user)
get :show, params: { id: public_snippet.to_param }, format: :json
get :show, params: { id: personal_snippet.to_param }, format: :json
expect(assigns(:blob)).to eq(personal_snippet.blobs.first)
expect(assigns(:blob)).to eq(public_snippet.blobs.first)
end
end
end
describe 'POST #mark_as_spam' do
let(:snippet) { create(:personal_snippet, :public, author: user) }
before do
allow_next_instance_of(Spam::AkismetService) do |instance|
allow(instance).to receive_messages(submit_spam: true)
end
stub_application_setting(akismet_enabled: true)
end
def mark_as_spam
admin = create(:admin)
create(:user_agent_detail, subject: snippet)
create(:user_agent_detail, subject: public_snippet)
sign_in(admin)
post :mark_as_spam, params: { id: snippet.id }
post :mark_as_spam, params: { id: public_snippet.id }
end
it 'updates the snippet' do
mark_as_spam
expect(snippet.reload).not_to be_submittable_as_spam
expect(public_snippet.reload).not_to be_submittable_as_spam
end
end
@ -269,9 +263,7 @@ RSpec.describe SnippetsController do
shared_examples 'CRLF line ending' do
let(:content) { "first line\r\nsecond line\r\nthird line" }
let(:formatted_content) { content.gsub(/\r\n/, "\n") }
let(:snippet) do
create(:personal_snippet, :public, :repository, author: user, content: content)
end
let(:snippet) { public_snippet }
before do
allow_next_instance_of(Blob) do |instance|
@ -340,8 +332,7 @@ RSpec.describe SnippetsController do
end
context 'when signed in user is not the author' do
let(:other_author) { create(:author) }
let(:other_personal_snippet) { create(:personal_snippet, :private, author: other_author) }
let(:other_personal_snippet) { create(:personal_snippet, :private, author: other_user) }
it 'responds with status 404' do
get :raw, params: { id: other_personal_snippet.to_param }
@ -385,7 +376,7 @@ RSpec.describe SnippetsController do
end
context 'when the personal snippet is public' do
let_it_be(:snippet) { create(:personal_snippet, :public, :repository, author: user) }
let(:snippet) { public_snippet }
context 'when signed in' do
before do
@ -429,11 +420,10 @@ RSpec.describe SnippetsController do
end
context 'award emoji on snippets' do
let(:personal_snippet) { create(:personal_snippet, :public, author: user) }
let(:another_user) { create(:user) }
let(:personal_snippet) { public_snippet }
before do
sign_in(another_user)
sign_in(other_user)
end
describe 'POST #toggle_award_emoji' do
@ -458,12 +448,10 @@ RSpec.describe SnippetsController do
end
describe 'POST #preview_markdown' do
let(:snippet) { create(:personal_snippet, :public) }
it 'renders json in a correct format' do
sign_in(user)
post :preview_markdown, params: { id: snippet, text: '*Markdown* text' }
post :preview_markdown, params: { id: public_snippet, text: '*Markdown* text' }
expect(json_response.keys).to match_array(%w(body references))
end

View file

@ -20,18 +20,12 @@ RSpec.describe 'User searches Alert Management alerts', :js do
end
context 'when a developer displays the alert list and the alert service is enabled they can search an alert' do
it 'shows the alert table with an alert for a valid search' do
expect(page).to have_selector('[data-testid="search-icon"]')
find('.gl-search-box-by-type-input').set('Alert')
expect(all('.dropdown-menu-selectable').count).to be(1)
end
it 'shows the an empty table with an invalid search' do
find('.gl-search-box-by-type-input').set('invalid search text')
expect(page).not_to have_selector('.dropdown-menu-selectable')
it 'shows the incident table with an incident for a valid search filter bar' do
expect(page).to have_selector('.filtered-search-wrapper')
expect(page).to have_selector('.gl-table')
expect(page).to have_css('[data-testid="severityField"]')
expect(all('tbody tr').count).to be(1)
expect(page).not_to have_selector('.empty-state')
end
end
end

View file

@ -0,0 +1,36 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'User updates Alert Management status', :js do
let_it_be(:project) { create(:project) }
let_it_be(:developer) { create(:user) }
let_it_be(:alerts_service) { create(:alerts_service, project: project) }
let_it_be(:alert) { create(:alert_management_alert, project: project, status: 'triggered') }
before_all do
project.add_developer(developer)
end
before do
sign_in(developer)
visit project_alert_management_index_path(project)
wait_for_requests
end
context 'when a developer+ displays the alerts list and the alert service is enabled they can update an alert status' do
it 'shows the alert table with an alert status dropdown' do
expect(page).to have_selector('.gl-table')
expect(find('.dropdown-menu-selectable')).to have_content('Triggered')
end
it 'updates the alert status' do
find('.dropdown-menu-selectable').click
find('.dropdown-item', text: 'Acknowledged').click
wait_for_requests
expect(find('.dropdown-menu-selectable')).to have_content('Acknowledged')
end
end
end

View file

@ -21,15 +21,15 @@ RSpec.describe 'Overview tab on a user profile', :js do
sign_in user
end
describe 'activities section' do
shared_context 'visit overview tab' do
before do
visit user.username
page.find('.js-overview-tab a').click
wait_for_requests
end
shared_context 'visit overview tab' do
before do
visit user.username
page.find('.js-overview-tab a').click
wait_for_requests
end
end
describe 'activities section' do
describe 'user has no activities' do
include_context 'visit overview tab'
@ -84,14 +84,6 @@ RSpec.describe 'Overview tab on a user profile', :js do
end
describe 'projects section' do
shared_context 'visit overview tab' do
before do
visit user.username
page.find('.js-overview-tab a').click
wait_for_requests
end
end
describe 'user has no personal projects' do
include_context 'visit overview tab'
@ -158,4 +150,52 @@ RSpec.describe 'Overview tab on a user profile', :js do
end
end
end
describe 'bot user' do
let(:bot_user) { create(:user, user_type: :security_bot) }
shared_context "visit bot's overview tab" do
before do
visit bot_user.username
page.find('.js-overview-tab a').click
wait_for_requests
end
end
describe 'feature flag enabled' do
before do
stub_feature_flags(security_auto_fix: true)
end
include_context "visit bot's overview tab"
it "activity panel's title is 'Bot activity'" do
page.within('.activities-block') do
expect(page).to have_text('Bot activity')
end
end
it 'does not show projects panel' do
expect(page).not_to have_selector('.projects-block')
end
end
describe 'feature flag disabled' do
before do
stub_feature_flags(security_auto_fix: false)
end
include_context "visit bot's overview tab"
it "activity panel's title is not 'Bot activity'" do
page.within('.activities-block') do
expect(page).not_to have_text('Bot activity')
end
end
it 'shows projects panel' do
expect(page).to have_selector('.projects-block')
end
end
end
end

View file

@ -182,4 +182,46 @@ RSpec.describe 'User page' do
it_behaves_like 'page meta description', 'Lorem ipsum dolor sit amet'
end
context 'with a bot user' do
let(:user) { create(:user, user_type: :security_bot) }
describe 'feature flag enabled' do
before do
stub_feature_flags(security_auto_fix: true)
end
it 'only shows Overview and Activity tabs' do
visit(user_path(user))
page.within '.nav-links' do
expect(page).to have_link('Overview')
expect(page).to have_link('Activity')
expect(page).not_to have_link('Groups')
expect(page).not_to have_link('Contributed projects')
expect(page).not_to have_link('Personal projects')
expect(page).not_to have_link('Snippets')
end
end
end
describe 'feature flag disabled' do
before do
stub_feature_flags(security_auto_fix: false)
end
it 'only shows Overview and Activity tabs' do
visit(user_path(user))
page.within '.nav-links' do
expect(page).to have_link('Overview')
expect(page).to have_link('Activity')
expect(page).to have_link('Groups')
expect(page).to have_link('Contributed projects')
expect(page).to have_link('Personal projects')
expect(page).to have_link('Snippets')
end
end
end
end
end

View file

@ -1,25 +1,17 @@
import { shallowMount } from '@vue/test-utils';
import { GlEmptyState } from '@gitlab/ui';
import AlertManagementEmptyState from '~/alert_management/components/alert_management_empty_state.vue';
import defaultProvideValues from '../mocks/alerts_provide_config.json';
describe('AlertManagementEmptyState', () => {
let wrapper;
function mountComponent({
props = {
alertManagementEnabled: false,
userCanEnableAlertManagement: false,
},
stubs = {},
} = {}) {
function mountComponent({ provide = {} } = {}) {
wrapper = shallowMount(AlertManagementEmptyState, {
propsData: {
enableAlertManagementPath: '/link',
alertsHelpUrl: '/link',
emptyAlertSvgPath: 'illustration/path',
...props,
provide: {
...defaultProvideValues,
...provide,
},
stubs,
});
}
@ -42,7 +34,7 @@ describe('AlertManagementEmptyState', () => {
it('show OpsGenie integration state when OpsGenie mcv is true', () => {
mountComponent({
props: {
provide: {
alertManagementEnabled: false,
userCanEnableAlertManagement: false,
opsgenieMvcEnabled: true,

Some files were not shown because too many files have changed in this diff Show more