Commit d7ce7307 authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent e43077ab
# Make sure to update all the similar conditions in other CI config files if you modify these conditions
.if-not-ee: &if-not-ee
if: '$CI_PROJECT_NAME !~ /^gitlab(-ee)?$/'
# Make sure to update all the similar conditions in other CI config files if you modify these conditions
.if-default-refs: &if-default-refs
if: '$CI_COMMIT_REF_NAME == "master" || $CI_COMMIT_REF_NAME =~ /^[\d-]+-stable(-ee)?$/ || $CI_COMMIT_REF_NAME =~ /^\d+-\d+-auto-deploy-\d+$/ || $CI_COMMIT_REF_NAME =~ /^security\// || $CI_MERGE_REQUEST_IID || $CI_COMMIT_TAG'
# Make sure to update all the similar patterns in other CI config files if you modify these patterns
.code-backstage-patterns: &code-backstage-patterns
- ".gitlab/ci/**/*"
- ".{eslintignore,gitattributes,nvmrc,prettierrc,stylelintrc,yamllint}"
- ".{codeclimate,eslintrc,gitlab-ci,haml-lint,haml-lint_todo,rubocop,rubocop_todo,scss-lint}.yml"
- ".csscomb.json"
- "Dockerfile.assets"
- "*_VERSION"
- "Gemfile{,.lock}"
- "Rakefile"
- "{babel.config,jest.config}.js"
- "config.ru"
- "{package.json,yarn.lock}"
- "{,ee/}{app,bin,config,db,haml_lint,lib,locale,public,scripts,symbol,vendor}/**/*"
- "doc/api/graphql/reference/*" # Files in this folder are auto-generated
# Backstage changes
- "Dangerfile"
- "danger/**/*"
- "{,ee/}fixtures/**/*"
- "{,ee/}rubocop/**/*"
- "{,ee/}spec/**/*"
- "doc/README.md" # Some RSpec test rely on this file
.dev-fixtures:rules:ee-and-foss:
rules:
- <<: *if-default-refs
changes: *code-backstage-patterns
when: on_success
.dev-fixtures:rules:ee-only:
rules:
- <<: *if-not-ee
when: never
- <<: *if-default-refs
changes: *code-backstage-patterns
when: on_success
.run-dev-fixtures: .run-dev-fixtures:
extends: extends:
- .only-code-rails-job-base - .default-tags
- .default-retry
- .default-cache
- .default-before_script
- .use-pg9 - .use-pg9
stage: test stage: test
needs: ["setup-test-env"] needs: ["setup-test-env"]
...@@ -13,17 +61,19 @@ ...@@ -13,17 +61,19 @@
SIZE: 0 # number of external projects to fork, requires network connection SIZE: 0 # number of external projects to fork, requires network connection
# SEED_NESTED_GROUPS: "false" # requires network connection # SEED_NESTED_GROUPS: "false" # requires network connection
run-dev-fixtures-foss: run-dev-fixtures:
extends: .run-dev-fixtures extends:
- .run-dev-fixtures
- .dev-fixtures:rules:ee-and-foss
script: script:
- scripts/gitaly-test-spawn - scripts/gitaly-test-spawn
- RAILS_ENV=test bundle exec rake db:seed_fu - RAILS_ENV=test bundle exec rake db:seed_fu
run-dev-fixtures-ee: run-dev-fixtures-ee:
extends: extends:
- .only-ee
- .use-pg9-ee
- .run-dev-fixtures - .run-dev-fixtures
- .dev-fixtures:rules:ee-only
- .use-pg9-ee
script: script:
- scripts/gitaly-test-spawn - scripts/gitaly-test-spawn
- cp ee/db/fixtures/development/* $FIXTURE_PATH - cp ee/db/fixtures/development/* $FIXTURE_PATH
......
This diff is collapsed.
<script>
/**
* This component is an iterative step towards refactoring and simplifying `vue_shared/components/file_row.vue`
* https://gitlab.com/gitlab-org/gitlab/-/merge_requests/23720
*/
import FileRow from '~/vue_shared/components/file_row.vue';
export default {
components: {
FileRow,
},
};
</script>
<template>
<file-row v-bind="$attrs" v-on="$listeners" />
</template>
...@@ -3,7 +3,8 @@ import { mapActions, mapGetters, mapState } from 'vuex'; ...@@ -3,7 +3,8 @@ import { mapActions, mapGetters, mapState } from 'vuex';
import { GlTooltipDirective } from '@gitlab/ui'; import { GlTooltipDirective } from '@gitlab/ui';
import { s__, sprintf } from '~/locale'; import { s__, sprintf } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import FileRow from '~/vue_shared/components/file_row.vue'; import FileTree from '~/vue_shared/components/file_tree.vue';
import DiffFileRow from './diff_file_row.vue';
import FileRowStats from './file_row_stats.vue'; import FileRowStats from './file_row_stats.vue';
export default { export default {
...@@ -12,7 +13,7 @@ export default { ...@@ -12,7 +13,7 @@ export default {
}, },
components: { components: {
Icon, Icon,
FileRow, FileTree,
}, },
props: { props: {
hideFileStats: { hideFileStats: {
...@@ -61,6 +62,7 @@ export default { ...@@ -61,6 +62,7 @@ export default {
searchPlaceholder: sprintf(s__('MergeRequest|Search files (%{modifier_key}P)'), { searchPlaceholder: sprintf(s__('MergeRequest|Search files (%{modifier_key}P)'), {
modifier_key: /Mac/i.test(navigator.userAgent) ? '' : 'Ctrl+', modifier_key: /Mac/i.test(navigator.userAgent) ? '' : 'Ctrl+',
}), }),
DiffFileRow,
}; };
</script> </script>
...@@ -91,7 +93,7 @@ export default { ...@@ -91,7 +93,7 @@ export default {
</div> </div>
<div :class="{ 'pt-0 tree-list-blobs': !renderTreeList }" class="tree-list-scroll"> <div :class="{ 'pt-0 tree-list-blobs': !renderTreeList }" class="tree-list-scroll">
<template v-if="filteredTreeList.length"> <template v-if="filteredTreeList.length">
<file-row <file-tree
v-for="file in filteredTreeList" v-for="file in filteredTreeList"
:key="file.key" :key="file.key"
:file="file" :file="file"
...@@ -99,6 +101,7 @@ export default { ...@@ -99,6 +101,7 @@ export default {
:hide-extra-on-tree="true" :hide-extra-on-tree="true"
:extra-component="fileRowExtraComponent" :extra-component="fileRowExtraComponent"
:show-changed-icon="true" :show-changed-icon="true"
:file-row-component="$options.DiffFileRow"
@toggleTreeOpen="toggleTreeOpen" @toggleTreeOpen="toggleTreeOpen"
@clickFile="scrollToFile" @clickFile="scrollToFile"
/> />
......
<script>
/**
* This component is an iterative step towards refactoring and simplifying `vue_shared/components/file_row.vue`
* https://gitlab.com/gitlab-org/gitlab/-/merge_requests/23720
*/
import FileRow from '~/vue_shared/components/file_row.vue';
export default {
components: {
FileRow,
},
};
</script>
<template>
<file-row v-bind="$attrs" v-on="$listeners" />
</template>
<script> <script>
import { mapActions, mapGetters, mapState } from 'vuex'; import { mapActions, mapGetters, mapState } from 'vuex';
import { GlSkeletonLoading } from '@gitlab/ui'; import { GlSkeletonLoading } from '@gitlab/ui';
import FileRow from '~/vue_shared/components/file_row.vue'; import FileTree from '~/vue_shared/components/file_tree.vue';
import IdeFileRow from './ide_file_row.vue';
import NavDropdown from './nav_dropdown.vue'; import NavDropdown from './nav_dropdown.vue';
import FileRowExtra from './file_row_extra.vue'; import FileRowExtra from './file_row_extra.vue';
...@@ -9,7 +10,7 @@ export default { ...@@ -9,7 +10,7 @@ export default {
components: { components: {
GlSkeletonLoading, GlSkeletonLoading,
NavDropdown, NavDropdown,
FileRow, FileTree,
}, },
props: { props: {
viewerType: { viewerType: {
...@@ -36,6 +37,7 @@ export default { ...@@ -36,6 +37,7 @@ export default {
...mapActions(['updateViewer', 'toggleTreeOpen']), ...mapActions(['updateViewer', 'toggleTreeOpen']),
}, },
FileRowExtra, FileRowExtra,
IdeFileRow,
}; };
</script> </script>
...@@ -53,12 +55,13 @@ export default { ...@@ -53,12 +55,13 @@ export default {
</header> </header>
<div class="ide-tree-body h-100"> <div class="ide-tree-body h-100">
<template v-if="currentTree.tree.length"> <template v-if="currentTree.tree.length">
<file-row <file-tree
v-for="file in currentTree.tree" v-for="file in currentTree.tree"
:key="file.key" :key="file.key"
:file="file" :file="file"
:level="0" :level="0"
:extra-component="$options.FileRowExtra" :extra-component="$options.FileRowExtra"
:file-row-component="$options.IdeFileRow"
@toggleTreeOpen="toggleTreeOpen" @toggleTreeOpen="toggleTreeOpen"
/> />
</template> </template>
......
...@@ -195,8 +195,8 @@ export default { ...@@ -195,8 +195,8 @@ export default {
</gl-dropdown-item> </gl-dropdown-item>
<gl-dropdown-item <gl-dropdown-item
v-if="clipboardText" v-if="clipboardText"
ref="copyChartLink"
v-track-event="generateLinkToChartOptions(clipboardText)" v-track-event="generateLinkToChartOptions(clipboardText)"
class="js-chart-link"
:data-clipboard-text="clipboardText" :data-clipboard-text="clipboardText"
@click="showToast(clipboardText)" @click="showToast(clipboardText)"
> >
......
...@@ -62,9 +62,6 @@ export default { ...@@ -62,9 +62,6 @@ export default {
'is-open': this.file.opened, 'is-open': this.file.opened,
}; };
}, },
childFilesLevel() {
return this.file.isHeader ? 0 : this.level + 1;
},
}, },
watch: { watch: {
'file.active': function fileActiveWatch(active) { 'file.active': function fileActiveWatch(active) {
...@@ -131,53 +128,38 @@ export default { ...@@ -131,53 +128,38 @@ export default {
</script> </script>
<template> <template>
<div> <file-header v-if="file.isHeader" :path="file.path" />
<file-header v-if="file.isHeader" :path="file.path" /> <div
<div v-else
v-else :class="fileClass"
:class="fileClass" :title="file.name"
:title="file.name" class="file-row"
class="file-row" role="button"
role="button" @click="clickFile"
@click="clickFile" @mouseleave="toggleDropdown(false)"
@mouseleave="toggleDropdown(false)" >
> <div class="file-row-name-container">
<div class="file-row-name-container"> <span ref="textOutput" :style="levelIndentation" class="file-row-name str-truncated">
<span ref="textOutput" :style="levelIndentation" class="file-row-name str-truncated"> <file-icon
<file-icon v-if="!showChangedIcon || file.type === 'tree'"
v-if="!showChangedIcon || file.type === 'tree'" class="file-row-icon"
class="file-row-icon" :file-name="file.name"
:file-name="file.name" :loading="file.loading"
:loading="file.loading" :folder="isTree"
:folder="isTree" :opened="file.opened"
:opened="file.opened" :size="16"
:size="16"
/>
<changed-file-icon v-else :file="file" :size="16" class="append-right-5" />
{{ file.name }}
</span>
<component
:is="extraComponent"
v-if="extraComponent && !(hideExtraOnTree && file.type === 'tree')"
:file="file"
:dropdown-open="dropdownOpen"
@toggle="toggleDropdown($event)"
/> />
</div> <changed-file-icon v-else :file="file" :size="16" class="append-right-5" />
</div> {{ file.name }}
<template v-if="file.opened || file.isHeader"> </span>
<file-row <component
v-for="childFile in file.tree" :is="extraComponent"
:key="childFile.key" v-if="extraComponent && !(hideExtraOnTree && file.type === 'tree')"
:file="childFile" :file="file"
:level="childFilesLevel" :dropdown-open="dropdownOpen"
:hide-extra-on-tree="hideExtraOnTree" @toggle="toggleDropdown($event)"
:extra-component="extraComponent"
:show-changed-icon="showChangedIcon"
@toggleTreeOpen="toggleTreeOpen"
@clickFile="clickedFile"
/> />
</template> </div>
</div> </div>
</template> </template>
......
<script>
export default {
name: 'FileTree',
props: {
fileRowComponent: {
type: Object,
required: true,
},
level: {
type: Number,
required: true,
},
file: {
type: Object,
required: true,
},
},
computed: {
childFilesLevel() {
return this.file.isHeader ? 0 : this.level + 1;
},
},
};
</script>
<template>
<div>
<component
:is="fileRowComponent"
:level="level"
:file="file"
v-bind="$attrs"
v-on="$listeners"
/>
<template v-if="file.opened || file.isHeader">
<file-tree
v-for="childFile in file.tree"
:key="childFile.key"
:file-row-component="fileRowComponent"
:level="childFilesLevel"
:file="childFile"
v-bind="$attrs"
v-on="$listeners"
/>
</template>
</div>
</template>
---
title: Expose issue link type in REST API
merge_request: 24175
author:
type: added
...@@ -14,7 +14,7 @@ class MigrateEpicNotesMentionsToDb < ActiveRecord::Migration[5.2] ...@@ -14,7 +14,7 @@ class MigrateEpicNotesMentionsToDb < ActiveRecord::Migration[5.2]
INDEX_NAME = 'epic_mentions_temp_index' INDEX_NAME = 'epic_mentions_temp_index'
INDEX_CONDITION = "note LIKE '%@%'::text AND notes.noteable_type = 'Epic'" INDEX_CONDITION = "note LIKE '%@%'::text AND notes.noteable_type = 'Epic'"
QUERY_CONDITIONS = "#{INDEX_CONDITION} AND epic_user_mentions.epic_id IS NULL" QUERY_CONDITIONS = "#{INDEX_CONDITION} AND epic_user_mentions.epic_id IS NULL"
JOIN = 'LEFT JOIN epic_user_mentions ON notes.id = epic_user_mentions.note_id' JOIN = 'INNER JOIN epics ON epics.id = notes.noteable_id LEFT JOIN epic_user_mentions ON notes.id = epic_user_mentions.note_id'
class Note < ActiveRecord::Base class Note < ActiveRecord::Base
include EachBatch include EachBatch
......
...@@ -288,10 +288,13 @@ For source installations, edit the `gitlab.yml` and set the Sidekiq ...@@ -288,10 +288,13 @@ For source installations, edit the `gitlab.yml` and set the Sidekiq
## `gitlab-shell.log` ## `gitlab-shell.log`
This file lives in `/var/log/gitlab/gitlab-shell/gitlab-shell.log` for This file lives in `/var/log/gitlab/gitaly/gitlab-shell.log` for
Omnibus GitLab packages or in `/home/git/gitlab-shell/gitlab-shell.log` for Omnibus GitLab packages or in `/home/git/gitaly/gitlab-shell.log` for
installations from source. installations from source.
NOTE: **Note**
For GitLab 12.5 and earlier the file lives in `/var/log/gitlab/gitlab-shell/gitlab-shell.log`.
GitLab Shell is used by GitLab for executing Git commands and provide GitLab Shell is used by GitLab for executing Git commands and provide
SSH access to Git repositories. For example: SSH access to Git repositories. For example:
......
...@@ -67,7 +67,7 @@ POST /projects/:id/issues/:issue_iid/links ...@@ -67,7 +67,7 @@ POST /projects/:id/issues/:issue_iid/links
| `issue_iid` | integer | yes | The internal ID of a project's issue | | `issue_iid` | integer | yes | The internal ID of a project's issue |
| `target_project_id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) of a target project | | `target_project_id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) of a target project |
| `target_issue_iid` | integer/string | yes | The internal ID of a target project's issue | | `target_issue_iid` | integer/string | yes | The internal ID of a target project's issue |
| `link_type` | string | no | The type of the relation ("relates_to", "blocks", "is_blocked_by"), defaults to "relates_to"). Ignored unless `issue_link_types` feature flag is enabled. | | `link_type` | string | no | The type of the relation ("relates_to", "blocks", "is_blocked_by"), defaults to "relates_to"). |
```shell ```shell
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/4/issues/1/links?target_project_id=5&target_issue_iid=1" curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/4/issues/1/links?target_project_id=5&target_issue_iid=1"
......
...@@ -95,7 +95,7 @@ The following table depicts the various user permission levels in a project. ...@@ -95,7 +95,7 @@ The following table depicts the various user permission levels in a project.
| Stop environments | | | ✓ | ✓ | ✓ | | Stop environments | | | ✓ | ✓ | ✓ |
| Add tags | | | ✓ | ✓ | ✓ | | Add tags | | | ✓ | ✓ | ✓ |
| Cancel and retry jobs | | | ✓ | ✓ | ✓ | | Cancel and retry jobs | | | ✓ | ✓ | ✓ |
| Create or update commit status | | | ✓ | ✓ | ✓ | | Create or update commit status | | | ✓ (*5*) | ✓ | ✓ |
| Update a container registry | | | ✓ | ✓ | ✓ | | Update a container registry | | | ✓ | ✓ | ✓ |
| Remove a container registry image | | | ✓ | ✓ | ✓ | | Remove a container registry image | | | ✓ | ✓ | ✓ |
| Create/edit/delete project milestones | | | ✓ | ✓ | ✓ | | Create/edit/delete project milestones | | | ✓ | ✓ | ✓ |
...@@ -144,6 +144,7 @@ The following table depicts the various user permission levels in a project. ...@@ -144,6 +144,7 @@ The following table depicts the various user permission levels in a project.
(*2*): Guest users can only view the confidential issues they created themselves. (*2*): Guest users can only view the confidential issues they created themselves.
(*3*): If **Public pipelines** is enabled in **Project Settings > CI/CD**. (*3*): If **Public pipelines** is enabled in **Project Settings > CI/CD**.
(*4*): Not allowed for Guest, Reporter, Developer, Maintainer, or Owner. See [Protected Branches](./project/protected_branches.md). (*4*): Not allowed for Guest, Reporter, Developer, Maintainer, or Owner. See [Protected Branches](./project/protected_branches.md).
(*5*): If the [branch is protected](./project/protected_branches.md#using-the-allowed-to-merge-and-allowed-to-push-settings), this depends on the access Developers and Maintainers are given.
## Project features permissions ## Project features permissions
......
...@@ -13,7 +13,7 @@ module Gitlab ...@@ -13,7 +13,7 @@ module Gitlab
def perform(resource_model, join, conditions, with_notes, start_id, end_id) def perform(resource_model, join, conditions, with_notes, start_id, end_id)
resource_model = "#{ISOLATION_MODULE}::#{resource_model}".constantize if resource_model.is_a?(String) resource_model = "#{ISOLATION_MODULE}::#{resource_model}".constantize if resource_model.is_a?(String)
model = with_notes ? "#{ISOLATION_MODULE}::Note".constantize : resource_model model = with_notes ? Gitlab::BackgroundMigration::UserMentions::Models::Note : resource_model
resource_user_mention_model = resource_model.user_mention_model resource_user_mention_model = resource_model.user_mention_model
records = model.joins(join).where(conditions).where(id: start_id..end_id) records = model.joins(join).where(conditions).where(id: start_id..end_id)
...@@ -21,7 +21,7 @@ module Gitlab ...@@ -21,7 +21,7 @@ module Gitlab
records.in_groups_of(BULK_INSERT_SIZE, false).each do |records| records.in_groups_of(BULK_INSERT_SIZE, false).each do |records|
mentions = [] mentions = []
records.each do |record| records.each do |record|
mentions << record.build_mention_values mentions << record.build_mention_values(resource_user_mention_model.resource_foreign_key)
end end
Gitlab::Database.bulk_insert( Gitlab::Database.bulk_insert(
......
...@@ -65,11 +65,11 @@ module Gitlab ...@@ -65,11 +65,11 @@ module Gitlab
false false
end end
def build_mention_values def build_mention_values(resource_foreign_key)
refs = all_references(author) refs = all_references(author)
{ {
"#{self.user_mention_model.resource_foreign_key}": user_mention_resource_id, "#{resource_foreign_key}": user_mention_resource_id,
note_id: user_mention_note_id, note_id: user_mention_note_id,
mentioned_users_ids: array_to_sql(refs.mentioned_users.pluck(:id)), mentioned_users_ids: array_to_sql(refs.mentioned_users.pluck(:id)),
mentioned_projects_ids: array_to_sql(refs.mentioned_projects.pluck(:id)), mentioned_projects_ids: array_to_sql(refs.mentioned_projects.pluck(:id)),
......
...@@ -19,10 +19,6 @@ module Gitlab ...@@ -19,10 +19,6 @@ module Gitlab
belongs_to :noteable, polymorphic: true belongs_to :noteable, polymorphic: true
belongs_to :project belongs_to :project
def user_mention_model
"#{CreateResourceUserMention::ISOLATION_MODULE}::#{noteable.class}".constantize.user_mention_model
end
def for_personal_snippet? def for_personal_snippet?
noteable.class.name == 'PersonalSnippet' noteable.class.name == 'PersonalSnippet'
end end
......
import { shallowMount } from '@vue/test-utils';
import DiffFileRow from '~/diffs/components/diff_file_row.vue';
import FileRow from '~/vue_shared/components/file_row.vue';
describe('Diff File Row component', () => {
let wrapper;
const createComponent = (props = {}) => {
wrapper = shallowMount(DiffFileRow, {
propsData: { ...props },
});
};
afterEach(() => {
wrapper.destroy();
});
it('renders file row component', () => {
createComponent({
level: 4,
file: {},
});
expect(wrapper.find(FileRow).exists()).toEqual(true);
});
});
import { shallowMount } from '@vue/test-utils';
import IdeFileRow from '~/ide/components/ide_file_row.vue';
import FileRow from '~/vue_shared/components/file_row.vue';
describe('Ide File Row component', () => {
let wrapper;
const createComponent = (props = {}) => {
wrapper = shallowMount(IdeFileRow, {
propsData: { ...props },
});
};
afterEach(() => {
wrapper.destroy();
});
it('renders file row component', () => {
createComponent({
level: 4,
file: {},
});
expect(wrapper.find(FileRow).exists()).toEqual(true);
});
});
import { shallowMount, createLocalVue, mount } from '@vue/test-utils'; import { shallowMount, createLocalVue, mount } from '@vue/test-utils';
import { GlDropdownItem, GlButton, GlToast } from '@gitlab/ui'; import { GlDropdownItem, GlButton } from '@gitlab/ui';
import VueDraggable from 'vuedraggable'; import VueDraggable from 'vuedraggable';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
...@@ -10,6 +10,7 @@ import Dashboard from '~/monitoring/components/dashboard.vue'; ...@@ -10,6 +10,7 @@ import Dashboard from '~/monitoring/components/dashboard.vue';
import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue'; import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue';
import DashboardsDropdown from '~/monitoring/components/dashboards_dropdown.vue'; import DashboardsDropdown from '~/monitoring/components/dashboards_dropdown.vue';
import GroupEmptyState from '~/monitoring/components/group_empty_state.vue'; import GroupEmptyState from '~/monitoring/components/group_empty_state.vue';
import PanelType from 'ee_else_ce/monitoring/components/panel_type.vue';
import { createStore } from '~/monitoring/stores'; import { createStore } from '~/monitoring/stores';
import * as types from '~/monitoring/stores/mutation_types'; import * as types from '~/monitoring/stores/mutation_types';
import { setupComponentStore, propsData } from '../init_utils'; import { setupComponentStore, propsData } from '../init_utils';
...@@ -540,37 +541,36 @@ describe('Dashboard', () => { ...@@ -540,37 +541,36 @@ describe('Dashboard', () => {
}); });
}); });
// https://gitlab.com/gitlab-org/gitlab-ce/issues/66922 describe('Clipboard text in panels', () => {
// eslint-disable-next-line jest/no-disabled-tests
describe.skip('link to chart', () => {
const currentDashboard = 'TEST_DASHBOARD'; const currentDashboard = 'TEST_DASHBOARD';
localVue.use(GlToast);
const link = () => wrapper.find('.js-chart-link'); const getClipboardTextAt = i =>
const clipboardText = () => link().element.dataset.clipboardText; wrapper
.findAll(PanelType)
.at(i)
.props('clipboardText');
beforeEach(done => { beforeEach(done => {
createShallowWrapper({ hasMetrics: true, currentDashboard }); createShallowWrapper({ hasMetrics: true, currentDashboard });
setTimeout(done); setupComponentStore(wrapper);
});
it('adds a copy button to the dropdown', () => { wrapper.vm.$nextTick(done);
expect(link().text()).toContain('Generate link to chart');
}); });
it('contains a link to the dashboard', () => { it('contains a link to the dashboard', () => {
expect(clipboardText()).toContain(`dashboard=${currentDashboard}`); expect(getClipboardTextAt(0)).toContain(`dashboard=${currentDashboard}`);
expect(clipboardText()).toContain(`group=`); expect(getClipboardTextAt(0)).toContain(`group=`);
expect(clipboardText()).toContain(`title=`); expect(getClipboardTextAt(0)).toContain(`title=`);
expect(clipboardText()).toContain(`y_label=`); expect(getClipboardTextAt(0)).toContain(`y_label=`);
}); });
it('undefined parameter is stripped', done => { it('strips the undefined parameter', done => {
wrapper.setProps({ currentDashboard: undefined }); wrapper.setProps({ currentDashboard: undefined });
wrapper.vm.$nextTick(() => { wrapper.vm.$nextTick(() => {
expect(clipboardText()).not.toContain(`dashboard=`); expect(getClipboardTextAt(0)).not.toContain(`dashboard=`);
expect(clipboardText()).toContain(`y_label=`); expect(getClipboardTextAt(0)).toContain(`y_label=`);
done(); done();
}); });
}); });
...@@ -579,18 +579,10 @@ describe('Dashboard', () => { ...@@ -579,18 +579,10 @@ describe('Dashboard', () => {
wrapper.setProps({ currentDashboard: null }); wrapper.setProps({ currentDashboard: null });
wrapper.vm.$nextTick(() => { wrapper.vm.$nextTick(() => {
expect(clipboardText()).not.toContain(`dashboard=`); expect(getClipboardTextAt(0)).not.toContain(`dashboard=`);
expect(clipboardText()).toContain(`y_label=`); expect(getClipboardTextAt(0)).toContain(`y_label=`);
done(); done();
}); });
}); });
it('creates a toast when clicked', () => {
jest.spyOn(wrapper.vm.$toast, 'show').and.stub();
link().vm.$emit('click');
expect(wrapper.vm.$toast.show).toHaveBeenCalled();
});
}); });
}); });
...@@ -3,22 +3,29 @@ import AxiosMockAdapter from 'axios-mock-adapter'; ...@@ -3,22 +3,29 @@ import AxiosMockAdapter from 'axios-mock-adapter';
import { setTestTimeout } from 'helpers/timeout'; import { setTestTimeout } from 'helpers/timeout';
import invalidUrl from '~/lib/utils/invalid_url'; import invalidUrl from '~/lib/utils/invalid_url';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import PanelType from '~/monitoring/components/panel_type.vue'; import PanelType from '~/monitoring/components/panel_type.vue';
import EmptyChart from '~/monitoring/components/charts/empty_chart.vue'; import EmptyChart from '~/monitoring/components/charts/empty_chart.vue';
import TimeSeriesChart from '~/monitoring/components/charts/time_series.vue'; import TimeSeriesChart from '~/monitoring/components/charts/time_series.vue';
import AnomalyChart from '~/monitoring/components/charts/anomaly.vue'; import AnomalyChart from '~/monitoring/components/charts/anomaly.vue';
import { graphDataPrometheusQueryRange } from '../../javascripts/monitoring/mock_data'; import { anomalyMockGraphData, graphDataPrometheusQueryRange } from 'jest/monitoring/mock_data';
import { anomalyMockGraphData } from '../../frontend/monitoring/mock_data';
import { createStore } from '~/monitoring/stores'; import { createStore } from '~/monitoring/stores';
global.IS_EE = true; global.IS_EE = true;
global.URL.createObjectURL = jest.fn(); global.URL.createObjectURL = jest.fn();
const mocks = {
$toast: {
show: jest.fn(),
},
};
describe('Panel Type component', () => { describe('Panel Type component', () => {
let axiosMock; let axiosMock;
let store; let store;
let state; let state;
let wrapper; let wrapper;
const exampleText = 'example_text'; const exampleText = 'example_text';
const createWrapper = props => { const createWrapper = props => {
...@@ -27,6 +34,7 @@ describe('Panel Type component', () => { ...@@ -27,6 +34,7 @@ describe('Panel Type component', () => {
...props, ...props,
}, },
store, store,
mocks,
}); });
}; };
...@@ -88,7 +96,7 @@ describe('Panel Type component', () => { ...@@ -88,7 +96,7 @@ describe('Panel Type component', () => {
}); });
it('sets no clipboard copy link on dropdown by default', () => { it('sets no clipboard copy link on dropdown by default', () => {
const link = () => wrapper.find('.js-chart-link'); const link = () => wrapper.find({ ref: 'copyChartLink' });
expect(link().exists()).toBe(false); expect(link().exists()).toBe(false);
}); });
...@@ -196,6 +204,7 @@ describe('Panel Type component', () => { ...@@ -196,6 +204,7 @@ describe('Panel Type component', () => {
}); });
describe('when cliboard data is available', () => { describe('when cliboard data is available', () => {
const link = () => wrapper.find({ ref: 'copyChartLink' });
const clipboardText = 'A value to copy.'; const clipboardText = 'A value to copy.';
beforeEach(() => { beforeEach(() => {
...@@ -210,11 +219,19 @@ describe('Panel Type component', () => { ...@@ -210,11 +219,19 @@ describe('Panel Type component', () => {
}); });
it('sets clipboard text on the dropdown', () => { it('sets clipboard text on the dropdown', () => {
const link = () => wrapper.find('.js-chart-link');
expect(link().exists()).toBe(true); expect(link().exists()).toBe(true);
expect(link().element.dataset.clipboardText).toBe(clipboardText); expect(link().element.dataset.clipboardText).toBe(clipboardText);
}); });
it('adds a copy button to the dropdown', () => {
expect(link().text()).toContain('Generate link to chart');
});
it('opens a toast on click', () => {
link().vm.$emit('click');
expect(wrapper.vm.$toast.show).toHaveBeenCalled();
});
}); });
describe('when downloading metrics data as CSV', () => { describe('when downloading metrics data as CSV', () => {
......
import { pick } from 'lodash';
import { shallowMount } from '@vue/test-utils';
import FileTree from '~/vue_shared/components/file_tree.vue';
const MockFileRow = {
name: 'MockFileRow',
render() {
return this.$slots.default;
},
};
const TEST_LEVEL = 4;
const TEST_EXTA_ARGS = {
foo: 'lorem-ipsum',
bar: 'zoo',
};
describe('File Tree component', () => {
let wrapper;
const createComponent = (props = {}) => {
wrapper = shallowMount(FileTree, {
propsData: { level: TEST_LEVEL, fileRowComponent: MockFileRow, ...props },
attrs: { ...TEST_EXTA_ARGS },
});
};
const findFileRow = () => wrapper.find(MockFileRow);
const findChildrenTrees = () => wrapper.findAll(FileTree).wrappers.slice(1);
const findChildrenTreeProps = () =>
findChildrenTrees().map(x => ({
...x.props(),
...pick(x.attributes(), Object.keys(TEST_EXTA_ARGS)),
}));
afterEach(() => {
wrapper.destroy();
});
describe('file row component', () => {
beforeEach(() => {
createComponent({ file: {} });
});
it('renders file row component', () => {
expect(findFileRow().exists()).toEqual(true);
});
it('contains the required attribute keys', () => {
const fileRow = findFileRow();
// Checking strings b/c value in attributes are always strings
expect(fileRow.attributes()).toEqual({
file: {}.toString(),
level: TEST_LEVEL.toString(),
...TEST_EXTA_ARGS,
});
});
});
describe('file tree', () => {
const createChildren = () => [{ id: 1 }, { id: 2 }];
const createChildrenExpectation = (props = {}) =>
createChildren().map(file => ({
fileRowComponent: MockFileRow,
file,
...TEST_EXTA_ARGS,
...props,
}));
it.each`
key | value | desc | expectedChildren
${'isHeader'} | ${true} | ${'is shown if file is header'} | ${createChildrenExpectation({ level: 0 })}
${'opened'} | ${true} | ${'is shown if file is open'} | ${createChildrenExpectation({ level: TEST_LEVEL + 1 })}
${'isHeader'} | ${false} | ${'is hidden if file is header'} | ${[]}
${'opened'} | ${false} | ${'is hidden if file is open'} | ${[]}
`('$desc', ({ key, value, expectedChildren }) => {
createComponent({
file: {
[key]: value,
tree: createChildren(),
},
});
expect(findChildrenTreeProps()).toEqual(expectedChildren);
});
});
});
...@@ -19,7 +19,6 @@ describe('File row component', () => { ...@@ -19,7 +19,6 @@ describe('File row component', () => {
const findNewDropdown = () => vm.$el.querySelector('.ide-new-btn .dropdown'); const findNewDropdown = () => vm.$el.querySelector('.ide-new-btn .dropdown');
const findNewDropdownButton = () => vm.$el.querySelector('.ide-new-btn .dropdown button'); const findNewDropdownButton = () => vm.$el.querySelector('.ide-new-btn .dropdown button');
const findFileRow = () => vm.$el.querySelector('.file-row');
it('renders name', () => { it('renders name', () => {
createComponent({ createComponent({
...@@ -42,7 +41,7 @@ describe('File row component', () => { ...@@ -42,7 +41,7 @@ describe('File row component', () => {
}); });
spyOn(vm, '$emit').and.stub(); spyOn(vm, '$emit').and.stub();
vm.$el.querySelector('.file-row').click(); vm.$el.click();
expect(vm.$emit).toHaveBeenCalledWith('toggleTreeOpen', vm.file.path); expect(vm.$emit).toHaveBeenCalledWith('toggleTreeOpen', vm.file.path);
}); });
...@@ -87,7 +86,7 @@ describe('File row component', () => { ...@@ -87,7 +86,7 @@ describe('File row component', () => {
level: 0, level: 0,
}); });
expect(vm.$el.querySelector('.js-file-row-header')).not.toBe(null); expect(vm.$el.classList).toContain('js-file-row-header');
}); });
describe('new dropdown', () => { describe('new dropdown', () => {
...@@ -138,7 +137,7 @@ describe('File row component', () => { ...@@ -138,7 +137,7 @@ describe('File row component', () => {
}); });
it('closes when row triggers mouseleave', () => { it('closes when row triggers mouseleave', () => {
findFileRow().dispatchEvent(new Event('mouseleave')); vm.$el.dispatchEvent(new Event('mouseleave'));
expect(vm.dropdownOpen).toBe(false); expect(vm.dropdownOpen).toBe(false);
}); });
......
# frozen_string_literal: true
shared_examples 'resource mentions migration' do |migration_class, resource_class|
it 'migrates resource mentions' do
join = migration_class::JOIN
conditions = migration_class::QUERY_CONDITIONS
expect do
subject.perform(resource_class.name, join, conditions, false, resource_class.minimum(:id), resource_class.maximum(:id))
end.to change { user_mentions.count }.by(1)
user_mention = user_mentions.last
expect(user_mention.mentioned_users_ids.sort).to eq(mentioned_users.pluck(:id).sort)
expect(user_mention.mentioned_groups_ids.sort).to eq([group.id])
expect(user_mention.mentioned_groups_ids.sort).not_to include(inaccessible_group.id)
# check that performing the same job twice does not fail and does not change counts
expect do
subject.perform(resource_class.name, join, conditions, false, resource_class.minimum(:id), resource_class.maximum(:id))
end.to change { user_mentions.count }.by(0)
end
end
shared_examples 'resource notes mentions migration' do |migration_class, resource_class|
before do
note1.becomes(Note).save!
note2.becomes(Note).save!
note3.becomes(Note).save!
# note4.becomes(Note).save(validate: false)
end
it 'migrates mentions from note' do
join = migration_class::JOIN
conditions = migration_class::QUERY_CONDITIONS
# there are 4 notes for each noteable_type, but one does not have mentions and
# another one's noteable_id points to an inexistent resource
expect(notes.where(noteable_type: resource_class.to_s).count).to eq 4
expect do
subject.perform(resource_class.name, join, conditions, true, Note.minimum(:id), Note.maximum(:id))
end.to change { user_mentions.count }.by(2)
# check that the user_mention for regular note is created
user_mention = user_mentions.first
expect(Note.find(user_mention.note_id).system).to be false
expect(user_mention.mentioned_users_ids.sort).to eq(users.pluck(:id).sort)
expect(user_mention.mentioned_groups_ids.sort).to eq([group.id])
expect(user_mention.mentioned_groups_ids.sort).not_to include(inaccessible_group.id)
# check that the user_mention for system note is created
user_mention = user_mentions.second
expect(Note.find(user_mention.note_id).system).to be true
expect(user_mention.mentioned_users_ids.sort).to eq(users.pluck(:id).sort)
expect(user_mention.mentioned_groups_ids.sort).to eq([group.id])
expect(user_mention.mentioned_groups_ids.sort).not_to include(inaccessible_group.id)
# check that performing the same job twice does not fail and does not change counts
expect do
subject.perform(resource_class.name, join, conditions, true, Note.minimum(:id), Note.maximum(:id))
end.to change { user_mentions.count }.by(0)
end
end
shared_examples 'schedules resource mentions migration' do |resource_class, is_for_notes|
it 'schedules background migrations' do
Sidekiq::Testing.fake! do
Timecop.freeze do
migrate!
migration = described_class::MIGRATION
join = described_class::JOIN
conditions = described_class::QUERY_CONDITIONS
expect(migration).to be_scheduled_delayed_migration(2.minutes, resource_class.name, join, conditions, is_for_notes, resource1.id, resource1.id)
expect(migration).to be_scheduled_delayed_migration(4.minutes, resource_class.name, join, conditions, is_for_notes, resource2.id, resource2.id)
expect(migration).to be_scheduled_delayed_migration(6.minutes, resource_class.name, join, conditions, is_for_notes, resource3.id, resource3.id)
expect(BackgroundMigrationWorker.jobs.size).to eq 3
end
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment