Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
d9e71b0d41
commit
316fbf9f95
129 changed files with 2676 additions and 1736 deletions
12
CHANGELOG.md
12
CHANGELOG.md
|
@ -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)
|
||||
|
|
|
@ -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: '',
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -63,5 +63,3 @@ export const trackAlertStatusUpdateOptions = {
|
|||
action: 'update_alert_status',
|
||||
label: 'Status',
|
||||
};
|
||||
|
||||
export const DEFAULT_PAGE_SIZE = 20;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
|
@ -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();
|
||||
};
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
mutation issueSetLabels($input: UpdateIssueInput!) {
|
||||
updateIssue(input: $input) {
|
||||
issue {
|
||||
labels {
|
||||
nodes {
|
||||
id
|
||||
title
|
||||
color
|
||||
description
|
||||
}
|
||||
}
|
||||
}
|
||||
errors
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
},
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
/>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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' };
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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" />
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
/>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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…'),
|
||||
};
|
|
@ -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>
|
|
@ -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;
|
||||
};
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
.monaco-editor.gl-editor-lite {
|
||||
.line-numbers {
|
||||
@include gl-pt-0;
|
||||
}
|
||||
[id^='editor-lite-'] {
|
||||
height: 500px;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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:)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 @@
|
|||
·
|
||||
%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
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix workflow:rules not accessing passed-upstream and trigger variables
|
||||
merge_request: 44935
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Show all inherited labels in projects and subgroups
|
||||
merge_request: 45161
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Show origin path of labels on subgroup labels page
|
||||
merge_request: 45040
|
||||
author:
|
||||
type: added
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
title: Fix rollback portion of migration that adds temporary index for container scanning findings
|
||||
merge_request: 44593
|
||||
author:
|
||||
type: fixed
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
title: 'Revert 42465 and 42343: Expanded collapsed diff files'
|
||||
merge_request: 43361
|
||||
author:
|
||||
type: other
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
title: Improve merge error when pre-receive hooks fail in fast-forward merge
|
||||
merge_request: 44843
|
||||
author:
|
||||
type: fixed
|
|
@ -49,7 +49,7 @@
|
|||
- error_tracking
|
||||
- feature_flags
|
||||
- foundations
|
||||
- fuzz-testing
|
||||
- fuzz_testing
|
||||
- gdk
|
||||
- geo_replication
|
||||
- git_lfs
|
||||
|
|
7
config/feature_flags/development/security_auto_fix.yml
Normal file
7
config/feature_flags/development/security_auto_fix.yml
Normal file
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
name: security_auto_fix
|
||||
introduced_by_url:
|
||||
rollout_issue_url:
|
||||
group: group::composition analysis
|
||||
type: development
|
||||
default_enabled: false
|
|
@ -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
|
||||
|
|
|
@ -61,6 +61,7 @@ buildpacks
|
|||
bundler
|
||||
bundlers
|
||||
burndown
|
||||
burnup
|
||||
cacheable
|
||||
CAS
|
||||
CentOS
|
||||
|
|
|
@ -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:**
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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).
|
||||
|
|
|
@ -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}**).
|
||||
|
||||
|
|
142
doc/user/project/milestones/burndown_and_burnup_charts.md
Normal file
142
doc/user/project/milestones/burndown_and_burnup_charts.md
Normal 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. -->
|
|
@ -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 |
BIN
doc/user/project/milestones/img/burndown_chart_fixed_v13_5.png
Normal file
BIN
doc/user/project/milestones/img/burndown_chart_fixed_v13_5.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 32 KiB |
BIN
doc/user/project/milestones/img/burndown_chart_legacy_v13_5.png
Normal file
BIN
doc/user/project/milestones/img/burndown_chart_legacy_v13_5.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
BIN
doc/user/project/milestones/img/burnup_chart_v13_5.png
Normal file
BIN
doc/user/project/milestones/img/burnup_chart_v13_5.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 29 KiB |
|
@ -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)**
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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: [])
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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:)
|
||||
|
|
|
@ -36,7 +36,7 @@ module Gitlab
|
|||
end
|
||||
|
||||
def exportable
|
||||
@project.present(exportable_params)
|
||||
@project.present(**exportable_params)
|
||||
end
|
||||
|
||||
def exportable_params
|
||||
|
|
|
@ -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:)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:)
|
||||
|
|
|
@ -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 ""
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -79,7 +79,7 @@ module QA
|
|||
private
|
||||
|
||||
def text_area
|
||||
find('#editor textarea', visible: false)
|
||||
find('.monaco-editor textarea', visible: false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
Loading…
Reference in a new issue