summaryrefslogtreecommitdiff
path: root/app/assets/javascripts
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-10-15 18:08:43 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-10-15 18:08:43 +0000
commit316fbf9f95dcdd16775f0339415572c3195eea92 (patch)
tree40d86a896fc0ff8ce22fbed7e5e3dffc2adceebf /app/assets/javascripts
parentd9e71b0d412fb9d2d7fc8b00dddac21617eaaf19 (diff)
downloadgitlab-ce-316fbf9f95dcdd16775f0339415572c3195eea92.tar.gz
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts')
-rw-r--r--app/assets/javascripts/alert_management/components/alert_management_empty_state.vue31
-rw-r--r--app/assets/javascripts/alert_management/components/alert_management_list_wrapper.vue63
-rw-r--r--app/assets/javascripts/alert_management/components/alert_management_table.vue450
-rw-r--r--app/assets/javascripts/alert_management/components/alert_status.vue8
-rw-r--r--app/assets/javascripts/alert_management/components/sidebar/sidebar_assignees.vue6
-rw-r--r--app/assets/javascripts/alert_management/constants.js2
-rw-r--r--app/assets/javascripts/alert_management/graphql/queries/get_alerts.query.graphql4
-rw-r--r--app/assets/javascripts/alert_management/graphql/queries/get_count_by_status.query.graphql4
-rw-r--r--app/assets/javascripts/alert_management/list.js35
-rw-r--r--app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue22
-rw-r--r--app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue120
-rw-r--r--app/assets/javascripts/boards/index.js9
-rw-r--r--app/assets/javascripts/boards/queries/issue_set_labels.mutation.graphql15
-rw-r--r--app/assets/javascripts/boards/stores/actions.js26
-rw-r--r--app/assets/javascripts/boards/stores/getters.js2
-rw-r--r--app/assets/javascripts/ci_variable_list/components/ci_environments_dropdown.vue2
-rw-r--r--app/assets/javascripts/clusters/components/knative_domain_editor.vue1
-rw-r--r--app/assets/javascripts/feature_flags/components/new_environments_dropdown.vue1
-rw-r--r--app/assets/javascripts/incidents/components/incidents_list.vue508
-rw-r--r--app/assets/javascripts/incidents/constants.js11
-rw-r--r--app/assets/javascripts/incidents/graphql/queries/get_count_by_status.query.graphql4
-rw-r--r--app/assets/javascripts/incidents/graphql/queries/get_incidents.query.graphql4
-rw-r--r--app/assets/javascripts/incidents/list.js8
-rw-r--r--app/assets/javascripts/incidents_settings/components/pagerduty_form.vue1
-rw-r--r--app/assets/javascripts/jira_import/components/jira_import_form.vue2
-rw-r--r--app/assets/javascripts/milestones/project_milestone_combobox.vue1
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard_header.vue2
-rw-r--r--app/assets/javascripts/monitoring/components/dashboards_dropdown.vue6
-rw-r--r--app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue1
-rw-r--r--app/assets/javascripts/projects/commits/components/author_select.vue1
-rw-r--r--app/assets/javascripts/ref/components/ref_selector.vue1
-rw-r--r--app/assets/javascripts/snippets/components/snippet_blob_actions_edit.vue2
-rw-r--r--app/assets/javascripts/user_popovers.js1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue7
-rw-r--r--app/assets/javascripts/vue_shared/components/editor_lite.vue14
-rw-r--r--app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/constants.js21
-rw-r--r--app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue313
-rw-r--r--app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/utils.js11
-rw-r--r--app/assets/javascripts/vue_shared/components/timezone_dropdown.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue28
40 files changed, 965 insertions, 785 deletions
diff --git a/app/assets/javascripts/alert_management/components/alert_management_empty_state.vue b/app/assets/javascripts/alert_management/components/alert_management_empty_state.vue
index 68443166f40..c5ff2dc0d11 100644
--- a/app/assets/javascripts/alert_management/components/alert_management_empty_state.vue
+++ b/app/assets/javascripts/alert_management/components/alert_management_empty_state.vue
@@ -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: '',
diff --git a/app/assets/javascripts/alert_management/components/alert_management_list_wrapper.vue b/app/assets/javascripts/alert_management/components/alert_management_list_wrapper.vue
index 094f33fed3b..5e9cdfb3fed 100644
--- a/app/assets/javascripts/alert_management/components/alert_management_list_wrapper.vue
+++ b/app/assets/javascripts/alert_management/components/alert_management_list_wrapper.vue
@@ -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>
diff --git a/app/assets/javascripts/alert_management/components/alert_management_table.vue b/app/assets/javascripts/alert_management/components/alert_management_table.vue
index 6000acb6aa3..f287b425826 100644
--- a/app/assets/javascripts/alert_management/components/alert_management_table.vue
+++ b/app/assets/javascripts/alert_management/components/alert_management_table.vue
@@ -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;
- },
- 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;
+ isEmpty() {
+ return !this.alerts?.list?.length;
},
},
- 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>
+ <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>
+ </template>
+ </gl-sprintf>
+ </gl-alert>
- <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>
+ <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>
- <h4 class="d-block d-md-none my-3">
+ <template #title>
{{ 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>
- <template #cell(startedAt)="{ item }">
- <time-ago v-if="item.startedAt" :time="item.startedAt" />
- </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 #cell(eventCount)="{ item }">
- {{ item.eventCount }}
- </template>
+ <template #cell(startedAt)="{ item }">
+ <time-ago v-if="item.startedAt" :time="item.startedAt" />
+ </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(eventCount)="{ item }">
+ {{ item.eventCount }}
+ </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(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(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(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(status)="{ item }">
- <alert-status
- :alert="item"
- :project-path="projectPath"
- :is-sidebar="false"
- @alert-error="handleAlertError"
- />
- </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 #empty>
- {{ s__('AlertManagement|No alerts to display.') }}
- </template>
+ <template #cell(status)="{ item }">
+ <alert-status
+ :alert="item"
+ :project-path="projectPath"
+ :is-sidebar="false"
+ @alert-error="handleAlertError"
+ @hide-dropdown="handleStatusUpdate"
+ />
+ </template>
- <template #table-busy>
- <gl-loading-icon size="lg" color="dark" class="mt-3" />
- </template>
- </gl-table>
+ <template #empty>
+ {{ s__('AlertManagement|No alerts to display.') }}
+ </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 #table-busy>
+ <gl-loading-icon size="lg" color="dark" class="mt-3" />
+ </template>
+ </gl-table>
+ </template>
+ </paginated-table-with-search-and-tabs>
</div>
</template>
diff --git a/app/assets/javascripts/alert_management/components/alert_status.vue b/app/assets/javascripts/alert_management/components/alert_status.vue
index c505ef6c15b..3083a85cbd9 100644
--- a/app/assets/javascripts/alert_management/components/alert_status.vue
+++ b/app/assets/javascripts/alert_management/components/alert_status.vue
@@ -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(
diff --git a/app/assets/javascripts/alert_management/components/sidebar/sidebar_assignees.vue b/app/assets/javascripts/alert_management/components/sidebar/sidebar_assignees.vue
index 2e667bf99a8..5e4fd56738b 100644
--- a/app/assets/javascripts/alert_management/components/sidebar/sidebar_assignees.vue
+++ b/app/assets/javascripts/alert_management/components/sidebar/sidebar_assignees.vue
@@ -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
diff --git a/app/assets/javascripts/alert_management/constants.js b/app/assets/javascripts/alert_management/constants.js
index 73cb5ecdf98..b79a64646eb 100644
--- a/app/assets/javascripts/alert_management/constants.js
+++ b/app/assets/javascripts/alert_management/constants.js
@@ -63,5 +63,3 @@ export const trackAlertStatusUpdateOptions = {
action: 'update_alert_status',
label: 'Status',
};
-
-export const DEFAULT_PAGE_SIZE = 20;
diff --git a/app/assets/javascripts/alert_management/graphql/queries/get_alerts.query.graphql b/app/assets/javascripts/alert_management/graphql/queries/get_alerts.query.graphql
index 8ac00bbc6b5..bc7e51a2e90 100644
--- a/app/assets/javascripts/alert_management/graphql/queries/get_alerts.query.graphql
+++ b/app/assets/javascripts/alert_management/graphql/queries/get_alerts.query.graphql
@@ -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
diff --git a/app/assets/javascripts/alert_management/graphql/queries/get_count_by_status.query.graphql b/app/assets/javascripts/alert_management/graphql/queries/get_count_by_status.query.graphql
index 5a6faea5cd8..40ec4c56171 100644
--- a/app/assets/javascripts/alert_management/graphql/queries/get_count_by_status.query.graphql
+++ b/app/assets/javascripts/alert_management/graphql/queries/get_count_by_status.query.graphql
@@ -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
diff --git a/app/assets/javascripts/alert_management/list.js b/app/assets/javascripts/alert_management/list.js
index e180ab5f7e3..e34450204fb 100644
--- a/app/assets/javascripts/alert_management/list.js
+++ b/app/assets/javascripts/alert_management/list.js
@@ -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');
},
});
};
diff --git a/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue b/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue
index 9a746d2baa7..6a44f87d0e7 100644
--- a/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue
+++ b/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue
@@ -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
diff --git a/app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue b/app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue
new file mode 100644
index 00000000000..0f063c7582e
--- /dev/null
+++ b/app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue
@@ -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>
diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js
index dc4ccc93951..9b501a3c6b8 100644
--- a/app/assets/javascripts/boards/index.js
+++ b/app/assets/javascripts/boards/index.js
@@ -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();
};
diff --git a/app/assets/javascripts/boards/queries/issue_set_labels.mutation.graphql b/app/assets/javascripts/boards/queries/issue_set_labels.mutation.graphql
new file mode 100644
index 00000000000..3c5f4b3e3bd
--- /dev/null
+++ b/app/assets/javascripts/boards/queries/issue_set_labels.mutation.graphql
@@ -0,0 +1,15 @@
+mutation issueSetLabels($input: UpdateIssueInput!) {
+ updateIssue(input: $input) {
+ issue {
+ labels {
+ nodes {
+ id
+ title
+ color
+ description
+ }
+ }
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/boards/stores/actions.js b/app/assets/javascripts/boards/stores/actions.js
index a1fd05f2a3b..bd1bf17b0c7 100644
--- a/app/assets/javascripts/boards/stores/actions.js
+++ b/app/assets/javascripts/boards/stores/actions.js
@@ -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();
},
diff --git a/app/assets/javascripts/boards/stores/getters.js b/app/assets/javascripts/boards/stores/getters.js
index 9279d18ff1e..89a3b14b262 100644
--- a/app/assets/javascripts/boards/stores/getters.js
+++ b/app/assets/javascripts/boards/stores/getters.js
@@ -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;
}
diff --git a/app/assets/javascripts/ci_variable_list/components/ci_environments_dropdown.vue b/app/assets/javascripts/ci_variable_list/components/ci_environments_dropdown.vue
index 5bae2751a17..ceb94b1f0f8 100644
--- a/app/assets/javascripts/ci_variable_list/components/ci_environments_dropdown.vue
+++ b/app/assets/javascripts/ci_variable_list/components/ci_environments_dropdown.vue
@@ -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"
diff --git a/app/assets/javascripts/clusters/components/knative_domain_editor.vue b/app/assets/javascripts/clusters/components/knative_domain_editor.vue
index 19ce3e36cd7..cb415d902e8 100644
--- a/app/assets/javascripts/clusters/components/knative_domain_editor.vue
+++ b/app/assets/javascripts/clusters/components/knative_domain_editor.vue
@@ -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"
diff --git a/app/assets/javascripts/feature_flags/components/new_environments_dropdown.vue b/app/assets/javascripts/feature_flags/components/new_environments_dropdown.vue
index 2888746005e..f1371c0320d 100644
--- a/app/assets/javascripts/feature_flags/components/new_environments_dropdown.vue
+++ b/app/assets/javascripts/feature_flags/components/new_environments_dropdown.vue
@@ -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"
/>
diff --git a/app/assets/javascripts/incidents/components/incidents_list.vue b/app/assets/javascripts/incidents/components/incidents_list.vue
index 3ecd911e814..245d71ce55f 100644
--- a/app/assets/javascripts/incidents/components/incidents_list.vue
+++ b/app/assets/javascripts/incidents/components/incidents_list.vue
@@ -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;
- },
- 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);
+ isEmpty() {
+ return !this.incidents?.list?.length;
},
- 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>
+ <template v-else>
+ {{ $options.i18n.unassigned }}
</template>
- </gl-avatars-inline>
+ </div>
</template>
- <template v-else>
- {{ $options.i18n.unassigned }}
+
+ <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>
- </div>
- </template>
- <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 v-if="errored" #empty>
+ {{ $options.i18n.noIncidents }}
+ </template>
+ </gl-table>
</template>
-
- <template v-if="errored" #empty>
- {{ $options.i18n.noIncidents }}
+ <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>
- </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>
diff --git a/app/assets/javascripts/incidents/constants.js b/app/assets/javascripts/incidents/constants.js
index 4fccefb66c5..9c31a5702a2 100644
--- a/app/assets/javascripts/incidents/constants.js
+++ b/app/assets/javascripts/incidents/constants.js
@@ -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' };
diff --git a/app/assets/javascripts/incidents/graphql/queries/get_count_by_status.query.graphql b/app/assets/javascripts/incidents/graphql/queries/get_count_by_status.query.graphql
index fd96825c0f7..4e44a506c4f 100644
--- a/app/assets/javascripts/incidents/graphql/queries/get_count_by_status.query.graphql
+++ b/app/assets/javascripts/incidents/graphql/queries/get_count_by_status.query.graphql
@@ -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
diff --git a/app/assets/javascripts/incidents/graphql/queries/get_incidents.query.graphql b/app/assets/javascripts/incidents/graphql/queries/get_incidents.query.graphql
index dd2a42ba4e8..f97664a3b77 100644
--- a/app/assets/javascripts/incidents/graphql/queries/get_incidents.query.graphql
+++ b/app/assets/javascripts/incidents/graphql/queries/get_incidents.query.graphql
@@ -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
diff --git a/app/assets/javascripts/incidents/list.js b/app/assets/javascripts/incidents/list.js
index 15af7432436..6f87fbbe775 100644
--- a/app/assets/javascripts/incidents/list.js
+++ b/app/assets/javascripts/incidents/list.js
@@ -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,
diff --git a/app/assets/javascripts/incidents_settings/components/pagerduty_form.vue b/app/assets/javascripts/incidents_settings/components/pagerduty_form.vue
index ae6b72679e1..9a8c4bc5af9 100644
--- a/app/assets/javascripts/incidents_settings/components/pagerduty_form.vue
+++ b/app/assets/javascripts/incidents_settings/components/pagerduty_form.vue
@@ -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>
diff --git a/app/assets/javascripts/jira_import/components/jira_import_form.vue b/app/assets/javascripts/jira_import/components/jira_import_form.vue
index 4339021d9a0..4a1bca110fd 100644
--- a/app/assets/javascripts/jira_import/components/jira_import_form.vue
+++ b/app/assets/javascripts/jira_import/components/jira_import_form.vue
@@ -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" />
diff --git a/app/assets/javascripts/milestones/project_milestone_combobox.vue b/app/assets/javascripts/milestones/project_milestone_combobox.vue
index 5ee917573ce..0fa5585e858 100644
--- a/app/assets/javascripts/milestones/project_milestone_combobox.vue
+++ b/app/assets/javascripts/milestones/project_milestone_combobox.vue
@@ -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"
diff --git a/app/assets/javascripts/monitoring/components/dashboard_header.vue b/app/assets/javascripts/monitoring/components/dashboard_header.vue
index e468728a954..0f6a9ce3814 100644
--- a/app/assets/javascripts/monitoring/components/dashboard_header.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard_header.vue
@@ -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">
diff --git a/app/assets/javascripts/monitoring/components/dashboards_dropdown.vue b/app/assets/javascripts/monitoring/components/dashboards_dropdown.vue
index 932efeaaf0e..1a349aa154a 100644
--- a/app/assets/javascripts/monitoring/components/dashboards_dropdown.vue
+++ b/app/assets/javascripts/monitoring/components/dashboards_dropdown.vue
@@ -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
diff --git a/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue b/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue
index 1cec08b93bd..b05cf080aea 100644
--- a/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue
+++ b/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue
@@ -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"
diff --git a/app/assets/javascripts/projects/commits/components/author_select.vue b/app/assets/javascripts/projects/commits/components/author_select.vue
index 2204ec3cbe7..3bc772fe60a 100644
--- a/app/assets/javascripts/projects/commits/components/author_select.vue
+++ b/app/assets/javascripts/projects/commits/components/author_select.vue
@@ -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"
/>
diff --git a/app/assets/javascripts/ref/components/ref_selector.vue b/app/assets/javascripts/ref/components/ref_selector.vue
index 85b123530b5..0084450c9b0 100644
--- a/app/assets/javascripts/ref/components/ref_selector.vue
+++ b/app/assets/javascripts/ref/components/ref_selector.vue
@@ -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"
diff --git a/app/assets/javascripts/snippets/components/snippet_blob_actions_edit.vue b/app/assets/javascripts/snippets/components/snippet_blob_actions_edit.vue
index 19625b37f0e..ab2553265a2 100644
--- a/app/assets/javascripts/snippets/components/snippet_blob_actions_edit.vue
+++ b/app/assets/javascripts/snippets/components/snippet_blob_actions_edit.vue
@@ -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"
diff --git a/app/assets/javascripts/user_popovers.js b/app/assets/javascripts/user_popovers.js
index 0636d79e6f2..3521c1a105f 100644
--- a/app/assets/javascripts/user_popovers.js
+++ b/app/assets/javascripts/user_popovers.js
@@ -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,
});
}
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue
index 157d6d60290..e3c0b7935d7 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue
@@ -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"
diff --git a/app/assets/javascripts/vue_shared/components/editor_lite.vue b/app/assets/javascripts/vue_shared/components/editor_lite.vue
index 98889a0dced..bc3a9ee45f8 100644
--- a/app/assets/javascripts/vue_shared/components/editor_lite.vue
+++ b/app/assets/javascripts/vue_shared/components/editor_lite.vue
@@ -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>
diff --git a/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/constants.js b/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/constants.js
new file mode 100644
index 00000000000..b7768cfa5b9
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/constants.js
@@ -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…'),
+};
diff --git a/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue b/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue
new file mode 100644
index 00000000000..8e85d93e6d1
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue
@@ -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>
diff --git a/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/utils.js b/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/utils.js
new file mode 100644
index 00000000000..7de4263acbb
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/utils.js
@@ -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;
+};
diff --git a/app/assets/javascripts/vue_shared/components/timezone_dropdown.vue b/app/assets/javascripts/vue_shared/components/timezone_dropdown.vue
index 135b9842cbf..ac222b22112 100644
--- a/app/assets/javascripts/vue_shared/components/timezone_dropdown.vue
+++ b/app/assets/javascripts/vue_shared/components/timezone_dropdown.vue
@@ -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"
diff --git a/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue b/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue
index 6aaff000845..3f5738b2b93 100644
--- a/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue
+++ b/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue
@@ -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>