提交 4e33606f 编写于 作者: G GitLab Bot

Add latest changes from gitlab-org/gitlab@master

上级 6c516c90
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import ListIssue from 'ee_else_ce/boards/models/issue';
export function getMilestone() {
return null;
}
export function formatListIssues(listIssues) {
return listIssues.nodes.reduce((map, list) => {
return {
...map,
[list.id]: list.issues.nodes.map(
i =>
new ListIssue({
...i,
id: getIdFromGraphQLId(i.id),
labels: i.labels?.nodes || [],
assignees: i.assignees?.nodes || [],
}),
),
};
}, {});
}
export default {
getMilestone,
formatListIssues,
};
......@@ -42,7 +42,7 @@ export default {
},
},
computed: {
...mapState(['isShowingEpicsSwimlanes']),
...mapState(['isShowingEpicsSwimlanes', 'boardLists']),
isSwimlanesOn() {
return this.glFeatures.boardsWithSwimlanes && this.isShowingEpicsSwimlanes;
},
......@@ -73,11 +73,12 @@ export default {
<epics-swimlanes
v-else
ref="swimlanes"
:lists="lists"
:lists="boardLists"
:can-admin-list="canAdminList"
:disabled="disabled"
:board-id="boardId"
:group-id="groupId"
:root-path="rootPath"
/>
</div>
</template>
......@@ -117,7 +117,7 @@ export default () => {
boardId: this.boardId,
fullPath: $boardApp.dataset.fullPath,
};
this.setEndpoints(endpoints);
this.setInitialBoardData({ ...endpoints, boardType: this.parent });
boardsStore.setEndpoints(endpoints);
boardsStore.rootPath = this.boardsEndpoint;
......@@ -189,7 +189,7 @@ export default () => {
}
},
methods: {
...mapActions(['setEndpoints']),
...mapActions(['setInitialBoardData']),
updateTokens() {
this.filterManager.updateTokens();
},
......
......@@ -60,7 +60,9 @@ class List {
this.title = this.milestone.title;
}
if (!typeInfo.isBlank && this.id) {
// doNotFetchIssues is a temporary workaround until issues are fetched using GraphQL on issue boards
// Issue: https://gitlab.com/gitlab-org/gitlab/-/issues/229416
if (!typeInfo.isBlank && this.id && !obj.doNotFetchIssues) {
this.getIssues().catch(() => {
// TODO: handle request error
});
......
......@@ -4,6 +4,7 @@ fragment BoardListShared on BoardList {
position
listType
collapsed
maxIssueCount
label {
id
title
......
#import "./issue.fragment.graphql"
query GroupListIssues($fullPath: ID!, $boardId: ID!) {
group(fullPath: $fullPath) {
board(id: $boardId) {
lists {
nodes {
id
issues {
nodes {
...IssueNode
}
}
}
}
}
}
}
#import "~/graphql_shared/fragments/user.fragment.graphql"
fragment IssueNode on Issue {
id
iid
title
referencePath: reference(full: true)
dueDate
timeEstimate
weight
confidential
webUrl
subscribed
blocked
epic {
id
}
assignees {
nodes {
...User
}
}
labels {
nodes {
id
title
color
description
}
}
}
#import "./issue.fragment.graphql"
query ProjectListIssues($fullPath: ID!, $boardId: ID!) {
project(fullPath: $fullPath) {
board(id: $boardId) {
lists {
nodes {
id
issues {
nodes {
...IssueNode
}
}
}
}
}
}
}
import * as types from './mutation_types';
import createDefaultClient from '~/lib/graphql';
import { BoardType } from '~/boards/constants';
import { formatListIssues } from '../boards_util';
import groupListsIssuesQuery from '../queries/group_lists_issues.query.graphql';
import projectListsIssuesQuery from '../queries/project_lists_issues.query.graphql';
const gqlClient = createDefaultClient();
const notImplemented = () => {
/* eslint-disable-next-line @gitlab/require-i18n-strings */
......@@ -6,8 +13,8 @@ const notImplemented = () => {
};
export default {
setEndpoints: ({ commit }, endpoints) => {
commit(types.SET_ENDPOINTS, endpoints);
setInitialBoardData: ({ commit }, data) => {
commit(types.SET_INITIAL_BOARD_DATA, data);
},
setActiveId({ commit }, id) {
......@@ -38,6 +45,32 @@ export default {
notImplemented();
},
fetchIssuesForAllLists: ({ state, commit }) => {
commit(types.REQUEST_ISSUES_FOR_ALL_LISTS);
const { endpoints, boardType } = state;
const { fullPath, boardId } = endpoints;
const query = boardType === BoardType.group ? groupListsIssuesQuery : projectListsIssuesQuery;
const variables = {
fullPath,
boardId: `gid://gitlab/Board/${boardId}`,
};
return gqlClient
.query({
query,
variables,
})
.then(({ data }) => {
const { lists } = data[boardType]?.board;
const listIssues = formatListIssues(lists);
commit(types.RECEIVE_ISSUES_FOR_ALL_LISTS_SUCCESS, listIssues);
})
.catch(() => commit(types.RECEIVE_ISSUES_FOR_ALL_LISTS_FAILURE));
},
moveIssue: () => {
notImplemented();
},
......
......@@ -81,7 +81,7 @@ const boardsStore = {
showPage(page) {
this.state.currentPage = page;
},
addList(listObj) {
updateListPosition(listObj) {
const listType = listObj.listType || listObj.list_type;
let { position } = listObj;
if (listType === ListType.closed) {
......@@ -91,6 +91,10 @@ const boardsStore = {
}
const list = new List({ ...listObj, position });
return list;
},
addList(listObj) {
const list = this.updateListPosition(listObj);
this.state.lists = sortBy([...this.state.lists, list], 'position');
return list;
},
......@@ -850,19 +854,28 @@ const boardsStore = {
},
refreshIssueData(issue, obj) {
issue.id = obj.id;
issue.iid = obj.iid;
issue.title = obj.title;
issue.confidential = obj.confidential;
issue.dueDate = obj.due_date;
issue.sidebarInfoEndpoint = obj.issue_sidebar_endpoint;
issue.referencePath = obj.reference_path;
issue.path = obj.real_path;
issue.toggleSubscriptionEndpoint = obj.toggle_subscription_endpoint;
// issue.id = obj.id;
// issue.iid = obj.iid;
// issue.title = obj.title;
// issue.confidential = obj.confidential;
// issue.dueDate = obj.due_date || obj.dueDate;
// issue.sidebarInfoEndpoint = obj.issue_sidebar_endpoint;
// issue.referencePath = obj.reference_path || obj.referencePath;
// issue.path = obj.real_path || obj.webUrl;
// issue.toggleSubscriptionEndpoint = obj.toggle_subscription_endpoint;
// issue.project_id = obj.project_id;
// issue.timeEstimate = obj.time_estimate || obj.timeEstimate;
// issue.assignableLabelsEndpoint = obj.assignable_labels_endpoint;
// issue.blocked = obj.blocked;
// issue.epic = obj.epic;
const convertedObj = convertObjectPropsToCamelCase(obj, {
dropKeys: ['issue_sidebar_endpoint', 'real_path', 'webUrl'],
});
convertedObj.sidebarInfoEndpoint = obj.issue_sidebar_endpoint;
issue.path = obj.real_path || obj.webUrl;
issue.project_id = obj.project_id;
issue.timeEstimate = obj.time_estimate;
issue.assignableLabelsEndpoint = obj.assignable_labels_endpoint;
issue.blocked = obj.blocked;
Object.assign(issue, convertedObj);
if (obj.project) {
issue.project = new IssueProject(obj.project);
......
export const SET_ENDPOINTS = 'SET_ENDPOINTS';
export const SET_INITIAL_BOARD_DATA = 'SET_INITIAL_BOARD_DATA';
export const REQUEST_ADD_LIST = 'REQUEST_ADD_LIST';
export const RECEIVE_ADD_LIST_SUCCESS = 'RECEIVE_ADD_LIST_SUCCESS';
export const RECEIVE_ADD_LIST_ERROR = 'RECEIVE_ADD_LIST_ERROR';
......@@ -8,6 +8,9 @@ export const RECEIVE_UPDATE_LIST_ERROR = 'RECEIVE_UPDATE_LIST_ERROR';
export const REQUEST_REMOVE_LIST = 'REQUEST_REMOVE_LIST';
export const RECEIVE_REMOVE_LIST_SUCCESS = 'RECEIVE_REMOVE_LIST_SUCCESS';
export const RECEIVE_REMOVE_LIST_ERROR = 'RECEIVE_REMOVE_LIST_ERROR';
export const REQUEST_ISSUES_FOR_ALL_LISTS = 'REQUEST_ISSUES_FOR_ALL_LISTS';
export const RECEIVE_ISSUES_FOR_ALL_LISTS_SUCCESS = 'RECEIVE_ISSUES_FOR_ALL_LISTS_SUCCESS';
export const RECEIVE_ISSUES_FOR_ALL_LISTS_FAILURE = 'RECEIVE_ISSUES_FOR_ALL_LISTS_FAILURE';
export const REQUEST_ADD_ISSUE = 'REQUEST_ADD_ISSUE';
export const RECEIVE_ADD_ISSUE_SUCCESS = 'RECEIVE_ADD_ISSUE_SUCCESS';
export const RECEIVE_ADD_ISSUE_ERROR = 'RECEIVE_ADD_ISSUE_ERROR';
......
......@@ -6,8 +6,10 @@ const notImplemented = () => {
};
export default {
[mutationTypes.SET_ENDPOINTS]: (state, endpoints) => {
[mutationTypes.SET_INITIAL_BOARD_DATA]: (state, data) => {
const { boardType, ...endpoints } = data;
state.endpoints = endpoints;
state.boardType = boardType;
},
[mutationTypes.SET_ACTIVE_ID](state, id) {
......@@ -50,6 +52,20 @@ export default {
notImplemented();
},
[mutationTypes.REQUEST_ISSUES_FOR_ALL_LISTS]: state => {
state.isLoadingIssues = true;
},
[mutationTypes.RECEIVE_ISSUES_FOR_ALL_LISTS_SUCCESS]: (state, listIssues) => {
state.issuesByListId = listIssues;
state.isLoadingIssues = false;
},
[mutationTypes.RECEIVE_ISSUES_FOR_ALL_LISTS_FAILURE]: state => {
state.listIssueFetchFailure = true;
state.isLoadingIssues = false;
},
[mutationTypes.REQUEST_ADD_ISSUE]: () => {
notImplemented();
},
......
......@@ -2,6 +2,10 @@ import { inactiveId } from '~/boards/constants';
export default () => ({
endpoints: {},
boardType: null,
isShowingLabels: true,
activeId: inactiveId,
issuesByListId: {},
isLoadingIssues: false,
listIssueFetchFailure: false,
});
......@@ -302,7 +302,6 @@ export default {
/>
</gl-form-group>
<gl-form-group
v-if="glFeatures.alertRunbooks"
:label="s__('PrometheusAlerts|Runbook URL (optional)')"
label-for="alert-runbook"
>
......@@ -312,6 +311,7 @@ export default {
:disabled="formDisabled"
data-testid="alertRunbookField"
type="text"
:placeholder="s__('PrometheusAlerts|https://gitlab.com/gitlab-com/runbooks')"
/>
</gl-form-group>
</div>
......
<script>
import { mapState } from 'vuex';
import { pickBy } from 'lodash';
import { mapValues, pickBy } from 'lodash';
import invalidUrl from '~/lib/utils/invalid_url';
import { convertToFixedRange } from '~/lib/utils/datetime_range';
import { relativePathToAbsolute, getBaseURL, visitUrl, isSafeURL } from '~/lib/utils/url_utility';
import {
GlResizeObserverDirective,
GlIcon,
GlLink,
GlLoadingIcon,
GlNewDropdown as GlDropdown,
GlNewDropdownItem as GlDropdownItem,
GlNewDropdownDivider as GlDropdownDivider,
GlModal,
GlModalDirective,
GlSprintf,
GlTooltip,
GlTooltipDirective,
} from '@gitlab/ui';
......@@ -44,12 +46,14 @@ export default {
MonitorEmptyChart,
AlertWidget,
GlIcon,
GlLink,
GlLoadingIcon,
GlTooltip,
GlDropdown,
GlDropdownItem,
GlDropdownDivider,
GlModal,
GlSprintf,
},
directives: {
GlResizeObserver: GlResizeObserverDirective,
......@@ -341,6 +345,19 @@ export default {
this.$refs.copyChartLink.$el.firstChild.click();
}
},
getAlertRunbooks(queries) {
const hasRunbook = alert => Boolean(alert.runbookUrl);
const graphAlertsWithRunbooks = pickBy(this.getGraphAlerts(queries), hasRunbook);
const alertToRunbookTransform = alert => {
const alertQuery = queries.find(query => query.metricId === alert.metricId);
return {
key: alert.metricId,
href: alert.runbookUrl,
label: alertQuery.label,
};
};
return mapValues(graphAlertsWithRunbooks, alertToRunbookTransform);
},
},
panelTypes,
};
......@@ -436,6 +453,25 @@ export default {
>
{{ __('Alerts') }}
</gl-dropdown-item>
<gl-dropdown-item
v-for="runbook in getAlertRunbooks(graphData.metrics)"
:key="runbook.key"
:href="safeUrl(runbook.href)"
data-testid="runbookLink"
target="_blank"
rel="noopener noreferrer"
>
<span class="gl-display-flex gl-justify-content-space-between gl-align-items-center">
<span>
<gl-sprintf :message="s__('Metrics|View runbook - %{label}')">
<template #label>
{{ runbook.label }}
</template>
</gl-sprintf>
</span>
<gl-icon name="external-link" />
</span>
</gl-dropdown-item>
<template v-if="graphData.links && graphData.links.length">
<gl-dropdown-divider />
......
......@@ -598,17 +598,17 @@
$epic-icons-spacing: 40px;
.board-epic-lane {
max-width: calc(100vw - #{$contextual-sidebar-width} - $epic-icons-spacing);
max-width: calc(100vw - #{$contextual-sidebar-width} - #{$epic-icons-spacing});
.page-with-icon-sidebar & {
max-width: calc(100vw - #{$contextual-sidebar-collapsed-width} - $epic-icons-spacing);
max-width: calc(100vw - #{$contextual-sidebar-collapsed-width} - #{$epic-icons-spacing});
}
.page-with-icon-sidebar .is-compact & {
max-width: calc(100vw - #{$contextual-sidebar-collapsed-width} - #{$gutter-width} - $epic-icons-spacing);
max-width: calc(100vw - #{$contextual-sidebar-collapsed-width} - #{$gutter-width} - #{$epic-icons-spacing});
}
.is-compact & {
max-width: calc(100vw - #{$contextual-sidebar-width} - #{$gutter-width} - $epic-icons-spacing);
max-width: calc(100vw - #{$contextual-sidebar-width} - #{$gutter-width} - #{$epic-icons-spacing});
}
}
......@@ -98,19 +98,10 @@
}
}
.refresh-dashboard-button {
margin-top: 22px;
@media(max-width: map-get($grid-breakpoints, sm)) {
margin-top: 0;
}
}
.metric-area {
opacity: 0.25;
}
.rect-text-metric {
fill: $white;
stroke-width: 1;
......
......@@ -14,7 +14,6 @@ class Projects::EnvironmentsController < Projects::ApplicationController
push_frontend_feature_flag(:prometheus_computed_alerts)
push_frontend_feature_flag(:disable_metric_dashboard_refresh_rate)
push_frontend_feature_flag(:alert_runbooks)
push_frontend_feature_flag(:metrics_dashboard_new_panel_page)
end
before_action :authorize_read_environment!, except: [:metrics, :additional_metrics, :metrics_dashboard, :metrics_redirect]
......
......@@ -10,7 +10,6 @@ module Projects
before_action do
push_frontend_feature_flag(:prometheus_computed_alerts)
push_frontend_feature_flag(:disable_metric_dashboard_refresh_rate)
push_frontend_feature_flag(:alert_runbooks)
push_frontend_feature_flag(:metrics_dashboard_new_panel_page)
end
......
---
title: Add runbook to metric chart dropdown
merge_request: 39288
author:
type: added
---
title: Add runbooks to metric alerts
merge_request: 39315
author:
type: added
......@@ -631,6 +631,35 @@ or `gitlab-ctl promote-to-primary-node`, either:
bug](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/22021) was
fixed.
If the above does not work, another possible reason is that you have paused replication
from the original primary node before attempting to promote this node.
To double check this, you can do the following:
- Get the current secondary node's ID using:
```shell
sudo gitlab-rails runner 'puts GeoNode.current_node.id'
```
- Double check that the node is active through the database by running the following
on the secondary node, replacing `ID_FROM_ABOVE`:
```shell
sudo gitlab-rails dbconsole
SELECT enabled FROM geo_nodes WHERE id = ID_FROM_ABOVE;
```
- If the above returned `f` it means that the replication was paused.
You can re-enable it through an `UPDATE` statement in the database:
```shell
sudo gitlab-rails dbconsole
UPDATE geo_nodes SET enabled = 't' WHERE id = ID_FROM_ABOVE;
```
### Message: ``NoMethodError: undefined method `secondary?' for nil:NilClass``
When [promoting a **secondary** node](../disaster_recovery/index.md#step-3-promoting-a-secondary-node),
......
......@@ -902,7 +902,7 @@ curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://git
## Reorder an issue
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/211864) as a [community contribution](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/35349) in GitLab 13.2.
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/211864) in GitLab 13.2.
Reorders an issue, you can see the results when sorting issues manually
......
......@@ -14,8 +14,7 @@ This API is a project-specific version of these endpoints:
- [GitLab CI/CD Configuration templates](templates/gitlab_ci_ymls.md)
- [Open source license templates](templates/licenses.md)
- [Issue and merge request templates](../user/project/description_templates.md)
([introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/37890) in GitLab 13.3 as a
community contribution)
([introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/37890) in GitLab 13.3)
It deprecates these endpoints, which will be removed for API version 5.
......
......@@ -213,11 +213,47 @@ Arguments:
- `counter`: a counter from `Gitlab::UsageDataCounters`, that has `fallback_totals` method implemented
- or a `block`: which is evaluated
#### Ordinary Redis Counters
Examples of implementation:
- Using Redis methods [`INCR`](https://redis.io/commands/incr), [`GET`](https://redis.io/commands/get), and [`Gitlab::UsageDataCounters::WikiPageCounter`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/usage_data_counters/wiki_page_counter.rb)
- Using Redis methods [`HINCRBY`](https://redis.io/commands/hincrby), [`HGETALL`](https://redis.io/commands/hgetall), and [`Gitlab::UsageCounters::PodLogs`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/usage_counters/pod_logs.rb)
#### Redis HLL Counters
With `Gitlab::Redis::HLL` we have available data structures used to count unique values.
Implemented using Redis methods [PFADD](https://redis.io/commands/pfadd) and [PFCOUNT](https://redis.io/commands/pfcount).
Recommendations:
- Key should expire in 29 days.
- If possible, data granularity should be a week. For example a key could be composed from the metric's name and week of the year, `2020-33-{metric_name}`.
- Use a [feature flag](../../operations/feature_flags.md) in order to have a control over the impact when adding new metrics.
- If possible, data granularity should be week, for example a key could be composed from metric name and week of the year, 2020-33-{metric_name}
- Use a [feature flag](../../operations/feature_flags.md) in order to have a control over the impact when adding new metrics
Examples of implementation:
- [`Gitlab::UsageDataCounters::TrackUniqueActions`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/usage_data_counters/track_unique_actions.rb)
- [`Gitlab::Analytics::UniqueVisits`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/analytics/unique_visits.rb)
Example of usage:
```ruby
# Redis Counters
redis_usage_data(Gitlab::UsageDataCounters::WikiPageCounter)
redis_usage_data { ::Gitlab::UsageCounters::PodLogs.usage_totals[:total] }
# Redis HLL counter
counter = Gitlab::UsageDataCounters::TrackUniqueActions
redis_usage_data do
counter.count_unique_events(
event_action: Gitlab::UsageDataCounters::TrackUniqueActions::PUSH_ACTION,
date_from: time_period[:created_at].first,
date_to: time_period[:created_at].last
)
```
Note that Redis counters are in the [process of being deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/216330) and you should instead try to use Snowplow events instead. We're in the process of building [self-managed event tracking](https://gitlab.com/gitlab-org/telemetry/-/issues/373) and once this is available, we will convert all Redis counters into Snowplow events.
......
......@@ -13,7 +13,8 @@ your team when environment performance falls outside of the boundaries you set.
## Managed Prometheus instances
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/6590) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 11.2 for [custom metrics](index.md#adding-custom-metrics), and 11.3 for [library metrics](../../user/project/integrations/prometheus_library/metrics.md).
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/6590) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 11.2 for [custom metrics](index.md#adding-custom-metrics), and GitLab 11.3 for [library metrics](../../user/project/integrations/prometheus_library/metrics.md).
> - Runbook URLs [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/39315) in GitLab 13.3.
For managed Prometheus instances using auto configuration, you can
[configure alerts for metrics](index.md#adding-custom-metrics) directly in the
......@@ -24,6 +25,7 @@ For managed Prometheus instances using auto configuration, you can
**ellipsis** **{ellipsis_v}** icon in the top right corner of the metric.
1. Choose **Alerts**.
1. Set threshold and operator.
1. (Optional) Add a Runbook URL.
1. Click **Add** to save and activate the alert.
![Adding an alert](img/prometheus_alert.png)
......
......@@ -420,7 +420,7 @@ It's possible to generate diagrams and flowcharts from text in GitLab using
#### Mermaid
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/31818) as a [community contribution](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/36127) in GitLab 13.3.
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/31818) in GitLab 13.3.
Visit the [official page](https://mermaidjs.github.io/) for more details.
If you're new to using Mermaid or need help identifying issues in your Mermaid code,
......
......@@ -15433,6 +15433,9 @@ msgstr ""
msgid "Metrics|View logs"
msgstr ""
msgid "Metrics|View runbook - %{label}"
msgstr ""
msgid "Metrics|Y-axis label"
msgstr ""
......@@ -19429,6 +19432,9 @@ msgstr ""
msgid "PrometheusAlerts|Threshold"
msgstr ""
msgid "PrometheusAlerts|https://gitlab.com/gitlab-com/runbooks"
msgstr ""
msgid "PrometheusService|%{exporters} with %{metrics} were found"
msgstr ""
......@@ -24476,7 +24482,7 @@ msgstr ""
msgid "There is already a repository with that name on disk"
msgstr ""
msgid "There is no data available."
msgid "There is no chart data available."
msgstr ""
msgid "There is no data available. Please change your selection."
......@@ -24647,6 +24653,9 @@ msgstr ""
msgid "There was an error when unsubscribing from this label."
msgstr ""
msgid "There was an error while fetching the chart data."
msgstr ""
msgid "There was an error while fetching value stream analytics data."
msgstr ""
......
......@@ -5,7 +5,7 @@ import '~/boards/models/assignee';
import '~/boards/models/issue';
import '~/boards/models/list';
import boardsStore from '~/boards/stores/boards_store';
import { setMockEndpoints } from './mock_data';
import { setMockEndpoints, mockIssue } from './mock_data';
describe('Issue model', () => {
let issue;
......@@ -14,28 +14,7 @@ describe('Issue model', () => {
setMockEndpoints();
boardsStore.create();
issue = new ListIssue({
title: 'Testing',
id: 1,
iid: 1,
confidential: false,
labels: [
{
id: 1,
title: 'test',
color: 'red',
description: 'testing',
},
],
assignees: [
{
id: 1,
name: 'name',
username: 'username',
avatar_url: 'http://avatar_url',
},
],
});
issue = new ListIssue(mockIssue);
});
it('has label', () => {
......
......@@ -92,6 +92,29 @@ export const mockMilestone = {
due_date: '2019-12-31',
};
export const mockIssue = {
title: 'Testing',
id: 1,
iid: 1,
confidential: false,
labels: [
{
id: 1,
title: 'test',
color: 'red',
description: 'testing',
},
],
assignees: [
{
id: 1,
name: 'name',
username: 'username',
avatar_url: 'http://avatar_url',
},
],
};
export const BoardsMockData = {
GET: {
'/test/-/boards/1/lists/300/issues?id=300&page=1': {
......
......@@ -9,18 +9,18 @@ const expectNotImplemented = action => {
});
};
describe('setEndpoints', () => {
it('sets endpoints object', () => {
const mockEndpoints = {
describe('setInitialBoardData', () => {
it('sets data object', () => {
const mockData = {
foo: 'bar',
bar: 'baz',
};
return testAction(
actions.setEndpoints,
mockEndpoints,
actions.setInitialBoardData,
mockData,
{},
[{ type: types.SET_ENDPOINTS, payload: mockEndpoints }],
[{ type: types.SET_INITIAL_BOARD_DATA, payload: mockData }],
[],
);
});
......
import mutations from '~/boards/stores/mutations';
import * as types from '~/boards/stores/mutation_types';
import defaultState from '~/boards/stores/state';
import { mockIssue } from '../mock_data';
const expectNotImplemented = action => {
it('is not implemented', () => {
......@@ -15,7 +15,7 @@ describe('Board Store Mutations', () => {
state = defaultState();
});
describe('SET_ENDPOINTS', () => {
describe('SET_INITIAL_BOARD_DATA', () => {
it('Should set initial Boards data to state', () => {
const endpoints = {
boardsEndpoint: '/boards/',
......@@ -25,15 +25,17 @@ describe('Board Store Mutations', () => {
boardId: 1,
fullPath: 'gitlab-org',
};
const boardType = 'group';
mutations[types.SET_ENDPOINTS](state, endpoints);
mutations.SET_INITIAL_BOARD_DATA(state, { ...endpoints, boardType });
expect(state.endpoints).toEqual(endpoints);
expect(state.boardType).toEqual(boardType);
});
});
describe('SET_ACTIVE_ID', () => {
it('updates aciveListId to be the value that is passed', () => {
it('updates activeListId to be the value that is passed', () => {
const expectedId = 1;
mutations.SET_ACTIVE_ID(state, expectedId);
......@@ -78,6 +80,35 @@ describe('Board Store Mutations', () => {
expectNotImplemented(mutations.RECEIVE_REMOVE_LIST_ERROR);
});
describe('REQUEST_ISSUES_FOR_ALL_LISTS', () => {
it('sets isLoadingIssues to true', () => {
expect(state.isLoadingIssues).toBe(false);
mutations.REQUEST_ISSUES_FOR_ALL_LISTS(state);
expect(state.isLoadingIssues).toBe(true);
});
});
describe('RECEIVE_ISSUES_FOR_ALL_LISTS_SUCCESS', () => {
it('sets isLoadingIssues to false and updates issuesByListId object', () => {
const listIssues = {
'1': [mockIssue],
};
state = {
...state,
isLoadingIssues: true,
issuesByListId: {},
};
mutations.RECEIVE_ISSUES_FOR_ALL_LISTS_SUCCESS(state, listIssues);
expect(state.isLoadingIssues).toBe(false);
expect(state.issuesByListId).toEqual(listIssues);
});
});
describe('REQUEST_ADD_ISSUE', () => {
expectNotImplemented(mutations.REQUEST_ADD_ISSUE);
});
......
......@@ -36,7 +36,7 @@ describe('AlertWidgetForm', () => {
configuredAlert: metricId,
};
function createComponent(props = {}, featureFlags = {}) {
function createComponent(props = {}) {
const propsData = {
...defaultProps,
...props,
......@@ -44,9 +44,6 @@ describe('AlertWidgetForm', () => {
wrapper = shallowMount(AlertWidgetForm, {
propsData,
provide: {
glFeatures: featureFlags,
},
stubs: {
GlModal: ModalStub,
},
......@@ -88,7 +85,7 @@ describe('AlertWidgetForm', () => {
});
it('emits a "create" event when form submitted without existing alert', async () => {
createComponent(defaultProps, { alertRunbooks: true });
createComponent(defaultProps);
modal().vm.$emit('shown');
......@@ -109,7 +106,7 @@ describe('AlertWidgetForm', () => {
});
it('resets form when modal is dismissed (hidden)', () => {
createComponent(defaultProps, { alertRunbooks: true });
createComponent(defaultProps);
modal().vm.$emit('shown');
......@@ -199,7 +196,7 @@ describe('AlertWidgetForm', () => {
it('emits "update" event when form changed', () => {
const updatedRunbookUrl = `${INVALID_URL}/test`;
createComponent(propsWithAlertData, { alertRunbooks: true });
createComponent(propsWithAlertData);
modal().vm.$emit('shown');
......@@ -231,15 +228,9 @@ describe('AlertWidgetForm', () => {
expect(submitButtonTrackingOpts()).toEqual(dataTrackingOptions.update);
});
describe('alert runbooks feature flag', () => {
it('hides the runbook field when the flag is disabled', () => {
createComponent(undefined, { alertRunbooks: false });
expect(findRunbookField().exists()).toBe(false);
});
it('shows the runbook field when the flag is enabled', () => {
createComponent(undefined, { alertRunbooks: true });
describe('alert runbooks', () => {
it('shows the runbook field', () => {
createComponent();
expect(findRunbookField().exists()).toBe(true);
});
......
......@@ -9,6 +9,7 @@ import AlertWidget from '~/monitoring/components/alert_widget.vue';
import DashboardPanel from '~/monitoring/components/dashboard_panel.vue';
import {
mockAlert,
mockLogsHref,
mockLogsPath,
mockNamespace,
......@@ -55,9 +56,10 @@ describe('Dashboard Panel', () => {
const findCtxMenu = () => wrapper.find({ ref: 'contextualMenu' });
const findMenuItems = () => wrapper.findAll(GlDropdownItem);
const findMenuItemByText = text => findMenuItems().filter(i => i.text() === text);
const findAlertsWidget = () => wrapper.find(AlertWidget);
const createWrapper = (props, options) => {
wrapper = shallowMount(DashboardPanel, {
const createWrapper = (props, { mountFn = shallowMount, ...options } = {}) => {
wrapper = mountFn(DashboardPanel, {
propsData: {
graphData,
settingsPath: dashboardProps.settingsPath,
......@@ -78,6 +80,9 @@ describe('Dashboard Panel', () => {
});
};
const setMetricsSavedToDb = val =>
monitoringDashboard.getters.metricsSavedToDb.mockReturnValue(val);
beforeEach(() => {
setTestTimeout(1000);
......@@ -601,10 +606,6 @@ describe('Dashboard Panel', () => {
});
describe('panel alerts', () => {
const setMetricsSavedToDb = val =>
monitoringDashboard.getters.metricsSavedToDb.mockReturnValue(val);
const findAlertsWidget = () => wrapper.find(AlertWidget);
beforeEach(() => {
mockGetterReturnValue('metricsSavedToDb', []);
......@@ -730,4 +731,60 @@ describe('Dashboard Panel', () => {
expect(findManageLinksItem().exists()).toBe(false);
});
});
describe('Runbook url', () => {
const findRunbookLinks = () => wrapper.findAll('[data-testid="runbookLink"]');
const { metricId } = graphData.metrics[0];
const { alert_path: alertPath } = mockAlert;
const mockRunbookAlert = {
...mockAlert,
metricId,
};
beforeEach(() => {
mockGetterReturnValue('metricsSavedToDb', []);
});
it('does not show a runbook link when alerts are not present', () => {
createWrapper();
expect(findRunbookLinks().length).toBe(0);
});
describe('when alerts are present', () => {
beforeEach(() => {
setMetricsSavedToDb([metricId]);
createWrapper({
alertsEndpoint: '/endpoint',
prometheusAlertsAvailable: true,
});
});
it('does not show a runbook link when a runbook is not set', async () => {
findAlertsWidget().vm.$emit('setAlerts', alertPath, {
...mockRunbookAlert,
runbookUrl: '',
});
await wrapper.vm.$nextTick();
expect(findRunbookLinks().length).toBe(0);
});
it('shows a runbook link when a runbook is set', async () => {
findAlertsWidget().vm.$emit('setAlerts', alertPath, mockRunbookAlert);
await wrapper.vm.$nextTick();
expect(findRunbookLinks().length).toBe(1);
expect(
findRunbookLinks()
.at(0)
.attributes('href'),
).toBe(invalidUrl);
});
});
});
});
import invalidUrl from '~/lib/utils/invalid_url';
// This import path needs to be relative for now because this mock data is used in
// Karma specs too, where the helpers/test_constants alias can not be resolved
import { TEST_HOST } from '../helpers/test_constants';
......@@ -630,3 +631,14 @@ export const dashboardActionsMenuProps = {
validateQueryPath: 'https://path/to/validateQuery',
isOotbDashboard: true,
};
export const mockAlert = {
alert_path: 'alert_path',
id: 8,
metricId: 'mock_metric_id',
operator: '>',
query: 'testQuery',
runbookUrl: invalidUrl,
threshold: 5,
title: 'alert title',
};
......@@ -55,7 +55,7 @@ describe('releases/components/tag_field_new', () => {
describe('"Tag name" field', () => {
describe('rendering and behavior', () => {
beforeEach(createComponent);
beforeEach(() => createComponent());
it('renders a label', () => {
expect(findTagNameFormGroup().attributes().label).toBe('Tag name');
......@@ -124,7 +124,7 @@ describe('releases/components/tag_field_new', () => {
});
describe('"Create from" field', () => {
beforeEach(createComponent);
beforeEach(() => createComponent());
it('renders a label', () => {
expect(findCreateFromFormGroup().attributes().label).toBe('Create from');
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册