Commit a9a0f4f4 authored by Angelo Gulina's avatar Angelo Gulina Committed by Shinya Maeda

Expose environment path for the Alert details section

The feature is behind a feature flag
GraphQL Schema is updated and so the relevant documentation
parent 0ef3cf20
...@@ -30,6 +30,7 @@ import AlertSidebar from './alert_sidebar.vue'; ...@@ -30,6 +30,7 @@ import AlertSidebar from './alert_sidebar.vue';
import AlertMetrics from './alert_metrics.vue'; import AlertMetrics from './alert_metrics.vue';
import AlertDetailsTable from '~/vue_shared/components/alert_details_table.vue'; import AlertDetailsTable from '~/vue_shared/components/alert_details_table.vue';
import AlertSummaryRow from './alert_summary_row.vue'; import AlertSummaryRow from './alert_summary_row.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
const containerEl = document.querySelector('.page-with-contextual-sidebar'); const containerEl = document.querySelector('.page-with-contextual-sidebar');
...@@ -76,6 +77,7 @@ export default { ...@@ -76,6 +77,7 @@ export default {
SystemNote, SystemNote,
AlertMetrics, AlertMetrics,
}, },
mixins: [glFeatureFlagsMixin()],
inject: { inject: {
projectPath: { projectPath: {
default: '', default: '',
...@@ -147,6 +149,15 @@ export default { ...@@ -147,6 +149,15 @@ export default {
this.$router.replace({ name: 'tab', params: { tabId } }); this.$router.replace({ name: 'tab', params: { tabId } });
}, },
}, },
environmentName() {
return this.shouldDisplayEnvironment && this.alert?.environment?.name;
},
environmentPath() {
return this.shouldDisplayEnvironment && this.alert?.environment?.path;
},
shouldDisplayEnvironment() {
return this.glFeatures.exposeEnvironmentPathInAlertDetails;
},
}, },
mounted() { mounted() {
this.trackPageViews(); this.trackPageViews();
...@@ -299,19 +310,18 @@ export default { ...@@ -299,19 +310,18 @@ export default {
</span> </span>
</alert-summary-row> </alert-summary-row>
<alert-summary-row <alert-summary-row
v-if="alert.environment" v-if="environmentName"
:label="`${s__('AlertManagement|Environment')}:`" :label="`${s__('AlertManagement|Environment')}:`"
> >
<gl-link <gl-link
v-if="alert.environmentUrl" v-if="environmentPath"
class="gl-display-inline-block" class="gl-display-inline-block"
data-testid="environmentUrl" data-testid="environmentPath"
:href="alert.environmentUrl" :href="environmentPath"
target="_blank"
> >
{{ alert.environment }} {{ environmentName }}
</gl-link> </gl-link>
<span v-else data-testid="environment">{{ alert.environment }}</span> <span v-else data-testid="environmentName">{{ environmentName }}</span>
</alert-summary-row> </alert-summary-row>
<alert-summary-row <alert-summary-row
v-if="alert.startedAt" v-if="alert.startedAt"
......
import { defaultDataIdFromObject } from 'apollo-cache-inmemory';
import produce from 'immer';
import Vue from 'vue'; import Vue from 'vue';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import produce from 'immer';
import { defaultDataIdFromObject } from 'apollo-cache-inmemory';
import createDefaultClient from '~/lib/graphql'; import createDefaultClient from '~/lib/graphql';
import createRouter from './router';
import AlertDetails from './components/alert_details.vue'; import AlertDetails from './components/alert_details.vue';
import sidebarStatusQuery from './graphql/queries/sidebar_status.query.graphql'; import sidebarStatusQuery from './graphql/queries/sidebar_status.query.graphql';
import createRouter from './router';
Vue.use(VueApollo); Vue.use(VueApollo);
......
...@@ -11,6 +11,10 @@ fragment AlertDetailItem on AlertManagementAlert { ...@@ -11,6 +11,10 @@ fragment AlertDetailItem on AlertManagementAlert {
updatedAt updatedAt
endedAt endedAt
hosts hosts
environment {
name
path
}
details details
runbook runbook
todos { todos {
......
...@@ -7,9 +7,11 @@ import { ...@@ -7,9 +7,11 @@ import {
convertToSentenceCase, convertToSentenceCase,
splitCamelCase, splitCamelCase,
} from '~/lib/utils/text_utility'; } from '~/lib/utils/text_utility';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
const thClass = 'gl-bg-transparent! gl-border-1! gl-border-b-solid! gl-border-gray-200!'; const thClass = 'gl-bg-transparent! gl-border-1! gl-border-b-solid! gl-border-gray-200!';
const tdClass = 'gl-border-gray-100! gl-p-5!'; const tdClass = 'gl-border-gray-100! gl-p-5!';
const allowedFields = [ const allowedFields = [
'iid', 'iid',
'title', 'title',
...@@ -22,17 +24,15 @@ const allowedFields = [ ...@@ -22,17 +24,15 @@ const allowedFields = [
'description', 'description',
'endedAt', 'endedAt',
'details', 'details',
'environment',
'hosts', 'hosts',
]; ];
const isAllowed = fieldName => allowedFields.includes(fieldName);
export default { export default {
components: { components: {
GlLoadingIcon, GlLoadingIcon,
GlTable, GlTable,
}, },
mixins: [glFeatureFlagsMixin()],
props: { props: {
alert: { alert: {
type: Object, type: Object,
...@@ -60,14 +60,23 @@ export default { ...@@ -60,14 +60,23 @@ export default {
}, },
], ],
computed: { computed: {
flaggedAllowedFields() {
return this.shouldDisplayEnvironment ? [...allowedFields, 'environment'] : allowedFields;
},
items() { items() {
if (!this.alert) { if (!this.alert) {
return []; return [];
} }
return reduce( return reduce(
this.alert, this.alert,
(allowedItems, value, fieldName) => { (allowedItems, fieldValue, fieldName) => {
if (isAllowed(fieldName)) { if (this.isAllowed(fieldName)) {
let value;
if (fieldName === 'environment') {
value = fieldValue?.name;
} else {
value = fieldValue;
}
return [...allowedItems, { fieldName, value }]; return [...allowedItems, { fieldName, value }];
} }
return allowedItems; return allowedItems;
...@@ -75,6 +84,14 @@ export default { ...@@ -75,6 +84,14 @@ export default {
[], [],
); );
}, },
shouldDisplayEnvironment() {
return this.glFeatures.exposeEnvironmentPathInAlertDetails;
},
},
methods: {
isAllowed(fieldName) {
return this.flaggedAllowedFields.includes(fieldName);
},
}, },
}; };
</script> </script>
......
...@@ -10,5 +10,6 @@ class Projects::AlertManagementController < Projects::ApplicationController ...@@ -10,5 +10,6 @@ class Projects::AlertManagementController < Projects::ApplicationController
def details def details
@alert_id = params[:id] @alert_id = params[:id]
push_frontend_feature_flag(:expose_environment_path_in_alert_details, @project)
end end
end end
...@@ -68,6 +68,11 @@ module Types ...@@ -68,6 +68,11 @@ module Types
null: true, null: true,
description: 'Timestamp the alert ended' description: 'Timestamp the alert ended'
field :environment,
Types::EnvironmentType,
null: true,
description: 'Environment for the alert'
field :event_count, field :event_count,
GraphQL::INT_TYPE, GraphQL::INT_TYPE,
null: true, null: true,
......
...@@ -5,6 +5,8 @@ module Types ...@@ -5,6 +5,8 @@ module Types
graphql_name 'Environment' graphql_name 'Environment'
description 'Describes where code is deployed for a project' description 'Describes where code is deployed for a project'
present_using ::EnvironmentPresenter
authorize :read_environment authorize :read_environment
field :name, GraphQL::STRING_TYPE, null: false, field :name, GraphQL::STRING_TYPE, null: false,
...@@ -16,6 +18,10 @@ module Types ...@@ -16,6 +18,10 @@ module Types
field :state, GraphQL::STRING_TYPE, null: false, field :state, GraphQL::STRING_TYPE, null: false,
description: 'State of the environment, for example: available/stopped' description: 'State of the environment, for example: available/stopped'
field :path, GraphQL::STRING_TYPE, null: true,
description: 'The path to the environment. Will always return null ' \
'if `expose_environment_path_in_alert_details` feature flag is disabled'
field :metrics_dashboard, Types::Metrics::DashboardType, null: true, field :metrics_dashboard, Types::Metrics::DashboardType, null: true,
description: 'Metrics dashboard schema for the environment', description: 'Metrics dashboard schema for the environment',
resolver: Resolvers::Metrics::DashboardResolver resolver: Resolvers::Metrics::DashboardResolver
...@@ -23,6 +29,6 @@ module Types ...@@ -23,6 +29,6 @@ module Types
field :latest_opened_most_severe_alert, field :latest_opened_most_severe_alert,
Types::AlertManagement::AlertType, Types::AlertManagement::AlertType,
null: true, null: true,
description: 'The most severe open alert for the environment. If multiple alerts have equal severity, the most recent is returned.' description: 'The most severe open alert for the environment. If multiple alerts have equal severity, the most recent is returned'
end end
end end
...@@ -4,6 +4,7 @@ class Environment < ApplicationRecord ...@@ -4,6 +4,7 @@ class Environment < ApplicationRecord
include Gitlab::Utils::StrongMemoize include Gitlab::Utils::StrongMemoize
include ReactiveCaching include ReactiveCaching
include FastDestroyAll::Helpers include FastDestroyAll::Helpers
include Presentable
self.reactive_cache_refresh_interval = 1.minute self.reactive_cache_refresh_interval = 1.minute
self.reactive_cache_lifetime = 55.seconds self.reactive_cache_lifetime = 55.seconds
......
# frozen_string_literal: true
class EnvironmentPresenter < Gitlab::View::Presenter::Delegated
include ActionView::Helpers::UrlHelper
presents :environment
def path
if Feature.enabled?(:expose_environment_path_in_alert_details, project)
project_environment_path(project, self)
end
end
end
---
name: expose_environment_path_in_alert_details
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/43414
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/258638
type: development
group: group::progressive delivery
default_enabled: false
...@@ -244,6 +244,11 @@ type AlertManagementAlert implements Noteable { ...@@ -244,6 +244,11 @@ type AlertManagementAlert implements Noteable {
""" """
endedAt: Time endedAt: Time
"""
Environment for the alert
"""
environment: Environment
""" """
Number of events of this alert Number of events of this alert
""" """
...@@ -5773,7 +5778,7 @@ type Environment { ...@@ -5773,7 +5778,7 @@ type Environment {
id: ID! id: ID!
""" """
The most severe open alert for the environment. If multiple alerts have equal severity, the most recent is returned. The most severe open alert for the environment. If multiple alerts have equal severity, the most recent is returned
""" """
latestOpenedMostSevereAlert: AlertManagementAlert latestOpenedMostSevereAlert: AlertManagementAlert
...@@ -5792,6 +5797,12 @@ type Environment { ...@@ -5792,6 +5797,12 @@ type Environment {
""" """
name: String! name: String!
"""
The path to the environment. Will always return null if
`expose_environment_path_in_alert_details` feature flag is disabled
"""
path: String
""" """
State of the environment, for example: available/stopped State of the environment, for example: available/stopped
""" """
......
...@@ -666,6 +666,20 @@ ...@@ -666,6 +666,20 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
}, },
{
"name": "environment",
"description": "Environment for the alert",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "Environment",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{ {
"name": "eventCount", "name": "eventCount",
"description": "Number of events of this alert", "description": "Number of events of this alert",
...@@ -15990,7 +16004,7 @@ ...@@ -15990,7 +16004,7 @@
}, },
{ {
"name": "latestOpenedMostSevereAlert", "name": "latestOpenedMostSevereAlert",
"description": "The most severe open alert for the environment. If multiple alerts have equal severity, the most recent is returned.", "description": "The most severe open alert for the environment. If multiple alerts have equal severity, the most recent is returned",
"args": [ "args": [
], ],
...@@ -16047,6 +16061,20 @@ ...@@ -16047,6 +16061,20 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
}, },
{
"name": "path",
"description": "The path to the environment. Will always return null if `expose_environment_path_in_alert_details` feature flag is disabled",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{ {
"name": "state", "name": "state",
"description": "State of the environment, for example: available/stopped", "description": "State of the environment, for example: available/stopped",
...@@ -77,6 +77,7 @@ Describes an alert from the project's Alert Management. ...@@ -77,6 +77,7 @@ Describes an alert from the project's Alert Management.
| `details` | JSON | Alert details | | `details` | JSON | Alert details |
| `detailsUrl` | String! | The URL of the alert detail page | | `detailsUrl` | String! | The URL of the alert detail page |
| `endedAt` | Time | Timestamp the alert ended | | `endedAt` | Time | Timestamp the alert ended |
| `environment` | Environment | Environment for the alert |
| `eventCount` | Int | Number of events of this alert | | `eventCount` | Int | Number of events of this alert |
| `hosts` | String! => Array | List of hosts the alert came from | | `hosts` | String! => Array | List of hosts the alert came from |
| `iid` | ID! | Internal ID of the alert | | `iid` | ID! | Internal ID of the alert |
...@@ -943,9 +944,10 @@ Describes where code is deployed for a project. ...@@ -943,9 +944,10 @@ Describes where code is deployed for a project.
| Field | Type | Description | | Field | Type | Description |
| ----- | ---- | ----------- | | ----- | ---- | ----------- |
| `id` | ID! | ID of the environment | | `id` | ID! | ID of the environment |
| `latestOpenedMostSevereAlert` | AlertManagementAlert | The most severe open alert for the environment. If multiple alerts have equal severity, the most recent is returned. | | `latestOpenedMostSevereAlert` | AlertManagementAlert | The most severe open alert for the environment. If multiple alerts have equal severity, the most recent is returned |
| `metricsDashboard` | MetricsDashboard | Metrics dashboard schema for the environment | | `metricsDashboard` | MetricsDashboard | Metrics dashboard schema for the environment |
| `name` | String! | Human-readable name of the environment | | `name` | String! | Human-readable name of the environment |
| `path` | String | The path to the environment. Will always return null if `expose_environment_path_in_alert_details` feature flag is disabled |
| `state` | String! | State of the environment, for example: available/stopped | | `state` | String! | State of the environment, for example: available/stopped |
### Epic ### Epic
......
...@@ -308,3 +308,49 @@ Viewing logs from a metrics panel can be useful if you're triaging an ...@@ -308,3 +308,49 @@ Viewing logs from a metrics panel can be useful if you're triaging an
application incident and need to [explore logs](../metrics/dashboards/index.md#chart-context-menu) application incident and need to [explore logs](../metrics/dashboards/index.md#chart-context-menu)
from across your application. These logs help you understand what's affecting from across your application. These logs help you understand what's affecting
your application's performance and how to resolve any problems. your application's performance and how to resolve any problems.
## View the environment that generated the alert
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/232492) in GitLab 13.5.
> - It's [deployed behind a feature flag](../../user/feature_flags.md), disabled by default.
> - It's disabled on GitLab.com.
> - It's not recommended for production use.
> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-environment-link-in-alert-details). **(CORE ONLY)**
CAUTION: **Warning:**
This feature might not be available to you. Check the **version history** note above for details.
The environment information and the link are displayed in the Alert Details tab[#alert-details-tab].
### Enable or disable Environment Link in Alert Details **(CORE ONLY)**
Viewing the environment is under development and not ready for production use. It is
deployed behind a feature flag that is **disabled by default**.
[GitLab administrators with access to the GitLab Rails console](../../administration/feature_flags.md)
can enable it.
To enable it:
```ruby
Feature.enable(:expose_environment_path_in_alert_details)
```
To enable for just a particular project:
```ruby
project = Project.find_by_full_path('your-group/your-project')
Feature.enable(:expose_environment_path_in_alert_details, project)
```
To disable it:
```ruby
Feature.disable(:expose_environment_path_in_alert_details)
```
To disable for just a particular project:
```ruby
project = Project.find_by_full_path('your-group/your-project')
Feature.disable(:expose_environment_path_in_alert_details, project)
```
...@@ -42,7 +42,7 @@ RSpec.describe Projects::AlertManagementController do ...@@ -42,7 +42,7 @@ RSpec.describe Projects::AlertManagementController do
let(:role) { :reporter } let(:role) { :reporter }
it 'shows 404' do it 'shows 404' do
get :index, params: { namespace_id: project.namespace, project_id: project } get :details, params: { namespace_id: project.namespace, project_id: project, id: id }
expect(response).to have_gitlab_http_status(:not_found) expect(response).to have_gitlab_http_status(:not_found)
end end
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Alert management', :js do
let_it_be(:project) { create(:project) }
let_it_be(:developer) { create(:user) }
before_all do
project.add_developer(developer)
end
context 'when visiting the alert details page' do
let!(:alert) { create(:alert_management_alert, :resolved, :with_fingerprint, title: 'dos-test', project: project, **options) }
let(:options) { {} }
before do
sign_in(user)
end
context 'when actor has permission to see the alert' do
let(:user) { developer }
it 'shows the alert details' do
visit(details_project_alert_management_path(project, alert))
within('.alert-management-details-table') do
expect(page).to have_content(alert.title)
end
end
context 'when alert belongs to an environment' do
let(:options) { { environment: environment } }
let!(:environment) { create(:environment, name: 'production', project: project) }
it 'shows the environment name' do
visit(details_project_alert_management_path(project, alert))
expect(page).to have_link(environment.name, href: project_environment_path(project, environment))
within('.alert-management-details-table') do
expect(page).to have_content(environment.name)
end
end
context 'when expose_environment_path_in_alert_details feature flag is disabled' do
before do
stub_feature_flags(expose_environment_path_in_alert_details: false)
end
it 'does not show the environment name' do
visit(details_project_alert_management_path(project, alert))
within('.alert-management-details-table') do
expect(page).to have_content(alert.title)
expect(page).not_to have_content(environment.name)
end
end
end
end
end
end
end
import { mount, shallowMount } from '@vue/test-utils';
import { GlAlert, GlLoadingIcon } from '@gitlab/ui'; import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
import axios from 'axios'; import axios from 'axios';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import AlertDetailsTable from '~/vue_shared/components/alert_details_table.vue';
import AlertDetails from '~/alert_management/components/alert_details.vue'; import AlertDetails from '~/alert_management/components/alert_details.vue';
import AlertSummaryRow from '~/alert_management/components/alert_summary_row.vue'; import AlertSummaryRow from '~/alert_management/components/alert_summary_row.vue';
import createIssueMutation from '~/alert_management/graphql/mutations/create_issue_from_alert.mutation.graphql';
import { joinPaths } from '~/lib/utils/url_utility';
import { import {
trackAlertsDetailsViewsOptions,
ALERTS_SEVERITY_LABELS, ALERTS_SEVERITY_LABELS,
trackAlertsDetailsViewsOptions,
} from '~/alert_management/constants'; } from '~/alert_management/constants';
import createIssueMutation from '~/alert_management/graphql/mutations/create_issue_from_alert.mutation.graphql';
import { joinPaths } from '~/lib/utils/url_utility';
import Tracking from '~/tracking'; import Tracking from '~/tracking';
import AlertDetailsTable from '~/vue_shared/components/alert_details_table.vue';
import mockAlerts from '../mocks/alerts.json'; import mockAlerts from '../mocks/alerts.json';
const mockAlert = mockAlerts[0]; const mockAlert = mockAlerts[0];
const environmentName = 'Production';
const environmentPath = '/fake/path';
describe('AlertDetails', () => { describe('AlertDetails', () => {
let wrapper; let environmentData = {
name: environmentName,
path: environmentPath,
};
let glFeatures = { exposeEnvironmentPathInAlertDetails: false };
let mock; let mock;
let wrapper;
const projectPath = 'root/alerts'; const projectPath = 'root/alerts';
const projectIssuesPath = 'root/alerts/-/issues'; const projectIssuesPath = 'root/alerts/-/issues';
const projectId = '1'; const projectId = '1';
...@@ -33,9 +40,17 @@ describe('AlertDetails', () => { ...@@ -33,9 +40,17 @@ describe('AlertDetails', () => {
projectPath, projectPath,
projectIssuesPath, projectIssuesPath,
projectId, projectId,
glFeatures,
}, },
data() { data() {
return { alert: { ...mockAlert }, sidebarStatus: false, ...data }; return {
alert: {
...mockAlert,
environment: environmentData,
},
sidebarStatus: false,
...data,
};
}, },
mocks: { mocks: {
$apollo: { $apollo: {
...@@ -72,7 +87,8 @@ describe('AlertDetails', () => { ...@@ -72,7 +87,8 @@ describe('AlertDetails', () => {
const findCreateIncidentBtn = () => wrapper.findByTestId('createIncidentBtn'); const findCreateIncidentBtn = () => wrapper.findByTestId('createIncidentBtn');
const findViewIncidentBtn = () => wrapper.findByTestId('viewIncidentBtn'); const findViewIncidentBtn = () => wrapper.findByTestId('viewIncidentBtn');
const findIncidentCreationAlert = () => wrapper.findByTestId('incidentCreationError'); const findIncidentCreationAlert = () => wrapper.findByTestId('incidentCreationError');
const findEnvironmentLink = () => wrapper.findByTestId('environmentUrl'); const findEnvironmentName = () => wrapper.findByTestId('environmentName');
const findEnvironmentPath = () => wrapper.findByTestId('environmentPath');
const findDetailsTable = () => wrapper.find(AlertDetailsTable); const findDetailsTable = () => wrapper.find(AlertDetailsTable);
describe('Alert details', () => { describe('Alert details', () => {
...@@ -120,8 +136,6 @@ describe('AlertDetails', () => { ...@@ -120,8 +136,6 @@ describe('AlertDetails', () => {
field | data | isShown field | data | isShown
${'eventCount'} | ${1} | ${true} ${'eventCount'} | ${1} | ${true}
${'eventCount'} | ${undefined} | ${false} ${'eventCount'} | ${undefined} | ${false}
${'environment'} | ${undefined} | ${false}
${'environment'} | ${'Production'} | ${true}
${'monitoringTool'} | ${'New Relic'} | ${true} ${'monitoringTool'} | ${'New Relic'} | ${true}
${'monitoringTool'} | ${undefined} | ${false} ${'monitoringTool'} | ${undefined} | ${false}
${'service'} | ${'Prometheus'} | ${true} ${'service'} | ${'Prometheus'} | ${true}
...@@ -144,16 +158,34 @@ describe('AlertDetails', () => { ...@@ -144,16 +158,34 @@ describe('AlertDetails', () => {
}); });
}); });
describe('environment URL fields', () => { describe('environment fields', () => {
it('should show the environment URL when available', () => { describe('when exposeEnvironmentPathInAlertDetails is disabled', () => {
const environment = 'Production'; beforeEach(mountComponent);
const environmentUrl = 'fake/url';
mountComponent({ it('should not show the environment', () => {
data: { alert: { ...mockAlert, environment, environmentUrl } }, expect(findEnvironmentName().exists()).toBe(false);
expect(findEnvironmentPath().exists()).toBe(false);
});
}); });
expect(findEnvironmentLink().text()).toBe(environment); describe('when exposeEnvironmentPathInAlertDetails is enabled', () => {
expect(findEnvironmentLink().attributes('href')).toBe(environmentUrl); beforeEach(() => {
glFeatures = { exposeEnvironmentPathInAlertDetails: true };
mountComponent();
});
it('should show the environment name with link to path', () => {
expect(findEnvironmentName().exists()).toBe(false);
expect(findEnvironmentPath().text()).toBe(environmentName);
expect(findEnvironmentPath().attributes('href')).toBe(environmentPath);
});
it('should only show the environment name if the path is not provided', () => {
environmentData = { name: environmentName, path: null };
mountComponent();
expect(findEnvironmentPath().exists()).toBe(false);
expect(findEnvironmentName().text()).toBe(environmentName);
});
}); });
}); });
......
...@@ -18,13 +18,24 @@ const mockAlert = { ...@@ -18,13 +18,24 @@ const mockAlert = {
__typename: 'AlertManagementAlert', __typename: 'AlertManagementAlert',
}; };
const environmentName = 'Production';
const environmentPath = '/fake/path';
describe('AlertDetails', () => { describe('AlertDetails', () => {
let environmentData = { name: environmentName, path: environmentPath };
let glFeatures = { exposeEnvironmentPathInAlertDetails: false };
let wrapper; let wrapper;
function mountComponent(propsData = {}) { function mountComponent(propsData = {}) {
wrapper = mount(AlertDetailsTable, { wrapper = mount(AlertDetailsTable, {
provide: {
glFeatures,
},
propsData: { propsData: {
alert: mockAlert, alert: {
...mockAlert,
environment: environmentData,
},
loading: false, loading: false,
...propsData, ...propsData,
}, },
...@@ -38,6 +49,12 @@ describe('AlertDetails', () => { ...@@ -38,6 +49,12 @@ describe('AlertDetails', () => {
const findTableComponent = () => wrapper.find(GlTable); const findTableComponent = () => wrapper.find(GlTable);
const findTableKeys = () => findTableComponent().findAll('tbody td:first-child'); const findTableKeys = () => findTableComponent().findAll('tbody td:first-child');
const findTableFieldValueByKey = fieldKey =>
findTableComponent()
.findAll('tbody tr')
.filter(row => row.text().includes(fieldKey))
.at(0)
.find('td:nth-child(2)');
const findTableField = (fields, fieldName) => fields.filter(row => row.text() === fieldName); const findTableField = (fields, fieldName) => fields.filter(row => row.text() === fieldName);
describe('Alert details', () => { describe('Alert details', () => {
...@@ -62,11 +79,7 @@ describe('AlertDetails', () => { ...@@ -62,11 +79,7 @@ describe('AlertDetails', () => {
}); });
describe('with table data', () => { describe('with table data', () => {
const environment = 'myEnvironment'; beforeEach(mountComponent);
const environmentUrl = 'fake/url';
beforeEach(() => {
mountComponent({ alert: { ...mockAlert, environment, environmentUrl } });
});
it('renders a table', () => { it('renders a table', () => {
expect(findTableComponent().exists()).toBe(true); expect(findTableComponent().exists()).toBe(true);
...@@ -83,18 +96,43 @@ describe('AlertDetails', () => { ...@@ -83,18 +96,43 @@ describe('AlertDetails', () => {
expect(findTableField(fields, 'Title').exists()).toBe(true); expect(findTableField(fields, 'Title').exists()).toBe(true);
expect(findTableField(fields, 'Severity').exists()).toBe(true); expect(findTableField(fields, 'Severity').exists()).toBe(true);
expect(findTableField(fields, 'Status').exists()).toBe(true); expect(findTableField(fields, 'Status').exists()).toBe(true);
expect(findTableField(fields, 'Environment').exists()).toBe(true);
expect(findTableField(fields, 'Hosts').exists()).toBe(true); expect(findTableField(fields, 'Hosts').exists()).toBe(true);
expect(findTableField(fields, 'Environment').exists()).toBe(false);
}); });
it('should not show disallowed alert fields', () => { it('should not show disallowed and flaggedAllowed alert fields', () => {
const fields = findTableKeys(); const fields = findTableKeys();
expect(findTableField(fields, 'Typename').exists()).toBe(false); expect(findTableField(fields, 'Typename').exists()).toBe(false);
expect(findTableField(fields, 'Todos').exists()).toBe(false); expect(findTableField(fields, 'Todos').exists()).toBe(false);
expect(findTableField(fields, 'Notes').exists()).toBe(false); expect(findTableField(fields, 'Notes').exists()).toBe(false);
expect(findTableField(fields, 'Assignees').exists()).toBe(false); expect(findTableField(fields, 'Assignees').exists()).toBe(false);
expect(findTableField(fields, 'EnvironmentUrl').exists()).toBe(false); expect(findTableField(fields, 'Environment').exists()).toBe(false);
});
});
describe('when exposeEnvironmentPathInAlertDetails is enabled', () => {
beforeEach(() => {
glFeatures = { exposeEnvironmentPathInAlertDetails: true };
mountComponent();
});
it('should show flaggedAllowed alert fields', () => {
const fields = findTableKeys();
expect(findTableField(fields, 'Environment').exists()).toBe(true);
});
it('should display only the name for the environment', () => {
expect(findTableFieldValueByKey('Iid').text()).toBe('1527542');
expect(findTableFieldValueByKey('Environment').text()).toBe(environmentName);
});
it('should not display the environment row if there is not data', () => {
environmentData = { name: null, path: null };
mountComponent();
expect(findTableFieldValueByKey('Environment').text()).toBeFalsy();
}); });
}); });
}); });
......
...@@ -32,6 +32,7 @@ RSpec.describe GitlabSchema.types['AlertManagementAlert'] do ...@@ -32,6 +32,7 @@ RSpec.describe GitlabSchema.types['AlertManagementAlert'] do
todos todos
details_url details_url
prometheus_alert prometheus_alert
environment
] ]
expect(described_class).to have_graphql_fields(*expected_fields) expect(described_class).to have_graphql_fields(*expected_fields)
......
...@@ -7,7 +7,7 @@ RSpec.describe GitlabSchema.types['Environment'] do ...@@ -7,7 +7,7 @@ RSpec.describe GitlabSchema.types['Environment'] do
it 'has the expected fields' do it 'has the expected fields' do
expected_fields = %w[ expected_fields = %w[
name id state metrics_dashboard latest_opened_most_severe_alert name id state metrics_dashboard latest_opened_most_severe_alert path
] ]
expect(described_class).to have_graphql_fields(*expected_fields) expect(described_class).to have_graphql_fields(*expected_fields)
...@@ -28,6 +28,7 @@ RSpec.describe GitlabSchema.types['Environment'] do ...@@ -28,6 +28,7 @@ RSpec.describe GitlabSchema.types['Environment'] do
project(fullPath: "#{project.full_path}") { project(fullPath: "#{project.full_path}") {
environment(name: "#{environment.name}") { environment(name: "#{environment.name}") {
name name
path
state state
} }
} }
...@@ -43,6 +44,18 @@ RSpec.describe GitlabSchema.types['Environment'] do ...@@ -43,6 +44,18 @@ RSpec.describe GitlabSchema.types['Environment'] do
expect(subject['data']['project']['environment']['name']).to eq(environment.name) expect(subject['data']['project']['environment']['name']).to eq(environment.name)
end end
it 'returns the path when the feature is enabled' do
expect(subject['data']['project']['environment']['path']).to eq(
Gitlab::Routing.url_helpers.project_environment_path(project, environment)
)
end
it 'does not return the path when the feature is disabled' do
stub_feature_flags(expose_environment_path_in_alert_details: false)
expect(subject['data']['project']['environment']['path']).to be_nil
end
context 'when query alert data for the environment' do context 'when query alert data for the environment' do
let_it_be(:query) do let_it_be(:query) do
%( %(
......
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