Commit b28974de authored by Dmytro Zaporozhets's avatar Dmytro Zaporozhets

Merge branch '229399-incidents-list-mvc' into 'master'

Add  basic incidents list

See merge request gitlab-org/gitlab!37314
parents 0f78d17b f86d9a37
......@@ -318,7 +318,7 @@ export default {
</script>
<template>
<div>
<div class="alert-management-list">
<div class="incident-management-list">
<gl-alert v-if="showNoAlertsMsg" @dismiss="isAlertDismissed = true">
<gl-sprintf :message="$options.i18n.noAlertsMsg">
<template #link="{ content }">
......
<script>
import { GlLoadingIcon, GlTable, GlAlert } from '@gitlab/ui';
import { s__ } from '~/locale';
import getIncidents from '../graphql/queries/get_incidents.query.graphql';
import { I18N } 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-bg-blue-50 gl-hover-border-b-solid gl-hover-border-blue-200';
export default {
i18n: I18N,
fields: [
{
key: 'title',
label: s__('IncidentManagement|Incident'),
thClass: `gl-pointer-events-none gl-w-half`,
tdClass,
},
{
key: 'createdAt',
label: s__('IncidentManagement|Date created'),
thClass: `${thClass} gl-pointer-events-none`,
tdClass,
},
{
key: 'assignees',
label: s__('IncidentManagement|Assignees'),
thClass: 'gl-pointer-events-none',
tdClass,
},
],
components: {
GlLoadingIcon,
GlTable,
GlAlert,
},
inject: ['projectPath'],
apollo: {
incidents: {
query: getIncidents,
variables() {
return {
projectPath: this.projectPath,
labelNames: ['incident'],
};
},
update: ({ project: { issues: { nodes = [] } = {} } = {} }) => nodes,
error() {
this.errored = true;
},
},
},
data() {
return {
errored: false,
isErrorAlertDismissed: false,
};
},
computed: {
showErrorMsg() {
return this.errored && !this.isErrorAlertDismissed;
},
loading() {
return this.$apollo.queries.incidents.loading;
},
hasIncidents() {
return this.incidents?.length;
},
tbodyTrClass() {
return {
[bodyTrClass]: !this.loading && this.hasIncidents,
};
},
},
methods: {
getAssignees(assignees) {
return assignees.nodes?.length > 0
? assignees.nodes[0]?.username
: s__('IncidentManagement|Unassigned');
},
},
};
</script>
<template>
<div class="incident-management-list">
<gl-alert v-if="showErrorMsg" variant="danger" @dismiss="isErrorAlertDismissed = true">
{{ $options.i18n.errorMsg }}
</gl-alert>
<h4 class="gl-display-block d-md-none my-3">
{{ s__('IncidentManagement|Incidents') }}
</h4>
<gl-table
:items="incidents"
:fields="$options.fields"
:show-empty="true"
:busy="loading"
stacked="md"
:tbody-tr-class="tbodyTrClass"
:no-local-sorting="true"
fixed
>
<template #cell(title)="{ item }">
<div class="gl-max-w-full text-truncate" :title="item.title">{{ item.title }}</div>
</template>
<template #cell(createdAt)="{ item }">
{{ item.createdAt }}
</template>
<template #cell(assignees)="{ item }">
<div class="gl-max-w-full text-truncate" data-testid="assigneesField">
{{ getAssignees(item.assignees) }}
</div>
</template>
<template #table-busy>
<gl-loading-icon size="lg" color="dark" class="mt-3" />
</template>
<template #empty>
{{ $options.i18n.noIncidents }}
</template>
</gl-table>
</div>
</template>
/* eslint-disable import/prefer-default-export */
import { s__ } from '~/locale';
export const I18N = {
errorMsg: s__('IncidentManagement|There was an error displaying the incidents.'),
noIncidents: s__('IncidentManagement|No incidents to display.'),
};
query getIncidents($projectPath: ID!, $labelNames: [String], $state: IssuableState) {
project(fullPath: $projectPath) {
issues(state: $state, labelName: $labelNames) {
nodes {
iid
title
createdAt
labels {
nodes {
title
color
}
}
assignees {
nodes {
username
}
}
}
}
}
}
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import IncidentsList from './components/incidents_list.vue';
Vue.use(VueApollo);
export default () => {
const selector = '#js-incidents';
const domEl = document.querySelector(selector);
const { projectPath } = domEl.dataset;
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
});
return new Vue({
el: selector,
provide: {
projectPath,
},
apolloProvider,
components: {
IncidentsList,
},
render(createElement) {
return createElement('incidents-list', {
props: {
projectPath,
},
});
},
});
};
import IncidentsList from '~/incidents/list';
document.addEventListener('DOMContentLoaded', () => {
IncidentsList();
});
.alert-management-list,
.incident-management-list,
.alert-management-details {
.icon-critical {
color: $red-800;
......
.alert-management-list {
.incident-management-list {
.new-alert {
background-color: $issues-today-bg;
}
// these styles need to be deleted once GlTable component looks in GitLab same as in @gitlab/ui
table {
color: $gray-700;
@include gl-text-gray-700;
tr {
&:focus {
......@@ -24,9 +24,9 @@
}
th {
background-color: transparent;
font-weight: $gl-font-weight-bold;
color: $gl-gray-600;
@include gl-bg-transparent;
@include gl-font-weight-bold;
@include gl-text-gray-600;
&[aria-sort='none']:hover {
background-image: url('data:image/svg+xml, %3csvg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="4 0 8 16"%3e %3cpath style="fill: %23BABABA;" fill-rule="evenodd" d="M11.707085,11.7071 L7.999975,15.4142 L4.292875,11.7071 C3.902375,11.3166 3.902375, 10.6834 4.292875,10.2929 C4.683375,9.90237 5.316575,9.90237 5.707075,10.2929 L6.999975, 11.5858 L6.999975,2 C6.999975,1.44771 7.447695,1 7.999975,1 C8.552255,1 8.999975,1.44771 8.999975,2 L8.999975,11.5858 L10.292865,10.2929 C10.683395 ,9.90237 11.316555,9.90237 11.707085,10.2929 C12.097605,10.6834 12.097605,11.3166 11.707085,11.7071 Z"/%3e %3c/svg%3e');
......@@ -46,15 +46,24 @@
}
@include media-breakpoint-down(sm) {
.alert-management-table {
table {
tr {
border-top: 0;
@include gl-border-t-0;
.table-col {
min-height: 68px;
}
&:hover {
@include gl-bg-white;
@include gl-border-none;
}
}
&.alert-management-table {
.table-col {
&:last-child {
background-color: $gray-10;
@include gl-bg-gray-10;
&::before {
content: none !important;
......@@ -66,12 +75,6 @@
}
}
}
&:hover {
background-color: $white;
border-color: $white;
border-bottom-style: none;
}
}
}
}
......
# frozen_string_literal: true
class Projects::IncidentsController < Projects::ApplicationController
before_action :authorize_read_incidents!
def index
end
end
# frozen_string_literal: true
module Projects::IncidentsHelper
def incidents_data(project)
{
'project-path' => project.full_path
}
end
end
......@@ -465,6 +465,7 @@ module ProjectsHelper
serverless: :read_cluster,
error_tracking: :read_sentry_issue,
alert_management: :read_alert_management_alert,
incidents: :read_incidents,
labels: :read_label,
issues: :read_issue,
project_members: :read_project_member,
......@@ -732,6 +733,8 @@ module ProjectsHelper
functions
error_tracking
alert_management
incidents
incident_management
user
gcp
logs
......
......@@ -258,6 +258,7 @@ class ProjectPolicy < BasePolicy
enable :read_merge_request
enable :read_sentry_issue
enable :update_sentry_issue
enable :read_incidents
enable :read_prometheus
enable :read_metrics_dashboard_annotation
enable :metrics_dashboard
......
......@@ -228,10 +228,16 @@
- if project_nav_tab?(:alert_management)
= nav_link(controller: :alert_management) do
= link_to project_alert_management_index_path(@project), title: _('Alerts'), class: 'shortcuts-tracking qa-operations-tracking-link' do
= link_to project_alert_management_index_path(@project), title: _('Alerts') do
%span
= _('Alerts')
- if project_nav_tab?(:incidents)
= nav_link(controller: :incidents) do
= link_to project_incidents_path(@project), title: _('Incidents') do
%span
= _('Incidents')
- if project_nav_tab? :environments
= render_if_exists "layouts/nav/sidebar/tracing_link"
......@@ -242,7 +248,7 @@
- if project_nav_tab?(:error_tracking)
= nav_link(controller: :error_tracking) do
= link_to project_error_tracking_index_path(@project), title: _('Error Tracking'), class: 'shortcuts-tracking qa-operations-tracking-link' do
= link_to project_error_tracking_index_path(@project), title: _('Error Tracking') do
%span
= _('Error Tracking')
......
- page_title _('Incidents')
#js-incidents{ data: incidents_data(@project) }
---
title: Add basic incidents list
merge_request: 37314
author:
type: added
......@@ -300,6 +300,8 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
post 'incidents/integrations/pagerduty', to: 'incident_management/pager_duty_incidents#create'
resources :incidents, only: [:index]
namespace :error_tracking do
resources :projects, only: :index
end
......
......@@ -12641,6 +12641,27 @@ msgstr ""
msgid "Incident Management Limits"
msgstr ""
msgid "IncidentManagement|Assignees"
msgstr ""
msgid "IncidentManagement|Date created"
msgstr ""
msgid "IncidentManagement|Incident"
msgstr ""
msgid "IncidentManagement|Incidents"
msgstr ""
msgid "IncidentManagement|No incidents to display."
msgstr ""
msgid "IncidentManagement|There was an error displaying the incidents."
msgstr ""
msgid "IncidentManagement|Unassigned"
msgstr ""
msgid "IncidentSettings|Alert integration"
msgstr ""
......@@ -12656,6 +12677,9 @@ msgstr ""
msgid "IncidentSettings|Set up integrations with external tools to help better manage incidents."
msgstr ""
msgid "Incidents"
msgstr ""
msgid "Include a Terms of Service agreement and Privacy Policy that all users must accept."
msgstr ""
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Projects::IncidentsController do
let_it_be(:project) { create(:project) }
let_it_be(:developer) { create(:user) }
let_it_be(:guest) { create(:user) }
before_all do
project.add_developer(developer)
project.add_guest(guest)
end
describe 'GET #index' do
def make_request
get :index, params: { namespace_id: project.namespace, project_id: project }
end
it 'shows the page for user with developer role' do
sign_in(developer)
make_request
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template(:index)
end
context 'when user is unauthorized' do
it 'redirects to the login page' do
sign_out(developer)
make_request
expect(response).to redirect_to(new_user_session_path)
end
end
context 'when user is a guest' do
it 'shows 404' do
sign_in(guest)
make_request
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
end
import { mount } from '@vue/test-utils';
import { GlAlert, GlLoadingIcon, GlTable } from '@gitlab/ui';
import IncidentsList from '~/incidents/components/incidents_list.vue';
import { I18N } from '~/incidents/constants';
describe('Incidents List', () => {
let wrapper;
const findTable = () => wrapper.find(GlTable);
const findTableRows = () => wrapper.findAll('table tbody tr');
const findAlert = () => wrapper.find(GlAlert);
const findLoader = () => wrapper.find(GlLoadingIcon);
function mountComponent({ data = { incidents: [] }, loading = false }) {
wrapper = mount(IncidentsList, {
data() {
return data;
},
mocks: {
$apollo: {
queries: {
incidents: {
loading,
},
},
},
},
provide: {
projectPath: '/project/path',
},
});
}
afterEach(() => {
if (wrapper) {
wrapper.destroy();
wrapper = null;
}
});
it('shows the loading state', () => {
mountComponent({
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
loading: true,
});
expect(findLoader().exists()).toBe(true);
});
it('shows empty state', () => {
mountComponent({
data: { incidents: [] },
loading: false,
});
expect(findTable().text()).toContain(I18N.noIncidents);
});
it('shows error state', () => {
mountComponent({
data: { incidents: [], errored: true },
loading: false,
});
expect(findTable().text()).toContain(I18N.noIncidents);
expect(findAlert().exists()).toBe(true);
});
it('displays basic list', () => {
const incidents = [
{ title: 1, assignees: [] },
{ title: 2, assignees: [] },
{ title: 3, assignees: [] },
];
mountComponent({
data: { incidents },
loading: false,
});
expect(findTableRows().length).toBe(incidents.length);
});
});
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Projects::IncidentsHelper do
include Gitlab::Routing.url_helpers
let(:project) { create(:project) }
let(:project_path) { project.full_path }
describe '#incidents_data' do
subject(:data) { helper.incidents_data(project) }
it 'returns frontend configuration' do
expect(data).to match('project-path' => project_path)
end
end
end
......@@ -64,6 +64,7 @@ RSpec.shared_context 'project navbar structure' do
nav_sub_items: [
_('Metrics'),
_('Alerts'),
_('Incidents'),
_('Environments'),
_('Error Tracking'),
_('Serverless'),
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment