Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-07-07 12:09:16 +00:00
parent c417764f00
commit 0254867cf0
111 changed files with 1500 additions and 390 deletions

View File

@ -164,6 +164,7 @@ gem 'diff_match_patch', '~> 0.1.0'
# Application server
gem 'rack', '~> 2.0.9'
gem 'rack-timeout', '~> 0.5.1'
group :unicorn do
gem 'unicorn', '~> 5.5'
@ -173,7 +174,6 @@ end
group :puma do
gem 'gitlab-puma', '~> 4.3.3.gitlab.2', require: false
gem 'gitlab-puma_worker_killer', '~> 0.1.1.gitlab.1', require: false
gem 'rack-timeout', require: false
end
# State machine

View File

@ -817,7 +817,7 @@ GEM
rack
rack-test (1.1.0)
rack (>= 1.0, < 3)
rack-timeout (0.5.1)
rack-timeout (0.5.2)
rails (6.0.3.1)
actioncable (= 6.0.3.1)
actionmailbox (= 6.0.3.1)
@ -1350,7 +1350,7 @@ DEPENDENCIES
rack-cors (~> 1.0.6)
rack-oauth2 (~> 1.9.3)
rack-proxy (~> 0.6.0)
rack-timeout
rack-timeout (~> 0.5.1)
rails (~> 6.0.3.1)
rails-controller-testing
rails-i18n (~> 6.0)

View File

@ -222,7 +222,7 @@ export default class Clusters {
initRemoveClusterActions() {
const el = document.querySelector('#js-cluster-remove-actions');
if (el && el.dataset) {
const { clusterName, clusterPath } = el.dataset;
const { clusterName, clusterPath, hasManagementProject } = el.dataset;
this.removeClusterAction = new Vue({
el,
@ -231,6 +231,7 @@ export default class Clusters {
props: {
clusterName,
clusterPath,
hasManagementProject,
},
});
},

View File

@ -1,7 +1,7 @@
<script>
import { escape } from 'lodash';
import SplitButton from '~/vue_shared/components/split_button.vue';
import { GlModal, GlDeprecatedButton, GlFormInput } from '@gitlab/ui';
import { GlModal, GlButton, GlDeprecatedButton, GlFormInput } from '@gitlab/ui';
import { s__, sprintf } from '~/locale';
import csrf from '~/lib/utils/csrf';
@ -27,6 +27,7 @@ export default {
components: {
SplitButton,
GlModal,
GlButton,
GlDeprecatedButton,
GlFormInput,
},
@ -39,6 +40,10 @@ export default {
type: String,
required: true,
},
hasManagementProject: {
type: Boolean,
required: false,
},
},
data() {
return {
@ -90,6 +95,9 @@ export default {
canSubmit() {
return this.enteredClusterName === this.clusterName;
},
canCleanupResources() {
return !this.hasManagementProject;
},
},
methods: {
handleClickRemoveCluster(cleanup = false) {
@ -112,12 +120,21 @@ export default {
<template>
<div>
<split-button
v-if="canCleanupResources"
:action-items="$options.splitButtonActionItems"
menu-class="dropdown-menu-large"
variant="danger"
@remove-cluster="handleClickRemoveCluster(false)"
@remove-cluster-and-cleanup="handleClickRemoveCluster(true)"
/>
<gl-button
v-else
variant="danger"
data-testid="btnRemove"
@click="handleClickRemoveCluster(false)"
>
{{ s__('ClusterIntegration|Remove integration') }}
</gl-button>
<gl-modal
ref="modal"
size="lg"

View File

@ -87,6 +87,7 @@ export default {
issuable_ids: this.form.find('input[name="update[issuable_ids]"]').val(),
subscription_event: this.form.find('input[name="update[subscription_event]"]').val(),
health_status: this.form.find('input[name="update[health_status]"]').val(),
epic_id: this.form.find('input[name="update[epic_id]"]').val(),
add_label_ids: [],
remove_label_ids: [],
},

View File

@ -71,6 +71,14 @@ export default class IssuableBulkUpdateSidebar {
})
.catch(() => {});
}
if (IS_EE) {
import('ee/vue_shared/components/sidebar/epics_select/epics_select_bundle')
.then(({ default: EpicSelect }) => {
EpicSelect();
})
.catch(() => {});
}
}
setupBulkUpdateActions() {

View File

@ -22,6 +22,7 @@ import Icon from '~/vue_shared/components/icon.vue';
import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue';
import DashboardsDropdown from './dashboards_dropdown.vue';
import RefreshButton from './refresh_button.vue';
import TrackEventDirective from '~/vue_shared/directives/track_event';
import { getAddMetricTrackingOptions, timeRangeToUrl } from '../utils';
@ -44,6 +45,7 @@ export default {
DateTimePicker,
DashboardsDropdown,
RefreshButton,
},
directives: {
GlModal: GlModalDirective,
@ -129,11 +131,7 @@ export default {
},
},
methods: {
...mapActions('monitoringDashboard', [
'filterEnvironments',
'fetchDashboardData',
'toggleStarredValue',
]),
...mapActions('monitoringDashboard', ['filterEnvironments', 'toggleStarredValue']),
selectDashboard(dashboard) {
const params = {
dashboard: encodeURIComponent(dashboard.path),
@ -149,9 +147,6 @@ export default {
onDateTimePickerInvalid() {
this.$emit('dateTimePickerInvalid');
},
refreshDashboard() {
this.fetchDashboardData();
},
toggleRearrangingPanels() {
this.$emit('setRearrangingPanels', !this.isRearrangingPanels);
@ -252,16 +247,7 @@ export default {
</div>
<div class="mb-2 pr-2 d-flex d-sm-block">
<gl-deprecated-button
ref="refreshDashboardBtn"
v-gl-tooltip
class="flex-grow-1"
variant="default"
:title="s__('Metrics|Refresh dashboard')"
@click="refreshDashboard"
>
<icon name="retry" />
</gl-deprecated-button>
<refresh-button />
</div>
<div class="flex-grow-1"></div>

View File

@ -0,0 +1,163 @@
<script>
import { n__, __ } from '~/locale';
import { mapActions } from 'vuex';
import {
GlButtonGroup,
GlButton,
GlNewDropdown,
GlNewDropdownItem,
GlNewDropdownDivider,
GlTooltipDirective,
} from '@gitlab/ui';
const makeInterval = (length = 0, unit = 's') => {
const shortLabel = `${length}${unit}`;
switch (unit) {
case 'd':
return {
interval: length * 24 * 60 * 60 * 1000,
shortLabel,
label: n__('%d day', '%d days', length),
};
case 'h':
return {
interval: length * 60 * 60 * 1000,
shortLabel,
label: n__('%d hour', '%d hours', length),
};
case 'm':
return {
interval: length * 60 * 1000,
shortLabel,
label: n__('%d minute', '%d minutes', length),
};
case 's':
default:
return {
interval: length * 1000,
shortLabel,
label: n__('%d second', '%d seconds', length),
};
}
};
export default {
components: {
GlButtonGroup,
GlButton,
GlNewDropdown,
GlNewDropdownItem,
GlNewDropdownDivider,
},
directives: {
GlTooltip: GlTooltipDirective,
},
data() {
return {
refreshInterval: null,
timeoutId: null,
};
},
computed: {
dropdownText() {
return this.refreshInterval?.shortLabel ?? __('Off');
},
},
watch: {
refreshInterval() {
if (this.refreshInterval !== null) {
this.startAutoRefresh();
} else {
this.stopAutoRefresh();
}
},
},
destroyed() {
this.stopAutoRefresh();
},
methods: {
...mapActions('monitoringDashboard', ['fetchDashboardData']),
refresh() {
this.fetchDashboardData();
},
startAutoRefresh() {
const schedule = () => {
if (this.refreshInterval) {
this.timeoutId = setTimeout(this.startAutoRefresh, this.refreshInterval.interval);
}
};
this.stopAutoRefresh();
if (document.hidden) {
// Inactive tab? Skip fetch and schedule again
schedule();
} else {
// Active tab! Fetch data and then schedule when settled
// eslint-disable-next-line promise/catch-or-return
this.fetchDashboardData().finally(schedule);
}
},
stopAutoRefresh() {
clearTimeout(this.timeoutId);
this.timeoutId = null;
},
setRefreshInterval(option) {
this.refreshInterval = option;
},
removeRefreshInterval() {
this.refreshInterval = null;
},
isChecked(option) {
if (this.refreshInterval) {
return option.interval === this.refreshInterval.interval;
}
return false;
},
},
refreshIntervals: [
makeInterval(5),
makeInterval(10),
makeInterval(30),
makeInterval(5, 'm'),
makeInterval(30, 'm'),
makeInterval(1, 'h'),
makeInterval(2, 'h'),
makeInterval(12, 'h'),
makeInterval(1, 'd'),
],
};
</script>
<template>
<gl-button-group>
<gl-button
v-gl-tooltip
class="gl-flex-grow-1"
variant="default"
:title="s__('Metrics|Refresh dashboard')"
icon="retry"
@click="refresh"
/>
<gl-new-dropdown v-gl-tooltip :title="s__('Metrics|Set refresh rate')" :text="dropdownText">
<gl-new-dropdown-item
:is-check-item="true"
:is-checked="refreshInterval === null"
@click="removeRefreshInterval()"
>{{ __('Off') }}</gl-new-dropdown-item
>
<gl-new-dropdown-divider />
<gl-new-dropdown-item
v-for="(option, i) in $options.refreshIntervals"
:key="i"
:is-check-item="true"
:is-checked="isChecked(option)"
@click="setRefreshInterval(option)"
>{{ option.label }}</gl-new-dropdown-item
>
</gl-new-dropdown>
</gl-button-group>
</template>

View File

@ -12,6 +12,7 @@ const successMessageSelector = '.validation-success';
const pendingMessageSelector = '.validation-pending';
const unavailableMessageSelector = '.validation-error';
const suggestionsMessageSelector = '.gl-path-suggestions';
const inputGroupSelector = '.input-group';
export default class GroupPathValidator extends InputValidator {
constructor(opts = {}) {
@ -39,7 +40,7 @@ export default class GroupPathValidator extends InputValidator {
static validateGroupPathInput(inputDomElement) {
const groupPath = inputDomElement.value;
if (inputDomElement.checkValidity() && groupPath.length > 0) {
if (inputDomElement.checkValidity() && groupPath.length > 1) {
GroupPathValidator.setMessageVisibility(inputDomElement, pendingMessageSelector);
fetchGroupPathAvailability(groupPath)
@ -69,9 +70,10 @@ export default class GroupPathValidator extends InputValidator {
}
static setMessageVisibility(inputDomElement, messageSelector, isVisible = true) {
const messageElement = inputDomElement.parentElement.parentElement.querySelector(
messageSelector,
);
const messageElement = inputDomElement
.closest(inputGroupSelector)
.parentElement.querySelector(messageSelector);
messageElement.classList.toggle('hide', !isVisible);
}

View File

@ -55,7 +55,7 @@ export default {
)
}}
<gl-link ref="accessibility-issue-learn-more" :href="learnMoreUrl" target="_blank">{{
s__('AccessibilityReport|Learn More')
s__('AccessibilityReport|Learn more')
}}</gl-link>
</div>
{{ sprintf(s__('AccessibilityReport|Message: %{message}'), { message: issue.message }) }}

View File

@ -4,9 +4,9 @@ import { joinPaths, escapeFileUrl } from '~/lib/utils/url_utility';
import { __ } from '../../locale';
import Icon from '../../vue_shared/components/icon.vue';
import getRefMixin from '../mixins/get_ref';
import getProjectShortPath from '../queries/getProjectShortPath.query.graphql';
import getProjectPath from '../queries/getProjectPath.query.graphql';
import getPermissions from '../queries/getPermissions.query.graphql';
import projectShortPathQuery from '../queries/project_short_path.query.graphql';
import projetPathQuery from '../queries/project_path.query.graphql';
import permissionsQuery from '../queries/permissions.query.graphql';
const ROW_TYPES = {
header: 'header',
@ -23,13 +23,13 @@ export default {
},
apollo: {
projectShortPath: {
query: getProjectShortPath,
query: projectShortPathQuery,
},
projectPath: {
query: getProjectPath,
query: projetPathQuery,
},
userPermissions: {
query: getPermissions,
query: permissionsQuery,
variables() {
return {
projectPath: this.projectPath,

View File

@ -8,8 +8,8 @@ import TimeagoTooltip from '../../vue_shared/components/time_ago_tooltip.vue';
import CiIcon from '../../vue_shared/components/ci_icon.vue';
import ClipboardButton from '../../vue_shared/components/clipboard_button.vue';
import getRefMixin from '../mixins/get_ref';
import getProjectPath from '../queries/getProjectPath.query.graphql';
import pathLastCommit from '../queries/pathLastCommit.query.graphql';
import projectPathQuery from '../queries/project_path.query.graphql';
import pathLastCommitQuery from '../queries/path_last_commit.query.graphql';
export default {
components: {
@ -28,10 +28,10 @@ export default {
mixins: [getRefMixin],
apollo: {
projectPath: {
query: getProjectPath,
query: projectPathQuery,
},
commit: {
query: pathLastCommit,
query: pathLastCommitQuery,
variables() {
return {
projectPath: this.projectPath,
@ -102,7 +102,7 @@ export default {
<template v-else-if="commit">
<user-avatar-link
v-if="commit.author"
:link-href="commit.author.webUrl"
:link-href="commit.author.webPath"
:img-src="commit.author.avatarUrl"
:img-size="40"
class="avatar-cell"
@ -118,7 +118,7 @@ export default {
<div class="commit-detail flex-list">
<div class="commit-content qa-commit-content">
<gl-link
:href="commit.webUrl"
:href="commit.webPath"
:class="{ 'font-italic': !commit.message }"
class="commit-row-message item-title"
v-html="commit.titleHtml"
@ -135,7 +135,7 @@ export default {
<div class="committer">
<gl-link
v-if="commit.author"
:href="commit.author.webUrl"
:href="commit.author.webPath"
class="commit-author-link js-user-link"
>
{{ commit.author.name }}

View File

@ -3,15 +3,15 @@ import $ from 'jquery';
import '~/behaviors/markdown/render_gfm';
import { GlLink, GlLoadingIcon } from '@gitlab/ui';
import { handleLocationHash } from '~/lib/utils/common_utils';
import getReadmeQuery from '../../queries/getReadme.query.graphql';
import readmeQery from '../../queries/readme.query.graphql';
export default {
apollo: {
readme: {
query: getReadmeQuery,
query: readmeQery,
variables() {
return {
url: this.blob.webUrl,
url: this.blob.webPath,
};
},
loadingKey: 'loading',
@ -51,7 +51,7 @@ export default {
<div class="js-file-title file-title-flex-parent">
<div class="file-header-content">
<i aria-hidden="true" class="fa fa-file-text-o fa-fw"></i>
<gl-link :href="blob.webUrl">
<gl-link :href="blob.webPath">
<strong>{{ blob.name }}</strong>
</gl-link>
</div>

View File

@ -2,7 +2,7 @@
import { GlSkeletonLoading } from '@gitlab/ui';
import { sprintf, __ } from '../../../locale';
import getRefMixin from '../../mixins/get_ref';
import getProjectPath from '../../queries/getProjectPath.query.graphql';
import projectPathQuery from '../../queries/project_path.query.graphql';
import TableHeader from './header.vue';
import TableRow from './row.vue';
import ParentRow from './parent_row.vue';
@ -17,7 +17,7 @@ export default {
mixins: [getRefMixin],
apollo: {
projectPath: {
query: getProjectPath,
query: projectPathQuery,
},
},
props: {
@ -96,7 +96,7 @@ export default {
:name="entry.name"
:path="entry.flatPath"
:type="entry.type"
:url="entry.webUrl"
:url="entry.webUrl || entry.webPath"
:submodule-tree-url="entry.treeUrl"
:lfs-oid="entry.lfsOid"
:loading-path="loadingPath"

View File

@ -12,7 +12,7 @@ import { escapeFileUrl } from '~/lib/utils/url_utility';
import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import FileIcon from '~/vue_shared/components/file_icon.vue';
import getRefMixin from '../../mixins/get_ref';
import getCommit from '../../queries/getCommit.query.graphql';
import commitQuery from '../../queries/commit.query.graphql';
export default {
components: {
@ -29,7 +29,7 @@ export default {
},
apollo: {
commit: {
query: getCommit,
query: commitQuery,
variables() {
return {
fileName: this.name,

View File

@ -3,9 +3,9 @@ import createFlash from '~/flash';
import { __ } from '../../locale';
import FileTable from './table/index.vue';
import getRefMixin from '../mixins/get_ref';
import getFiles from '../queries/getFiles.query.graphql';
import getProjectPath from '../queries/getProjectPath.query.graphql';
import getVueFileListLfsBadge from '../queries/getVueFileListLfsBadge.query.graphql';
import filesQuery from '../queries/files.query.graphql';
import projectPathQuery from '../queries/project_path.query.graphql';
import vueFileListLfsBadgeQuery from '../queries/vue_file_list_lfs_badge.query.graphql';
import FilePreview from './preview/index.vue';
import { readmeFile } from '../utils/readme';
@ -19,10 +19,10 @@ export default {
mixins: [getRefMixin],
apollo: {
projectPath: {
query: getProjectPath,
query: projectPathQuery,
},
vueFileListLfsBadge: {
query: getVueFileListLfsBadge,
query: vueFileListLfsBadgeQuery,
},
},
props: {
@ -75,7 +75,7 @@ export default {
return this.$apollo
.query({
query: getFiles,
query: filesQuery,
variables: {
projectPath: this.projectPath,
ref: this.ref,

View File

@ -1,8 +1,8 @@
import { normalizeData } from 'ee_else_ce/repository/utils/commit';
import axios from '~/lib/utils/axios_utils';
import getCommits from './queries/getCommits.query.graphql';
import getProjectPath from './queries/getProjectPath.query.graphql';
import getRef from './queries/getRef.query.graphql';
import commitsQuery from './queries/commits.query.graphql';
import projectPathQuery from './queries/project_path.query.graphql';
import refQuery from './queries/ref.query.graphql';
let fetchpromise;
let resolvers = [];
@ -22,8 +22,8 @@ export function fetchLogsTree(client, path, offset, resolver = null) {
if (fetchpromise) return fetchpromise;
const { projectPath } = client.readQuery({ query: getProjectPath });
const { escapedRef } = client.readQuery({ query: getRef });
const { projectPath } = client.readQuery({ query: projectPathQuery });
const { escapedRef } = client.readQuery({ query: refQuery });
fetchpromise = axios
.get(
@ -36,10 +36,10 @@ export function fetchLogsTree(client, path, offset, resolver = null) {
)
.then(({ data, headers }) => {
const headerLogsOffset = headers['more-logs-offset'];
const { commits } = client.readQuery({ query: getCommits });
const { commits } = client.readQuery({ query: commitsQuery });
const newCommitData = [...commits, ...normalizeData(data, path)];
client.writeQuery({
query: getCommits,
query: commitsQuery,
data: { commits: newCommitData },
});

View File

@ -1,9 +1,9 @@
import getRef from '../queries/getRef.query.graphql';
import refQuery from '../queries/ref.query.graphql';
export default {
apollo: {
ref: {
query: getRef,
query: refQuery,
manual: true,
result({ data, loading }) {
if (!loading) {

View File

@ -1,12 +1,12 @@
import getFiles from '../queries/getFiles.query.graphql';
import filesQuery from '../queries/files.query.graphql';
import getRefMixin from './get_ref';
import getProjectPath from '../queries/getProjectPath.query.graphql';
import projectPathQuery from '../queries/project_path.query.graphql';
export default {
mixins: [getRefMixin],
apollo: {
projectPath: {
query: getProjectPath,
query: projectPathQuery,
},
},
data() {
@ -21,7 +21,7 @@ export default {
return this.$apollo
.query({
query: getFiles,
query: filesQuery,
variables: {
projectPath: this.projectPath,
ref: this.ref,

View File

@ -1,6 +1,6 @@
#import "ee_else_ce/repository/queries/commit.fragment.graphql"
query getCommit($fileName: String!, $type: String!, $path: String!) {
query Commit($fileName: String!, $type: String!, $path: String!) {
commit(path: $path, fileName: $fileName, type: $type) @client {
...TreeEntryCommit
}

View File

@ -1,6 +1,6 @@
#import "ee_else_ce/repository/queries/commit.fragment.graphql"
query getCommits {
query Commits {
commits @client {
...TreeEntryCommit
}

View File

@ -8,7 +8,7 @@ fragment TreeEntry on Entry {
type
}
query getFiles(
query Files(
$projectPath: ID!
$path: String
$ref: String!
@ -23,7 +23,7 @@ query getFiles(
edges {
node {
...TreeEntry
webUrl
webPath
}
}
pageInfo {
@ -46,7 +46,7 @@ query getFiles(
edges {
node {
...TreeEntry
webUrl
webPath
lfsOid @include(if: $vueLfsEnabled)
}
}

View File

@ -1,3 +0,0 @@
query getProjectPath {
projectPath
}

View File

@ -1,4 +1,4 @@
query pathLastCommit($projectPath: ID!, $path: String, $ref: String!) {
query PathLastCommit($projectPath: ID!, $path: String, $ref: String!) {
project(fullPath: $projectPath) {
repository {
tree(path: $path, ref: $ref) {
@ -8,14 +8,14 @@ query pathLastCommit($projectPath: ID!, $path: String, $ref: String!) {
titleHtml
description
message
webUrl
webPath
authoredDate
authorName
authorGravatar
author {
name
avatarUrl
webUrl
webPath
}
signatureHtml
pipelines(ref: $ref, first: 1) {

View File

@ -1,4 +1,4 @@
query getPermissions($projectPath: ID!) {
query Permissions($projectPath: ID!) {
project(fullPath: $projectPath) {
userPermissions {
pushCode

View File

@ -0,0 +1,3 @@
query ProjectPath {
projectPath
}

View File

@ -1,3 +1,3 @@
query getProjectShortPath {
query ProjectShortPath {
projectShortPath @client
}

View File

@ -1,4 +1,4 @@
query getReadme($url: String!) {
query Readme($url: String!) {
readme(url: $url) @client {
html
}

View File

@ -1,4 +1,4 @@
query getRef {
query Ref {
ref @client
escapedRef @client
}

View File

@ -1,3 +1,3 @@
query getVueFileListLfsBadge {
query VueFileListLfsBadge {
vueFileListLfsBadge @client
}

View File

@ -1089,3 +1089,11 @@ header.header-content .dropdown-menu.frequent-items-dropdown-menu {
}
}
}
.bulk-update {
.dropdown-toggle-text {
&.is-default {
color: $gl-text-color;
}
}
}

View File

@ -26,6 +26,12 @@
margin-right: 6px;
}
.bulk-update {
.filter-item {
margin-right: 0;
}
}
.sort-filter {
display: inline-block;
float: right;

View File

@ -166,7 +166,7 @@
color: $sidebar-text;
}
svg {
.nav-icon-container svg {
fill: $sidebar-text;
}
}

View File

@ -30,6 +30,7 @@
}
&.status-box-issue-closed,
&.status-box-alert-resolved,
&.status-box-mr-merged {
background-color: $blue-500;
}

View File

@ -1,12 +1,17 @@
# frozen_string_literal: true
class InvitesController < ApplicationController
include Gitlab::Utils::StrongMemoize
before_action :member
skip_before_action :authenticate_user!, only: :decline
helper_method :member?, :current_user_matches_invite?
respond_to :html
def show
accept if skip_invitation_prompt?
end
def accept
@ -38,6 +43,20 @@ class InvitesController < ApplicationController
private
def skip_invitation_prompt?
!member? && current_user_matches_invite?
end
def current_user_matches_invite?
@member.invite_email == current_user.email
end
def member?
strong_memoize(:is_member) do
@member.source.users.include?(current_user)
end
end
def member
return @member if defined?(@member)

View File

@ -24,6 +24,7 @@
# created_before: datetime
# updated_after: datetime
# updated_before: datetime
# confidential: boolean
#
class IssuesFinder < IssuableFinder
CONFIDENTIAL_ACCESS_LEVEL = Gitlab::Access::REPORTER

View File

@ -7,6 +7,8 @@ module Mutations
ERROR_MESSAGE = 'You cannot perform write operations on a read-only instance'
field_class ::Types::BaseField
field :errors, [GraphQL::STRING_TYPE],
null: false,
description: 'Errors encountered during execution of the mutation.'

View File

@ -10,8 +10,13 @@ module Mutations
field :updated_ids,
[GraphQL::ID_TYPE],
null: false,
deprecated: { reason: 'Use todos', milestone: '13.2' },
description: 'Ids of the updated todos'
field :todos, [::Types::TodoType],
null: false,
description: 'Updated todos'
def resolve
authorize!(current_user)
@ -19,6 +24,7 @@ module Mutations
{
updated_ids: map_to_global_ids(updated_ids),
todos: Todo.id_in(updated_ids),
errors: []
}
end

View File

@ -14,7 +14,12 @@ module Mutations
field :updated_ids, [GraphQL::ID_TYPE],
null: false,
description: 'The ids of the updated todo items'
description: 'The ids of the updated todo items',
deprecated: { reason: 'Use todos', milestone: '13.2' }
field :todos, [::Types::TodoType],
null: false,
description: 'Updated todos'
def resolve(ids:)
check_update_amount_limit!(ids)
@ -24,6 +29,7 @@ module Mutations
{
updated_ids: gids_of(updated_ids),
todos: Todo.id_in(updated_ids),
errors: errors_on_objects(todos)
}
end

View File

@ -23,6 +23,8 @@ module Types
description: 'Timestamp of when the commit was authored'
field :web_url, type: GraphQL::STRING_TYPE, null: false,
description: 'Web URL of the commit'
field :web_path, type: GraphQL::STRING_TYPE, null: false,
description: 'Web path of the commit'
field :signature_html, type: GraphQL::STRING_TYPE, null: true, calls_gitaly: true,
description: 'Rendered HTML of the commit signature'
field :author_name, type: GraphQL::STRING_TYPE, null: true,

View File

@ -12,6 +12,8 @@ module Types
field :web_url, GraphQL::STRING_TYPE, null: true,
description: 'Web URL of the blob'
field :web_path, GraphQL::STRING_TYPE, null: true,
description: 'Web path of the blob'
field :lfs_oid, GraphQL::STRING_TYPE, null: true,
description: 'LFS ID of the blob',
resolve: -> (blob, args, ctx) do

View File

@ -13,6 +13,8 @@ module Types
field :web_url, GraphQL::STRING_TYPE, null: true,
description: 'Web URL for the tree entry (directory)'
field :web_path, GraphQL::STRING_TYPE, null: true,
description: 'Web path for the tree entry (directory)'
end
# rubocop: enable Graphql/AuthorizeTypes
end

View File

@ -22,6 +22,8 @@ module Types
description: "URL of the user's avatar"
field :web_url, GraphQL::STRING_TYPE, null: false,
description: 'Web URL of the user'
field :web_path, GraphQL::STRING_TYPE, null: false,
description: 'Web path of the user'
field :todos, Types::TodoType.connection_type, null: false,
resolver: Resolvers::TodoResolver,
description: 'Todos of the user'

View File

@ -131,6 +131,10 @@ module ServicesHelper
integration.configurable_events.present?
end
def project_jira_issues_integration?
false
end
extend self
end

View File

@ -97,11 +97,13 @@ module TodosHelper
'mr'
when Issue
'issue'
when AlertManagement::Alert
'alert'
end
content_tag(:span, nil, class: 'target-status') do
content_tag(:span, nil, class: "status-box status-box-#{type}-#{todo.target.state.dasherize}") do
todo.target.state.capitalize
content_tag(:span, nil, class: "status-box status-box-#{type}-#{todo.target.state.to_s.dasherize}") do
todo.target.state.to_s.capitalize
end
end
end
@ -214,7 +216,14 @@ module TodosHelper
end
def show_todo_state?(todo)
(todo.target.is_a?(MergeRequest) || todo.target.is_a?(Issue)) && %w(closed merged).include?(todo.target.state)
case todo.target
when MergeRequest, Issue
%w(closed merged).include?(todo.target.state)
when AlertManagement::Alert
%i(resolved).include?(todo.target.state)
else
false
end
end
def todo_group_options

View File

@ -135,6 +135,8 @@ module AlertManagement
scope :counts_by_status, -> { group(:status).count }
scope :counts_by_project_id, -> { group(:project_id).count }
alias_method :state, :status_name
def self.sort_by_attribute(method)
case method.to_s
when 'started_at_asc' then order_start_time(:asc)

View File

@ -52,7 +52,7 @@ class AuditEvent < ApplicationRecord
private
def default_author_value
::Gitlab::Audit::NullAuthor.for(author_id, details[:author_name])
::Gitlab::Audit::NullAuthor.for(author_id, (self[:author_name] || details[:author_name]))
end
end

View File

@ -228,7 +228,9 @@ module Clusters
def calculate_reactive_cache
return unless enabled?
{ connection_status: retrieve_connection_status, nodes: retrieve_nodes }
gitlab_kubernetes_nodes = Gitlab::Kubernetes::Node.new(self)
{ connection_status: retrieve_connection_status, nodes: gitlab_kubernetes_nodes.all.presence }
end
def persisted_applications
@ -383,54 +385,6 @@ module Clusters
result[:status]
end
def retrieve_nodes
result = ::Gitlab::Kubernetes::KubeClient.graceful_request(id) { kubeclient.get_nodes }
return unless result[:response]
cluster_nodes = result[:response]
result = ::Gitlab::Kubernetes::KubeClient.graceful_request(id) { kubeclient.metrics_client.get_nodes }
nodes_metrics = result[:response].to_a
cluster_nodes.inject([]) do |memo, node|
sliced_node = filter_relevant_node_attributes(node)
matched_node_metric = nodes_metrics.find { |node_metric| node_metric.metadata.name == node.metadata.name }
sliced_node_metrics = matched_node_metric ? filter_relevant_node_metrics_attributes(matched_node_metric) : {}
memo << sliced_node.merge(sliced_node_metrics)
end
end
def filter_relevant_node_attributes(node)
{
'metadata' => {
'name' => node.metadata.name
},
'status' => {
'capacity' => {
'cpu' => node.status.capacity.cpu,
'memory' => node.status.capacity.memory
},
'allocatable' => {
'cpu' => node.status.allocatable.cpu,
'memory' => node.status.allocatable.memory
}
}
}
end
def filter_relevant_node_metrics_attributes(node_metrics)
{
'usage' => {
'cpu' => node_metrics.usage.cpu,
'memory' => node_metrics.usage.memory
}
}
end
# To keep backward compatibility with AUTO_DEVOPS_DOMAIN
# environment variable, we need to ensure KUBE_INGRESS_BASE_DOMAIN
# is set if AUTO_DEVOPS_DOMAIN is set on any of the following options:

View File

@ -18,7 +18,7 @@ class SystemNoteMetadata < ApplicationRecord
designs_added designs_modified designs_removed designs_discussion_added
title time_tracking branch milestone discussion task moved
opened closed merged duplicate locked unlocked outdated
tag due_date pinned_embed cherry_pick health_status approved
tag due_date pinned_embed cherry_pick health_status approved unapproved
].freeze
validates :note, presence: true

View File

@ -18,6 +18,10 @@ class BlobPresenter < Gitlab::View::Presenter::Delegated
Gitlab::Routing.url_helpers.project_blob_url(blob.repository.project, File.join(blob.commit_id, blob.path))
end
def web_path
Gitlab::Routing.url_helpers.project_blob_path(blob.repository.project, File.join(blob.commit_id, blob.path))
end
private
def load_all_blob_data

View File

@ -21,6 +21,10 @@ class CommitPresenter < Gitlab::View::Presenter::Delegated
url_builder.build(commit)
end
def web_path
url_builder.build(commit, only_path: true)
end
def signature_html
return unless commit.has_signature?

View File

@ -6,4 +6,8 @@ class TreeEntryPresenter < Gitlab::View::Presenter::Delegated
def web_url
Gitlab::Routing.url_helpers.project_tree_url(tree.repository.project, File.join(tree.commit_id, tree.path))
end
def web_path
Gitlab::Routing.url_helpers.project_tree_path(tree.repository.project, File.join(tree.commit_id, tree.path))
end
end

View File

@ -6,4 +6,8 @@ class UserPresenter < Gitlab::View::Presenter::Delegated
def web_url
Gitlab::Routing.url_helpers.user_url(user)
end
def web_path
Gitlab::Routing.url_helpers.user_path(user)
end
end

View File

@ -61,6 +61,7 @@ class AuditEventService
def base_payload
{
author_id: @author.id,
author_name: @author.name,
entity_id: @entity.id,
entity_type: @entity.class.name
}

View File

@ -0,0 +1,39 @@
# frozen_string_literal: true
module MergeRequests
class RemoveApprovalService < MergeRequests::BaseService
# rubocop: disable CodeReuse/ActiveRecord
def execute(merge_request)
# paranoid protection against running wrong deletes
return unless merge_request.id && current_user.id
approval = merge_request.approvals.where(user: current_user)
trigger_approval_hooks(merge_request) do
next unless approval.destroy_all # rubocop: disable Cop/DestroyAll
reset_approvals_cache(merge_request)
create_note(merge_request)
end
end
# rubocop: enable CodeReuse/ActiveRecord
private
def reset_approvals_cache(merge_request)
merge_request.approvals.reset
end
def trigger_approval_hooks(merge_request)
yield
execute_hooks(merge_request, 'unapproved')
end
def create_note(merge_request)
SystemNoteService.unapprove_mr(merge_request, current_user)
end
end
end
MergeRequests::RemoveApprovalService.prepend_if_ee('EE::MergeRequests::RemoveApprovalService')

View File

@ -288,6 +288,10 @@ module SystemNoteService
merge_requests_service(noteable, noteable.project, user).approve_mr
end
def unapprove_mr(noteable, user)
merge_requests_service(noteable, noteable.project, user).unapprove_mr
end
private
def merge_requests_service(noteable, project, author)

View File

@ -163,7 +163,11 @@ module SystemNotes
create_note(NoteSummary.new(noteable, project, author, body, action: 'approved'))
end
def unapprove_mr
body = "unapproved this merge request"
create_note(NoteSummary.new(noteable, project, author, body, action: 'unapproved'))
end
end
end
SystemNotes::MergeRequestsService.prepend_if_ee('::EE::SystemNotes::MergeRequestsService')

View File

@ -40,4 +40,6 @@
%p
= s_("ClusterIntegration|Remove this Kubernetes cluster's configuration from this project. This will not delete your actual Kubernetes cluster.")
#js-cluster-remove-actions{ data: { cluster_path: clusterable.cluster_path(@cluster), cluster_name: @cluster.name } }
#js-cluster-remove-actions{ data: { cluster_path: clusterable.cluster_path(@cluster),
cluster_name: @cluster.name,
has_management_project: @cluster.management_project_id? } }

View File

@ -20,21 +20,19 @@
= link_to group.name, group_url(group)
as #{@member.human_access}.
- is_member = @member.source.users.include?(current_user)
- if is_member
- if member?
%p
- member_source = @member.source.is_a?(Group) ? _("group") : _("project")
= _("However, you are already a member of this %{member_source}. Sign in using a different account to accept the invitation.") % { member_source: member_source }
- if @member.invite_email != current_user.email
- if !current_user_matches_invite?
%p
- mail_to_invite_email = mail_to(@member.invite_email)
- mail_to_current_user = mail_to(current_user.email)
- link_to_current_user = link_to(current_user.to_reference, user_url(current_user))
= _("Note that this invitation was sent to %{mail_to_invite_email}, but you are signed in as %{link_to_current_user} with email %{mail_to_current_user}.").html_safe % { mail_to_invite_email: mail_to_invite_email, mail_to_current_user: mail_to_current_user, link_to_current_user: link_to_current_user }
- unless is_member
- unless member?
.actions
= link_to _("Accept invitation"), accept_invite_url(@token), method: :post, class: "btn btn-success"
= link_to _("Decline"), decline_invite_url(@token), method: :post, class: "btn btn-danger prepend-left-10"

View File

@ -80,7 +80,7 @@
= render_if_exists 'projects/sidebar/repository_locked_files'
- if project_nav_tab? :issues
= nav_link(controller: @project.issues_enabled? ? [:issues, :labels, :milestones, :boards] : :issues) do
= nav_link(controller: @project.issues_enabled? ? ['projects/issues', :labels, :milestones, :boards] : 'projects/issues') do
= link_to project_issues_path(@project), class: 'shortcuts-issues qa-issues-item' do
.nav-icon-container
= sprite_icon('issues')
@ -91,7 +91,7 @@
= number_with_delimiter(@project.open_issues_count(current_user))
%ul.sidebar-sub-level-items
= nav_link(controller: :issues, action: :index, html_options: { class: "fly-out-top-item" } ) do
= nav_link(controller: 'projects/issues', action: :index, html_options: { class: "fly-out-top-item" } ) do
= link_to project_issues_path(@project) do
%strong.fly-out-top-item-name
= _('Issues')
@ -120,19 +120,23 @@
= link_to project_milestones_path(@project), title: _('Milestones'), class: 'qa-milestones-link' do
%span
= _('Milestones')
- if project_nav_tab? :external_issue_tracker
= nav_link do
- issue_tracker = @project.external_issue_tracker
= link_to issue_tracker.issue_tracker_path, class: 'shortcuts-external_tracker' do
.nav-icon-container
= sprite_icon('external-link')
%span.nav-item-name
= issue_tracker.title
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(html_options: { class: "fly-out-top-item" } ) do
= link_to issue_tracker.issue_tracker_path do
%strong.fly-out-top-item-name
= issue_tracker.title
- if project_nav_tab?(:external_issue_tracker)
- issue_tracker = @project.external_issue_tracker
- if issue_tracker.is_a?(JiraService) && project_jira_issues_integration?
= render_if_exists 'layouts/nav/sidebar/project_jira_issues_link', issue_tracker: issue_tracker
- else
= nav_link do
= link_to issue_tracker.issue_tracker_path, target: '_blank', rel: 'noopener noreferrer', class: 'shortcuts-external_tracker' do
.nav-icon-container
= sprite_icon('external-link')
%span.nav-item-name
= issue_tracker.title
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(html_options: { class: "fly-out-top-item" } ) do
= link_to issue_tracker.issue_tracker_path, target: '_blank', rel: 'noopener noreferrer' do
%strong.fly-out-top-item-name
= issue_tracker.title
- if (project_nav_tab? :labels) && !@project.issues_enabled?
= nav_link(controller: [:labels]) do

View File

@ -1,5 +1,6 @@
- type = local_assigns.fetch(:type)
- bulk_issue_health_status_flag = Feature.enabled?(:bulk_update_health_status, @project&.group) && type == :issues && @project&.group&.feature_available?(:issuable_health_status)
- epic_bulk_edit_flag = @project&.group&.feature_available?(:epics) && type == :issues
%aside.issues-bulk-update.js-right-sidebar.right-sidebar{ "aria-live" => "polite", data: { 'signed-in': current_user.present? } }
.issuable-sidebar.hidden
@ -27,6 +28,13 @@
- field_name = "update[assignee_ids][]"
= dropdown_tag(_("Select assignee"), options: { toggle_class: "js-user-search js-update-assignee js-filter-submit js-filter-bulk-update", title: _("Assign to"), filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable",
placeholder: _("Search authors"), data: { first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: @project.id, field_name: field_name } })
- if epic_bulk_edit_flag
.block
.title
= _('Epic')
.filter-item.epic-bulk-edit
#js-epic-select-root{ data: { group_id: @project&.group&.id, show_header: "true" } }
%input{ id: 'issue_epic_id', type: 'hidden', name: 'update[epic_id]' }
.block
.title
= _('Milestone')

View File

@ -0,0 +1,5 @@
---
title: Add refresh rate options to dashboard header
merge_request: 35238
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Remove the second prompt to accept or decline an invitation
merge_request: 35777
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Add todo pill styling for resolved alert
merge_request: 35579
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Add parallel persistence for author_name on AuditEvent
merge_request: 35130
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Fix path conflict for Ghost on UpdateRoutesForLostAndFoundGroupAndOrphanedProjects
merge_request: 35425
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Todo Mutations should return the mutated todos
merge_request: 35998
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Hide cleanup button for clusters with management project
merge_request: 35576
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Update `rack-timeout` to `0.5.2`
merge_request: 36071
author:
type: changed

View File

@ -0,0 +1,22 @@
# frozen_string_literal: true
class AddAuthorNameToAuditEvent < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
unless column_exists?(:audit_events, :author_name)
with_lock_retries do
add_column :audit_events, :author_name, :text
end
end
add_text_limit :audit_events, :author_name, 255
end
def down
remove_column :audit_events, :author_name
end
end

View File

@ -136,6 +136,7 @@ class UpdateRoutesForLostAndFoundGroupAndOrphanedProjects < ActiveRecord::Migrat
# to ensure the Active Record's knowledge of the table structure is current
Namespace.reset_column_information
Route.reset_column_information
User.reset_column_information
# Find the ghost user, its namespace and the "lost and found" group
ghost_user = User.ghost
@ -158,6 +159,15 @@ class UpdateRoutesForLostAndFoundGroupAndOrphanedProjects < ActiveRecord::Migrat
'More info: gitlab.com/gitlab-org/gitlab/-/issues/198603'
lost_and_found_group.save!
# make sure that the ghost namespace has a unique path
ghost_namespace.generate_unique_path
if ghost_namespace.path_changed?
ghost_namespace.save!
# If the path changed, also update the Ghost User's username to match the new path.
ghost_user.update!(username: ghost_namespace.path)
end
# Update the routes for the Ghost user, the "lost and found" group
# and all the orphaned projects
ghost_namespace.ensure_route!

View File

@ -9366,7 +9366,9 @@ CREATE TABLE public.audit_events (
details text,
created_at timestamp without time zone,
updated_at timestamp without time zone,
ip_address inet
ip_address inet,
author_name text,
CONSTRAINT check_83ff8406e2 CHECK ((char_length(author_name) <= 255))
);
CREATE SEQUENCE public.audit_events_id_seq
@ -23561,6 +23563,7 @@ COPY "schema_migrations" (version) FROM STDIN;
20200622235737
20200623000148
20200623000320
20200623090030
20200623121135
20200623141544
20200623170000

View File

@ -251,6 +251,19 @@ When Puma is used instead of Unicorn, the following metrics are available:
| `puma_idle_threads` | Gauge | 12.0 | Number of spawned threads which are not processing a request |
| `puma_killer_terminations_total` | Gauge | 12.0 | Number of workers terminated by PumaWorkerKiller |
## Redis metrics
These client metrics are meant to complement Redis server metrics.
These metrics are broken down per [Redis
instance](https://docs.gitlab.com/omnibus/settings/redis.html#running-with-multiple-redis-instances).
These metrics all have a `storage` label which indicates the Redis
instance (`cache`, `shared_state` etc.).
| Metric | Type | Since | Description |
|:--------------------------------- |:------- |:----- |:----------- |
| `redis_client_exceptions_total` | Counter | 13.2 | Number of Redis client exceptions, broken down by exception class |
| `redis_client_requests_total` | Counter | 13.2 | Number of Redis client requests |
## Metrics shared directory
GitLab's Prometheus client requires a directory to store metrics data shared between multi-process services.

View File

@ -813,6 +813,11 @@ type Blob implements Entry {
"""
type: EntryType!
"""
Web path of the blob
"""
webPath: String
"""
Web URL of the blob
"""
@ -1216,6 +1221,11 @@ type Commit {
"""
titleHtml: String
"""
Web path of the commit
"""
webPath: String!
"""
Web URL of the commit
"""
@ -13119,9 +13129,14 @@ type TodoRestoreManyPayload {
errors: [String!]!
"""
The ids of the updated todo items
Updated todos
"""
updatedIds: [ID!]!
todos: [Todo!]!
"""
The ids of the updated todo items. Deprecated in 13.2: Use todos
"""
updatedIds: [ID!]! @deprecated(reason: "Use todos. Deprecated in 13.2")
}
"""
@ -13201,9 +13216,14 @@ type TodosMarkAllDonePayload {
errors: [String!]!
"""
Ids of the updated todos
Updated todos
"""
updatedIds: [ID!]!
todos: [Todo!]!
"""
Ids of the updated todos. Deprecated in 13.2: Use todos
"""
updatedIds: [ID!]! @deprecated(reason: "Use todos. Deprecated in 13.2")
}
"""
@ -13367,6 +13387,11 @@ type TreeEntry implements Entry {
"""
type: EntryType!
"""
Web path for the tree entry (directory)
"""
webPath: String
"""
Web URL for the tree entry (directory)
"""
@ -14268,6 +14293,11 @@ type User {
"""
username: String!
"""
Web path of the user
"""
webPath: String!
"""
Web URL of the user
"""

View File

@ -2128,6 +2128,20 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "webPath",
"description": "Web path of the blob",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "webUrl",
"description": "Web URL of the blob",
@ -3290,6 +3304,24 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "webPath",
"description": "Web path of the commit",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "webUrl",
"description": "Web URL of the commit",
@ -38752,9 +38784,35 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "todos",
"description": "Updated todos",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "Todo",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "updatedIds",
"description": "The ids of the updated todo items",
"description": "The ids of the updated todo items. Deprecated in 13.2: Use todos",
"args": [
],
@ -38775,8 +38833,8 @@
}
}
},
"isDeprecated": false,
"deprecationReason": null
"isDeprecated": true,
"deprecationReason": "Use todos. Deprecated in 13.2"
}
],
"inputFields": null,
@ -38987,9 +39045,35 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "todos",
"description": "Updated todos",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "Todo",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "updatedIds",
"description": "Ids of the updated todos",
"description": "Ids of the updated todos. Deprecated in 13.2: Use todos",
"args": [
],
@ -39010,8 +39094,8 @@
}
}
},
"isDeprecated": false,
"deprecationReason": null
"isDeprecated": true,
"deprecationReason": "Use todos. Deprecated in 13.2"
}
],
"inputFields": null,
@ -39466,6 +39550,20 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "webPath",
"description": "Web path for the tree entry (directory)",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "webUrl",
"description": "Web URL for the tree entry (directory)",
@ -41883,6 +41981,24 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "webPath",
"description": "Web path of the user",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "webUrl",
"description": "Web URL of the user",

View File

@ -164,6 +164,7 @@ Autogenerated return type of AwardEmojiToggle
| `path` | String! | Path of the entry |
| `sha` | String! | Last commit sha for the entry |
| `type` | EntryType! | Type of tree entry |
| `webPath` | String | Web path of the blob |
| `webUrl` | String | Web URL of the blob |
## Board
@ -227,6 +228,7 @@ Autogenerated return type of BoardListUpdateLimitMetrics
| `signatureHtml` | String | Rendered HTML of the commit signature |
| `title` | String | Title of the commit message |
| `titleHtml` | String | The GitLab Flavored Markdown rendering of `title` |
| `webPath` | String! | Web path of the commit |
| `webUrl` | String! | Web URL of the commit |
## CommitCreatePayload
@ -1966,7 +1968,8 @@ Autogenerated return type of TodoRestoreMany
| --- | ---- | ---------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `updatedIds` | ID! => Array | The ids of the updated todo items |
| `todos` | Todo! => Array | Updated todos |
| `updatedIds` **{warning-solid}** | ID! => Array | **Deprecated:** Use todos. Deprecated in 13.2 |
## TodoRestorePayload
@ -1986,7 +1989,8 @@ Autogenerated return type of TodosMarkAllDone
| --- | ---- | ---------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `updatedIds` | ID! => Array | Ids of the updated todos |
| `todos` | Todo! => Array | Updated todos |
| `updatedIds` **{warning-solid}** | ID! => Array | **Deprecated:** Use todos. Deprecated in 13.2 |
## ToggleAwardEmojiPayload
@ -2017,6 +2021,7 @@ Represents a directory
| `path` | String! | Path of the entry |
| `sha` | String! | Last commit sha for the entry |
| `type` | EntryType! | Type of tree entry |
| `webPath` | String | Web path for the tree entry (directory) |
| `webUrl` | String | Web URL for the tree entry (directory) |
## UpdateAlertStatusPayload
@ -2120,6 +2125,7 @@ Autogenerated return type of UpdateSnippet
| `state` | UserState! | State of the user |
| `userPermissions` | UserPermissions! | Permissions for the current user on the resource |
| `username` | String! | Username of the user. Unique within this instance of GitLab |
| `webPath` | String! | Web path of the user |
| `webUrl` | String! | Web URL of the user |
## UserPermissions

View File

@ -640,7 +640,6 @@ There are two options for safely adding new arguments to Sidekiq workers:
1. Set up a [multi-step deployment](#multi-step-deployment) in which the new argument is first added to the worker
1. Use a [parameter hash](#parameter-hash) for additional arguments. This is perhaps the most flexible option.
1. Use a parameter hash for additional arguments. This is perhaps the most flexible option.
##### Multi-step deployment

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -134,12 +134,24 @@ The changes of a wiki page over time are recorded in the wiki's Git repository,
and you can view them by clicking the **Page history** button.
From the history page you can see the revision of the page (Git commit SHA), its
author, the commit message, when it was last updated, and the page markup format.
author, the commit message, and when it was last updated.
To see how a previous version of the page looked like, click on a revision
number.
number in the **Page version** column.
![Wiki page history](img/wiki_page_history.png)
### Viewing the changes between page versions
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/15242) in GitLab 13.2.
Similar to versioned diff file views, you can see the changes made in a given Wiki page version:
1. Navigate to the Wiki page you're interested in.
1. Click on **Page history** to see all page versions.
1. Click on the commit message in the **Changes** column for the version you're interested in:
![Wiki page changes](img/wiki_page_diffs_v13_2.png)
## Wiki activity records
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/14902) in GitLab 12.10.

View File

@ -81,6 +81,19 @@ module Gitlab
self
end
def count_request
@request_counter ||= Gitlab::Metrics.counter(:redis_client_requests_total, 'Client side Redis request count, per Redis server')
@request_counter.increment({ storage: storage_key })
end
def count_exception(ex)
# This metric is meant to give a client side view of how the Redis
# server is doing. Redis itself does not expose error counts. This
# metric can be used for Redis alerting and service health monitoring.
@exception_counter ||= Gitlab::Metrics.counter(:redis_client_exceptions_total, 'Client side Redis exception count, per Redis server, per exception class')
@exception_counter.increment({ storage: storage_key, exception: ex.class.to_s })
end
private
def request_count_key

View File

@ -6,11 +6,14 @@ module Gitlab
module Instrumentation
module RedisInterceptor
def call(*args, &block)
instrumentation_class.count_request
instrumentation_class.redis_cluster_validate!(args.first)
start = Time.now
instrumentation_class.redis_cluster_validate!(args.first)
super(*args, &block)
rescue ::Redis::BaseError => ex
instrumentation_class.count_exception(ex)
raise ex
ensure
duration = (Time.now - start)

View File

@ -0,0 +1,78 @@
# frozen_string_literal: true
module Gitlab
module Kubernetes
class Node
def initialize(cluster)
@cluster = cluster
end
def all
nodes.map do |node|
attributes = node(node)
attributes.merge(node_metrics(node))
end
end
private
attr_reader :cluster
def nodes_from_cluster
graceful_request { cluster.kubeclient.get_nodes }
end
def nodes_metrics_from_cluster
graceful_request { cluster.kubeclient.metrics_client.get_nodes }
end
def nodes
@nodes ||= nodes_from_cluster[:response].to_a
end
def nodes_metrics
@nodes_metrics ||= nodes_metrics_from_cluster[:response].to_a
end
def node_metrics_from_node(node)
nodes_metrics.find do |node_metric|
node_metric.metadata.name == node.metadata.name
end
end
def graceful_request(&block)
::Gitlab::Kubernetes::KubeClient.graceful_request(cluster.id, &block)
end
def node(node)
{
'metadata' => {
'name' => node.metadata.name
},
'status' => {
'capacity' => {
'cpu' => node.status.capacity.cpu,
'memory' => node.status.capacity.memory
},
'allocatable' => {
'cpu' => node.status.allocatable.cpu,
'memory' => node.status.allocatable.memory
}
}
}
end
def node_metrics(node)
node_metrics = node_metrics_from_node(node)
return {} unless node_metrics
{
'usage' => {
'cpu' => node_metrics.usage.cpu,
'memory' => node_metrics.usage.memory
}
}
end
end
end
end

View File

@ -167,6 +167,11 @@ msgid_plural "%d groups selected"
msgstr[0] ""
msgstr[1] ""
msgid "%d hour"
msgid_plural "%d hours"
msgstr[0] ""
msgstr[1] ""
msgid "%d inaccessible merge request"
msgid_plural "%d inaccessible merge requests"
msgstr[0] ""
@ -1244,7 +1249,7 @@ msgstr ""
msgid "AccessTokens|reset it"
msgstr ""
msgid "AccessibilityReport|Learn More"
msgid "AccessibilityReport|Learn more"
msgstr ""
msgid "AccessibilityReport|Message: %{message}"
@ -8986,6 +8991,9 @@ msgstr ""
msgid "Epic events"
msgstr ""
msgid "Epic not found for given params"
msgstr ""
msgid "Epics"
msgstr ""
@ -12874,9 +12882,15 @@ msgstr ""
msgid "JiraService|If different from Web URL"
msgstr ""
msgid "JiraService|Issue List"
msgstr ""
msgid "JiraService|Jira API URL"
msgstr ""
msgid "JiraService|Jira Issues"
msgstr ""
msgid "JiraService|Jira comments will be created when an issue gets referenced in a commit."
msgstr ""
@ -12886,6 +12900,9 @@ msgstr ""
msgid "JiraService|Jira issue tracker"
msgstr ""
msgid "JiraService|Open Jira"
msgstr ""
msgid "JiraService|Password or API token"
msgstr ""
@ -13281,9 +13298,6 @@ msgstr ""
msgid "Learn GitLab"
msgstr ""
msgid "Learn More"
msgstr ""
msgid "Learn how to %{link_start}contribute to the built-in templates%{link_end}"
msgstr ""
@ -14505,6 +14519,9 @@ msgstr ""
msgid "Metrics|Select a value"
msgstr ""
msgid "Metrics|Set refresh rate"
msgstr ""
msgid "Metrics|Star dashboard"
msgstr ""
@ -15715,6 +15732,9 @@ msgstr ""
msgid "OfSearchInADropdown|Filter"
msgstr ""
msgid "Off"
msgstr ""
msgid "Oh no!"
msgstr ""
@ -20294,7 +20314,7 @@ msgstr ""
msgid "SecurityReports|Issue Created"
msgstr ""
msgid "SecurityReports|Learn More"
msgid "SecurityReports|Learn more"
msgstr ""
msgid "SecurityReports|Learn more about setting up your dashboard"
@ -20519,6 +20539,9 @@ msgstr ""
msgid "Select due date"
msgstr ""
msgid "Select epic"
msgstr ""
msgid "Select file"
msgstr ""

View File

@ -4,21 +4,44 @@ require 'spec_helper'
RSpec.describe InvitesController do
let(:token) { '123456' }
let(:user) { create(:user) }
let(:member) { create(:project_member, invite_token: token, invite_email: 'test@abc.com', user: user) }
let_it_be(:user) { create(:user) }
let(:member) { create(:project_member, :invited, invite_token: token, invite_email: user.email) }
let(:project_members) { member.source.users }
before do
controller.instance_variable_set(:@member, member)
sign_in(user)
end
describe 'GET #accept' do
it 'accepts user' do
get :accept, params: { id: token }
member.reload
describe 'GET #show' do
it 'accepts user if invite email matches signed in user' do
expect do
get :show, params: { id: token }
end.to change { project_members.include?(user) }.from(false).to(true)
expect(response).to have_gitlab_http_status(:found)
expect(flash[:notice]).to include 'You have been granted'
end
it 'forces re-confirmation if email does not match signed in user' do
member.invite_email = 'bogus@email.com'
expect do
get :show, params: { id: token }
end.not_to change { project_members.include?(user) }
expect(response).to have_gitlab_http_status(:ok)
expect(flash[:notice]).to be_nil
end
end
describe 'POST #accept' do
it 'accepts user' do
expect do
post :accept, params: { id: token }
end.to change { project_members.include?(user) }.from(false).to(true)
expect(response).to have_gitlab_http_status(:found)
expect(member.user).to eq(user)
expect(flash[:notice]).to include 'You have been granted'
end
end
@ -26,8 +49,8 @@ RSpec.describe InvitesController do
describe 'GET #decline' do
it 'declines user' do
get :decline, params: { id: token }
expect {member.reload}.to raise_error ActiveRecord::RecordNotFound
expect { member.reload }.to raise_error ActiveRecord::RecordNotFound
expect(response).to have_gitlab_http_status(:found)
expect(flash[:notice]).to include 'You have declined the invitation to join'
end

View File

@ -68,6 +68,35 @@ RSpec.describe 'Group' do
end
end
describe 'real-time group url validation', :js do
it 'shows a message if group url is available' do
fill_in 'group_path', with: 'az'
wait_for_requests
expect(page).to have_content('Group path is available')
end
it 'shows an error if group url is taken' do
fill_in 'group_path', with: user.username
wait_for_requests
expect(page).to have_content('Group path is already taken')
end
it 'does not break after an invalid form submit' do
fill_in 'group_name', with: 'MyGroup'
fill_in 'group_path', with: 'z'
click_button 'Create group'
expect(page).to have_content('Group URL is too short')
fill_in 'group_path', with: 'az'
wait_for_requests
expect(page).to have_content('Group path is available')
end
end
describe 'Mattermost team creation' do
before do
stub_mattermost_setting(enabled: mattermost_enabled)

View File

@ -2,8 +2,8 @@
require 'spec_helper'
RSpec.describe 'Invites' do
let(:user) { create(:user) }
RSpec.describe 'Invites', :aggregate_failures do
let(:user) { create(:user, email: 'user@example.com') }
let(:owner) { create(:user, name: 'John Doe') }
let(:group) { create(:group, name: 'Owned') }
let(:project) { create(:project, :repository, namespace: group) }
@ -11,7 +11,7 @@ RSpec.describe 'Invites' do
before do
project.add_maintainer(owner)
group.add_user(owner, Gitlab::Access::OWNER)
group.add_owner(owner)
group.add_developer('user@example.com', owner)
group_invite.generate_invite_token!
end
@ -23,12 +23,12 @@ RSpec.describe 'Invites' do
end
def fill_in_sign_up_form(new_user)
fill_in 'new_user_name', with: new_user.name
fill_in 'new_user_username', with: new_user.username
fill_in 'new_user_email', with: new_user.email
fill_in 'new_user_email_confirmation', with: new_user.email
fill_in 'new_user_password', with: new_user.password
click_button "Register"
fill_in 'new_user_name', with: new_user.name
fill_in 'new_user_username', with: new_user.username
fill_in 'new_user_email', with: new_user.email
fill_in 'new_user_email_confirmation', with: new_user.email
fill_in 'new_user_password', with: new_user.password
click_button 'Register'
end
def fill_in_sign_in_form(user)
@ -48,19 +48,15 @@ RSpec.describe 'Invites' do
expect(page).to have_content('To accept this invitation, sign in')
end
it 'sign in and redirects to invitation page' do
it 'sign in, grants access and redirects to group page' do
fill_in_sign_in_form(user)
expect(current_path).to eq(invite_path(group_invite.raw_invite_token))
expect(page).to have_content(
'You have been invited by John Doe to join group Owned as Developer.'
)
expect(page).to have_link('Accept invitation')
expect(page).to have_link('Decline')
expect(current_path).to eq(group_path(group))
expect(page).to have_content('You have been granted Developer access to group Owned.')
end
end
context 'when signed in as an exists member' do
context 'when signed in as an existing member' do
before do
sign_in(owner)
end
@ -71,166 +67,166 @@ RSpec.describe 'Invites' do
end
end
describe 'accepting the invitation' do
before do
sign_in(user)
visit invite_path(group_invite.raw_invite_token)
end
it 'grants access and redirects to group page' do
page.click_link 'Accept invitation'
expect(current_path).to eq(group_path(group))
expect(page).to have_content(
'You have been granted Developer access to group Owned.'
)
end
end
describe 'declining the application' do
context 'when signed in' do
before do
sign_in(user)
visit invite_path(group_invite.raw_invite_token)
end
it 'declines application and redirects to dashboard' do
page.click_link 'Decline'
expect(current_path).to eq(dashboard_projects_path)
expect(page).to have_content(
'You have declined the invitation to join group Owned.'
)
end
end
context 'when signed out' do
before do
visit decline_invite_path(group_invite.raw_invite_token)
end
it 'declines application and redirects to sign in page' do
expect(current_path).to eq(new_user_session_path)
expect(page).to have_content(
'You have declined the invitation to join group Owned.'
)
end
end
end
describe 'invite an user using their email address' do
context 'when inviting a user using their email address' do
let(:new_user) { build_stubbed(:user) }
let(:invite_email) { new_user.email }
let(:group_invite) { create(:group_member, :invited, group: group, invite_email: invite_email) }
let!(:project_invite) { create(:project_member, :invited, project: project, invite_email: invite_email) }
before do
stub_application_setting(send_user_confirmation_email: send_email_confirmation)
visit invite_path(group_invite.raw_invite_token)
end
context 'email confirmation disabled' do
let(:send_email_confirmation) { false }
it 'signs up and redirects to the dashboard page with all the projects/groups invitations automatically accepted' do
fill_in_sign_up_form(new_user)
expect(current_path).to eq(dashboard_projects_path)
expect(page).to have_content(project.full_name)
visit group_path(group)
expect(page).to have_content(group.full_name)
context 'when user has not signed in yet' do
before do
stub_application_setting(send_user_confirmation_email: send_email_confirmation)
visit invite_path(group_invite.raw_invite_token)
end
context 'the user sign-up using a different email address' do
let(:invite_email) { build_stubbed(:user).email }
context 'email confirmation disabled' do
let(:send_email_confirmation) { false }
it 'signs up and redirects to the invitation page' do
it 'signs up and redirects to the dashboard page with all the projects/groups invitations automatically accepted' do
fill_in_sign_up_form(new_user)
expect(current_path).to eq(invite_path(group_invite.raw_invite_token))
end
end
end
context 'email confirmation enabled' do
let(:send_email_confirmation) { true }
context 'when soft email confirmation is not enabled' do
before do
allow(User).to receive(:allow_unconfirmed_access_for).and_return 0
end
it 'signs up and redirects to root page with all the project/groups invitation automatically accepted' do
fill_in_sign_up_form(new_user)
confirm_email(new_user)
fill_in_sign_in_form(new_user)
expect(current_path).to eq(root_path)
expect(current_path).to eq(dashboard_projects_path)
expect(page).to have_content(project.full_name)
visit group_path(group)
expect(page).to have_content(group.full_name)
end
end
context 'when soft email confirmation is enabled' do
before do
allow(User).to receive(:allow_unconfirmed_access_for).and_return 2.days
end
context 'the user sign-up using a different email address' do
let(:invite_email) { build_stubbed(:user).email }
it 'signs up and redirects to root page with all the project/groups invitation automatically accepted' do
fill_in_sign_up_form(new_user)
confirm_email(new_user)
it 'signs up and redirects to the invitation page' do
fill_in_sign_up_form(new_user)
expect(current_path).to eq(root_path)
expect(page).to have_content(project.full_name)
visit group_path(group)
expect(page).to have_content(group.full_name)
expect(current_path).to eq(invite_path(group_invite.raw_invite_token))
end
end
end
it "doesn't accept invitations until the user confirms their email" do
fill_in_sign_up_form(new_user)
sign_in(owner)
visit project_project_members_path(project)
expect(page).to have_content 'Invited'
end
context 'the user sign-up using a different email address' do
let(:invite_email) { build_stubbed(:user).email }
context 'email confirmation enabled' do
let(:send_email_confirmation) { true }
context 'when soft email confirmation is not enabled' do
before do
stub_feature_flags(soft_email_confirmation: false)
allow(User).to receive(:allow_unconfirmed_access_for).and_return 0
end
it 'signs up and redirects to the invitation page' do
it 'signs up and redirects to root page with all the project/groups invitation automatically accepted' do
fill_in_sign_up_form(new_user)
confirm_email(new_user)
fill_in_sign_in_form(new_user)
expect(current_path).to eq(invite_path(group_invite.raw_invite_token))
expect(current_path).to eq(root_path)
expect(page).to have_content(project.full_name)
visit group_path(group)
expect(page).to have_content(group.full_name)
end
end
context 'when soft email confirmation is enabled' do
before do
stub_feature_flags(soft_email_confirmation: true)
allow(User).to receive(:allow_unconfirmed_access_for).and_return 2.days
end
it 'signs up and redirects to the invitation page' do
it 'signs up and redirects to root page with all the project/groups invitation automatically accepted' do
fill_in_sign_up_form(new_user)
confirm_email(new_user)
expect(current_path).to eq(invite_path(group_invite.raw_invite_token))
expect(current_path).to eq(root_path)
expect(page).to have_content(project.full_name)
visit group_path(group)
expect(page).to have_content(group.full_name)
end
end
it "doesn't accept invitations until the user confirms their email" do
fill_in_sign_up_form(new_user)
sign_in(owner)
visit project_project_members_path(project)
expect(page).to have_content 'Invited'
end
context 'the user sign-up using a different email address' do
let(:invite_email) { build_stubbed(:user).email }
context 'when soft email confirmation is not enabled' do
before do
stub_feature_flags(soft_email_confirmation: false)
allow(User).to receive(:allow_unconfirmed_access_for).and_return 0
end
it 'signs up and redirects to the invitation page' do
fill_in_sign_up_form(new_user)
confirm_email(new_user)
fill_in_sign_in_form(new_user)
expect(current_path).to eq(invite_path(group_invite.raw_invite_token))
end
end
context 'when soft email confirmation is enabled' do
before do
stub_feature_flags(soft_email_confirmation: true)
allow(User).to receive(:allow_unconfirmed_access_for).and_return 2.days
end
it 'signs up and redirects to the invitation page' do
fill_in_sign_up_form(new_user)
expect(current_path).to eq(invite_path(group_invite.raw_invite_token))
end
end
end
end
end
context 'when declining the invitation' do
let(:send_email_confirmation) { true }
context 'when signed in' do
before do
sign_in(user)
visit invite_path(group_invite.raw_invite_token)
end
it 'declines application and redirects to dashboard' do
page.click_link 'Decline'
expect(current_path).to eq(dashboard_projects_path)
expect(page).to have_content('You have declined the invitation to join group Owned.')
end
end
context 'when signed out' do
before do
visit decline_invite_path(group_invite.raw_invite_token)
end
it 'declines application and redirects to sign in page' do
expect(current_path).to eq(new_user_session_path)
expect(page).to have_content('You have declined the invitation to join group Owned.')
end
end
end
context 'when accepting the invitation' do
let(:send_email_confirmation) { true }
before do
sign_in(user)
visit invite_path(group_invite.raw_invite_token)
end
it 'grants access and redirects to group page' do
page.click_link 'Accept invitation'
expect(current_path).to eq(group_path(group))
expect(page).to have_content('You have been granted Owner access to group Owned.')
end
end
end
end

View File

@ -4,18 +4,7 @@ require 'spec_helper'
RSpec.describe 'User activates Jira', :js do
include_context 'project service activation'
let(:url) { 'http://jira.example.com' }
let(:test_url) { 'http://jira.example.com/rest/api/2/serverInfo' }
def fill_form(disable: false)
click_active_toggle if disable
fill_in 'service_url', with: url
fill_in 'service_username', with: 'username'
fill_in 'service_password', with: 'password'
fill_in 'service_jira_issue_transition_id', with: '25'
end
include_context 'project service Jira context'
describe 'user sets and activates Jira Service' do
context 'when Jira connection test succeeds' do
@ -33,9 +22,14 @@ RSpec.describe 'User activates Jira', :js do
expect(current_path).to eq(edit_project_service_path(project, :jira))
end
it 'shows the Jira link in the menu' do
page.within('.nav-sidebar') do
expect(page).to have_link('Jira', href: url)
unless Gitlab.ee?
it 'adds Jira link to sidebar menu' do
page.within('.nav-sidebar') do
expect(page).not_to have_link('Jira Issues')
expect(page).not_to have_link('Issue List', visible: false)
expect(page).not_to have_link('Open Jira', href: url, visible: false)
expect(page).to have_link('Jira', href: url)
end
end
end
end

View File

@ -28,7 +28,7 @@ describe('Remove cluster confirmation modal', () => {
describe('split button dropdown', () => {
const findModal = () => wrapper.find(GlModal).vm;
const findSplitButton = () => wrapper.find(SplitButton).vm;
const findSplitButton = () => wrapper.find(SplitButton);
beforeEach(() => {
createComponent({ clusterName: 'my-test-cluster' });
@ -36,7 +36,7 @@ describe('Remove cluster confirmation modal', () => {
});
it('opens modal with "cleanup" option', () => {
findSplitButton().$emit('remove-cluster-and-cleanup');
findSplitButton().vm.$emit('remove-cluster-and-cleanup');
return wrapper.vm.$nextTick().then(() => {
expect(findModal().show).toHaveBeenCalled();
@ -45,12 +45,23 @@ describe('Remove cluster confirmation modal', () => {
});
it('opens modal without "cleanup" option', () => {
findSplitButton().$emit('remove-cluster');
findSplitButton().vm.$emit('remove-cluster');
return wrapper.vm.$nextTick().then(() => {
expect(findModal().show).toHaveBeenCalled();
expect(wrapper.vm.confirmCleanup).toEqual(false);
});
});
describe('with cluster management project', () => {
beforeEach(() => {
createComponent({ hasManagementProject: true });
});
it('renders regular button instead', () => {
expect(findSplitButton().exists()).toBe(false);
expect(wrapper.find('[data-testid="btnRemove"]').exists()).toBe(true);
});
});
});
});

View File

@ -84,17 +84,7 @@ exports[`Dashboard template matches the default snapshot 1`] = `
<div
class="mb-2 pr-2 d-flex d-sm-block"
>
<gl-deprecated-button-stub
class="flex-grow-1"
size="md"
title="Refresh dashboard"
variant="default"
>
<icon-stub
name="retry"
size="16"
/>
</gl-deprecated-button-stub>
<refresh-button-stub />
</div>
<div

View File

@ -10,6 +10,7 @@ import { metricStates } from '~/monitoring/constants';
import Dashboard from '~/monitoring/components/dashboard.vue';
import DashboardHeader from '~/monitoring/components/dashboard_header.vue';
import RefreshButton from '~/monitoring/components/refresh_button.vue';
import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue';
import CustomMetricsFormFields from '~/custom_metrics/components/custom_metrics_form_fields.vue';
import DashboardsDropdown from '~/monitoring/components/dashboards_dropdown.vue';
@ -592,10 +593,9 @@ describe('Dashboard', () => {
setupStoreWithData(store);
return wrapper.vm.$nextTick().then(() => {
const refreshBtn = wrapper.find(DashboardHeader).findAll({ ref: 'refreshDashboardBtn' });
const refreshBtn = wrapper.find(DashboardHeader).find(RefreshButton);
expect(refreshBtn).toHaveLength(1);
expect(refreshBtn.is(GlDeprecatedButton)).toBe(true);
expect(refreshBtn.exists()).toBe(true);
});
});

View File

@ -0,0 +1,143 @@
import { shallowMount } from '@vue/test-utils';
import { createStore } from '~/monitoring/stores';
import { GlNewDropdown, GlNewDropdownItem, GlButton } from '@gitlab/ui';
import RefreshButton from '~/monitoring/components/refresh_button.vue';
describe('RefreshButton', () => {
let wrapper;
let store;
let dispatch;
let documentHidden;
const createWrapper = () => {
wrapper = shallowMount(RefreshButton, { store });
};
const findRefreshBtn = () => wrapper.find(GlButton);
const findDropdown = () => wrapper.find(GlNewDropdown);
const findOptions = () => findDropdown().findAll(GlNewDropdownItem);
const findOptionAt = index => findOptions().at(index);
const expectFetchDataToHaveBeenCalledTimes = times => {
const refreshCalls = dispatch.mock.calls.filter(([action, payload]) => {
return action === 'monitoringDashboard/fetchDashboardData' && payload === undefined;
});
expect(refreshCalls).toHaveLength(times);
};
beforeEach(() => {
store = createStore();
jest.spyOn(store, 'dispatch').mockResolvedValue();
dispatch = store.dispatch;
// Document can be mock hidden by overriding the `hidden` property
documentHidden = false;
Object.defineProperty(document, 'hidden', {
configurable: true,
get() {
return documentHidden;
},
});
createWrapper();
});
afterEach(() => {
dispatch.mockReset();
wrapper.destroy();
});
it('refreshes data when "refresh" is clicked', () => {
findRefreshBtn().vm.$emit('click');
expectFetchDataToHaveBeenCalledTimes(1);
});
it('refresh rate is "Off" in the dropdown', () => {
expect(findDropdown().props('text')).toBe('Off');
});
describe('refresh rate options', () => {
it('presents multiple options', () => {
expect(findOptions().length).toBeGreaterThan(1);
});
it('presents an "Off" option as the default option', () => {
expect(findOptionAt(0).text()).toBe('Off');
expect(findOptionAt(0).props('isChecked')).toBe(true);
});
});
describe('when a refresh rate is chosen', () => {
const optIndex = 2; // Other option than "Off"
beforeEach(() => {
findOptionAt(optIndex).vm.$emit('click');
return wrapper.vm.$nextTick;
});
it('refresh rate appears in the dropdown', () => {
expect(findDropdown().props('text')).toBe('10s');
});
it('refresh rate option is checked', () => {
expect(findOptionAt(0).props('isChecked')).toBe(false);
expect(findOptionAt(optIndex).props('isChecked')).toBe(true);
});
it('refreshes data when a new refresh rate is chosen', () => {
expectFetchDataToHaveBeenCalledTimes(1);
});
it('refreshes data after two intervals of time have passed', async () => {
jest.runOnlyPendingTimers();
expectFetchDataToHaveBeenCalledTimes(2);
await wrapper.vm.$nextTick();
jest.runOnlyPendingTimers();
expectFetchDataToHaveBeenCalledTimes(3);
});
it('does not refresh data if the document is hidden', async () => {
documentHidden = true;
jest.runOnlyPendingTimers();
expectFetchDataToHaveBeenCalledTimes(1);
await wrapper.vm.$nextTick();
jest.runOnlyPendingTimers();
expectFetchDataToHaveBeenCalledTimes(1);
});
it('data is not refreshed anymore after component is destroyed', () => {
expect(jest.getTimerCount()).toBe(1);
wrapper.destroy();
expect(jest.getTimerCount()).toBe(0);
});
describe('when "Off" refresh rate is chosen', () => {
beforeEach(() => {
findOptionAt(0).vm.$emit('click');
return wrapper.vm.$nextTick;
});
it('refresh rate is "Off" in the dropdown', () => {
expect(findDropdown().props('text')).toBe('Off');
});
it('refresh rate option is appears selected', () => {
expect(findOptionAt(0).props('isChecked')).toBe(true);
expect(findOptionAt(optIndex).props('isChecked')).toBe(false);
});
it('stops refreshing data', () => {
jest.runOnlyPendingTimers();
expectFetchDataToHaveBeenCalledTimes(1);
});
});
});
});

View File

@ -10,7 +10,7 @@ exports[`Repository last commit component renders commit widget 1`] = `
imgcssclasses=""
imgsize="40"
imgsrc="https://test.com"
linkhref="https://test.com/test"
linkhref="/test"
tooltipplacement="top"
tooltiptext=""
username=""
@ -24,7 +24,7 @@ exports[`Repository last commit component renders commit widget 1`] = `
>
<gl-link-stub
class="commit-row-message item-title"
href="https://test.com/commit/123"
href="/commit/123"
>
Commit title
</gl-link-stub>
@ -36,7 +36,7 @@ exports[`Repository last commit component renders commit widget 1`] = `
>
<gl-link-stub
class="commit-author-link js-user-link"
href="https://test.com/test"
href="/test"
>
Test
@ -110,7 +110,7 @@ exports[`Repository last commit component renders the signature HTML as returned
imgcssclasses=""
imgsize="40"
imgsrc="https://test.com"
linkhref="https://test.com/test"
linkhref="/test"
tooltipplacement="top"
tooltiptext=""
username=""
@ -124,7 +124,7 @@ exports[`Repository last commit component renders the signature HTML as returned
>
<gl-link-stub
class="commit-row-message item-title"
href="https://test.com/commit/123"
href="/commit/123"
>
Commit title
</gl-link-stub>
@ -136,7 +136,7 @@ exports[`Repository last commit component renders the signature HTML as returned
>
<gl-link-stub
class="commit-author-link js-user-link"
href="https://test.com/test"
href="/test"
>
Test

View File

@ -12,11 +12,13 @@ function createCommitData(data = {}) {
titleHtml: 'Commit title',
message: 'Commit message',
webUrl: 'https://test.com/commit/123',
webPath: '/commit/123',
authoredDate: '2019-01-01',
author: {
name: 'Test',
avatarUrl: 'https://test.com',
webUrl: 'https://test.com/test',
webPath: '/test',
},
pipeline: {
detailedStatus: {

View File

@ -16,7 +16,7 @@ exports[`Repository file preview component renders file HTML 1`] = `
/>
<gl-link-stub
href="http://test.com"
href="/test.md"
>
<strong>
README.md

View File

@ -31,6 +31,7 @@ describe('Repository file preview component', () => {
it('renders file HTML', () => {
factory({
webUrl: 'http://test.com',
webPath: '/test.md',
name: 'README.md',
});
@ -44,6 +45,7 @@ describe('Repository file preview component', () => {
it('handles hash after render', () => {
factory({
webUrl: 'http://test.com',
webPath: '/test.md',
name: 'README.md',
});
@ -60,6 +62,7 @@ describe('Repository file preview component', () => {
it('renders loading icon', () => {
factory({
webUrl: 'http://test.com',
webPath: '/test.md',
name: 'README.md',
});

View File

@ -21,7 +21,7 @@ RSpec.describe Mutations::Todos::MarkAllDone do
describe '#resolve' do
it 'marks all pending todos as done' do
updated_todo_ids = mutation_for(current_user).resolve.dig(:updated_ids)
updated_todo_ids, todos = mutation_for(current_user).resolve.values_at(:updated_ids, :todos)
expect(todo1.reload.state).to eq('done')
expect(todo2.reload.state).to eq('done')
@ -29,6 +29,7 @@ RSpec.describe Mutations::Todos::MarkAllDone do
expect(other_user_todo.reload.state).to eq('pending')
expect(updated_todo_ids).to contain_exactly(global_id_of(todo1), global_id_of(todo3))
expect(todos).to contain_exactly(todo1, todo3)
end
it 'behaves as expected if there are no todos for the requesting user' do

View File

@ -25,6 +25,8 @@ RSpec.describe Mutations::Todos::RestoreMany do
todo_ids = result[:updated_ids]
expect(todo_ids.size).to eq(1)
expect(todo_ids.first).to eq(todo1.to_global_id.to_s)
expect(result[:todos]).to contain_exactly(todo1)
end
it 'handles a todo which is already pending as expected' do
@ -33,6 +35,7 @@ RSpec.describe Mutations::Todos::RestoreMany do
expect_states_were_not_changed
expect(result[:updated_ids]).to eq([])
expect(result[:todos]).to be_empty
end
it 'ignores requests for todos which do not belong to the current user' do
@ -56,6 +59,7 @@ RSpec.describe Mutations::Todos::RestoreMany do
returned_todo_ids = result[:updated_ids]
expect(returned_todo_ids).to contain_exactly(todo1.to_global_id.to_s, todo4.to_global_id.to_s)
expect(result[:todos]).to contain_exactly(todo1, todo4)
expect(todo1.reload.state).to eq('pending')
expect(todo2.reload.state).to eq('pending')

View File

@ -10,7 +10,7 @@ RSpec.describe GitlabSchema.types['Commit'] do
it 'contains attributes related to commit' do
expect(described_class).to have_graphql_fields(
:id, :sha, :title, :description, :message, :title_html, :authored_date,
:author_name, :author_gravatar, :author, :web_url, :latest_pipeline,
:author_name, :author_gravatar, :author, :web_path, :web_url, :latest_pipeline,
:pipelines, :signature_html
)
end

View File

@ -5,5 +5,5 @@ require 'spec_helper'
RSpec.describe Types::Tree::BlobType do
specify { expect(described_class.graphql_name).to eq('Blob') }
specify { expect(described_class).to have_graphql_fields(:id, :sha, :name, :type, :path, :flat_path, :web_url, :lfs_oid) }
specify { expect(described_class).to have_graphql_fields(:id, :sha, :name, :type, :path, :flat_path, :web_path, :web_url, :lfs_oid) }
end

Some files were not shown because too many files have changed in this diff Show More