summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/incidents
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/incidents')
-rw-r--r--app/assets/javascripts/incidents/components/incidents_list.vue196
-rw-r--r--app/assets/javascripts/incidents/constants.js7
-rw-r--r--app/assets/javascripts/incidents/graphql/queries/get_count_by_status.query.graphql15
-rw-r--r--app/assets/javascripts/incidents/graphql/queries/get_incidents.query.graphql6
-rw-r--r--app/assets/javascripts/incidents/list.js6
5 files changed, 198 insertions, 32 deletions
diff --git a/app/assets/javascripts/incidents/components/incidents_list.vue b/app/assets/javascripts/incidents/components/incidents_list.vue
index 670c42cbdac..78eb828fd19 100644
--- a/app/assets/javascripts/incidents/components/incidents_list.vue
+++ b/app/assets/javascripts/incidents/components/incidents_list.vue
@@ -8,7 +8,6 @@ import {
GlAvatar,
GlTooltipDirective,
GlButton,
- GlSearchBoxByType,
GlIcon,
GlPagination,
GlTabs,
@@ -16,18 +15,35 @@ import {
GlBadge,
GlEmptyState,
} from '@gitlab/ui';
-import { debounce } from 'lodash';
+import Api from '~/api';
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 glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { convertToSnakeCase } from '~/lib/utils/text_utility';
-import { s__ } from '~/locale';
-import { mergeUrlParams, joinPaths, visitUrl } from '~/lib/utils/url_utility';
+import { s__, __ } from '~/locale';
+import { urlParamsToObject } from '~/lib/utils/common_utils';
+import {
+ visitUrl,
+ mergeUrlParams,
+ joinPaths,
+ updateHistory,
+ setUrlParams,
+} 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_SEARCH_DELAY, INCIDENT_STATUS_TABS } from '../constants';
+import {
+ I18N,
+ DEFAULT_PAGE_SIZE,
+ INCIDENT_STATUS_TABS,
+ TH_CREATED_AT_TEST_ID,
+ TH_SEVERITY_TEST_ID,
+ TH_PUBLISHED_TEST_ID,
+ INCIDENT_DETAILS_PATH,
+} from '../constants';
-const TH_TEST_ID = { 'data-testid': 'incident-management-created-at-sort' };
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';
@@ -49,8 +65,10 @@ export default {
{
key: 'severity',
label: s__('IncidentManagement|Severity'),
- thClass: `gl-pointer-events-none`,
- tdClass,
+ thClass,
+ tdClass: `${tdClass} sortable-cell`,
+ sortable: true,
+ thAttr: TH_SEVERITY_TEST_ID,
},
{
key: 'title',
@@ -64,7 +82,7 @@ export default {
thClass,
tdClass: `${tdClass} sortable-cell`,
sortable: true,
- thAttr: TH_TEST_ID,
+ thAttr: TH_CREATED_AT_TEST_ID,
},
{
key: 'assignees',
@@ -82,7 +100,6 @@ export default {
GlAvatar,
GlButton,
TimeAgoTooltip,
- GlSearchBoxByType,
GlIcon,
GlPagination,
GlTabs,
@@ -91,10 +108,12 @@ export default {
GlBadge,
GlEmptyState,
SeverityToken,
+ FilteredSearchBar,
},
directives: {
GlTooltip: GlTooltipDirective,
},
+ mixins: [glFeatureFlagsMixin()],
inject: [
'projectPath',
'newIssuePath',
@@ -103,6 +122,9 @@ export default {
'issuePath',
'publishedAvailable',
'emptyListSvgPath',
+ 'textQuery',
+ 'authorUsernamesQuery',
+ 'assigneeUsernamesQuery',
],
apollo: {
incidents: {
@@ -118,6 +140,8 @@ export default {
lastPageSize: this.pagination.lastPageSize,
prevPageCursor: this.pagination.prevPageCursor,
nextPageCursor: this.pagination.nextPageCursor,
+ authorUsername: this.authorUsername,
+ assigneeUsernames: this.assigneeUsernames,
};
},
update({ project: { issues: { nodes = [], pageInfo = {} } = {} } = {} }) {
@@ -135,6 +159,8 @@ export default {
variables() {
return {
searchTerm: this.searchTerm,
+ authorUsername: this.authorUsername,
+ assigneeUsernames: this.assigneeUsernames,
projectPath: this.projectPath,
issueTypes: ['INCIDENT'],
};
@@ -149,7 +175,7 @@ export default {
errored: false,
isErrorAlertDismissed: false,
redirecting: false,
- searchTerm: '',
+ searchTerm: this.textQuery,
pagination: initialPaginationState,
incidents: {},
sort: 'created_desc',
@@ -157,6 +183,9 @@ export default {
sortDesc: true,
statusFilter: '',
filteredByStatus: '',
+ authorUsername: this.authorUsernamesQuery,
+ assigneeUsernames: this.assigneeUsernamesQuery,
+ filterParams: {},
};
},
computed: {
@@ -208,7 +237,10 @@ export default {
{
key: 'published',
label: s__('IncidentManagement|Published'),
- thClass: 'gl-pointer-events-none',
+ thClass,
+ tdClass: `${tdClass} sortable-cell`,
+ sortable: true,
+ thAttr: TH_PUBLISHED_TEST_ID,
},
],
]
@@ -242,15 +274,59 @@ 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: {
- onInputChange: debounce(function debounceSearch(input) {
- const trimmedInput = input.trim();
- if (trimmedInput !== this.searchTerm) {
- this.searchTerm = trimmedInput;
- }
- }, INCIDENT_SEARCH_DELAY),
filterIncidentsByStatus(tabIndex) {
+ this.resetPagination();
const { filters, status } = this.$options.statusTabs[tabIndex];
this.statusFilter = filters;
this.filteredByStatus = status;
@@ -259,7 +335,10 @@ export default {
return Boolean(assignees.nodes?.length);
},
navigateToIncidentDetails({ iid }) {
- return visitUrl(joinPaths(this.issuePath, iid));
+ const path = this.glFeatures.issuesIncidentDetails
+ ? joinPaths(this.issuePath, INCIDENT_DETAILS_PATH)
+ : this.issuePath;
+ return visitUrl(joinPaths(path, iid));
},
handlePageChange(page) {
const { startCursor, endCursor } = this.incidents.pageInfo;
@@ -284,14 +363,73 @@ export default {
this.pagination = initialPaginationState;
},
fetchSortedData({ sortBy, sortDesc }) {
- const sortingDirection = sortDesc ? 'desc' : 'asc';
- const sortingColumn = convertToSnakeCase(sortBy).replace(/_.*/, '');
+ const sortingDirection = sortDesc ? 'DESC' : 'ASC';
+ const sortingColumn = convertToSnakeCase(sortBy)
+ .replace(/_.*/, '')
+ .toUpperCase();
+ this.resetPagination();
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;
+ },
+ 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,
+ });
+ },
},
};
</script>
@@ -331,12 +469,16 @@ export default {
</gl-button>
</div>
- <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
- :value="searchTerm"
- class="gl-bg-white"
- :placeholder="$options.i18n.searchPlaceholder"
- @input="onInputChange"
+ <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>
diff --git a/app/assets/javascripts/incidents/constants.js b/app/assets/javascripts/incidents/constants.js
index 289b36d9848..797439495e3 100644
--- a/app/assets/javascripts/incidents/constants.js
+++ b/app/assets/javascripts/incidents/constants.js
@@ -6,7 +6,7 @@ export const I18N = {
unassigned: s__('IncidentManagement|Unassigned'),
createIncidentBtnLabel: s__('IncidentManagement|Create incident'),
unPublished: s__('IncidentManagement|Unpublished'),
- searchPlaceholder: __('Search results…'),
+ searchPlaceholder: __('Search or filter results…'),
emptyState: {
title: s__('IncidentManagement|Display your incidents in a dedicated view'),
emptyClosedTabTitle: s__('IncidentManagement|There are no closed incidents'),
@@ -34,5 +34,8 @@ export const INCIDENT_STATUS_TABS = [
},
];
-export const INCIDENT_SEARCH_DELAY = 300;
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' };
+export const TH_PUBLISHED_TEST_ID = { 'data-testid': 'incident-management-published-sort' };
+export const INCIDENT_DETAILS_PATH = 'incident';
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 0b784b104a8..fd96825c0f7 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
@@ -1,6 +1,17 @@
-query getIncidentsCountByStatus($searchTerm: String, $projectPath: ID!, $issueTypes: [IssueType!]) {
+query getIncidentsCountByStatus(
+ $searchTerm: String
+ $projectPath: ID!
+ $issueTypes: [IssueType!]
+ $authorUsername: String = ""
+ $assigneeUsernames: String = ""
+) {
project(fullPath: $projectPath) {
- issueStatusCounts(search: $searchTerm, types: $issueTypes) {
+ issueStatusCounts(
+ search: $searchTerm
+ types: $issueTypes
+ authorUsername: $authorUsername
+ assigneeUsername: $assigneeUsernames
+ ) {
all
opened
closed
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 dab130835e2..dd2a42ba4e8 100644
--- a/app/assets/javascripts/incidents/graphql/queries/get_incidents.query.graphql
+++ b/app/assets/javascripts/incidents/graphql/queries/get_incidents.query.graphql
@@ -9,7 +9,9 @@ query getIncidents(
$lastPageSize: Int
$prevPageCursor: String = ""
$nextPageCursor: String = ""
- $searchTerm: String
+ $searchTerm: String = ""
+ $authorUsername: String = ""
+ $assigneeUsernames: String = ""
) {
project(fullPath: $projectPath) {
issues(
@@ -17,6 +19,8 @@ query getIncidents(
types: $issueTypes
sort: $sort
state: $status
+ authorUsername: $authorUsername
+ assigneeUsername: $assigneeUsernames
first: $firstPageSize
last: $lastPageSize
after: $nextPageCursor
diff --git a/app/assets/javascripts/incidents/list.js b/app/assets/javascripts/incidents/list.js
index 7505d07449c..aeec4a258b9 100644
--- a/app/assets/javascripts/incidents/list.js
+++ b/app/assets/javascripts/incidents/list.js
@@ -16,6 +16,9 @@ export default () => {
issuePath,
publishedAvailable,
emptyListSvgPath,
+ textQuery,
+ authorUsernamesQuery,
+ assigneeUsernamesQuery,
} = domEl.dataset;
const apolloProvider = new VueApollo({
@@ -32,6 +35,9 @@ export default () => {
issuePath,
publishedAvailable,
emptyListSvgPath,
+ textQuery,
+ authorUsernamesQuery,
+ assigneeUsernamesQuery,
},
apolloProvider,
components: {