Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
1abf48c10c
commit
cc803c04b8
27 changed files with 581 additions and 199 deletions
|
@ -1,11 +1,11 @@
|
|||
<script>
|
||||
import { GlButton } from '@gitlab/ui';
|
||||
import { GlButton, GlLink } from '@gitlab/ui';
|
||||
import ExperimentTracking from '~/experimentation/experiment_tracking';
|
||||
import { s__ } from '~/locale';
|
||||
import eventHub from '../event_hub';
|
||||
|
||||
export default {
|
||||
components: { GlButton },
|
||||
components: { GlButton, GlLink },
|
||||
props: {
|
||||
displayText: {
|
||||
type: String,
|
||||
|
@ -37,6 +37,42 @@ export default {
|
|||
required: false,
|
||||
default: undefined,
|
||||
},
|
||||
triggerElement: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'button',
|
||||
},
|
||||
event: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
isButton() {
|
||||
return this.triggerElement === 'button';
|
||||
},
|
||||
componentAttributes() {
|
||||
const baseAttributes = {
|
||||
class: this.classes,
|
||||
'data-qa-selector': 'invite_members_button',
|
||||
};
|
||||
|
||||
if (this.event && this.label) {
|
||||
return {
|
||||
...baseAttributes,
|
||||
'data-track-event': this.event,
|
||||
'data-track-label': this.label,
|
||||
};
|
||||
}
|
||||
|
||||
return baseAttributes;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.trackExperimentOnShow();
|
||||
|
@ -57,12 +93,15 @@ export default {
|
|||
|
||||
<template>
|
||||
<gl-button
|
||||
:class="classes"
|
||||
:icon="icon"
|
||||
v-if="isButton"
|
||||
v-bind="componentAttributes"
|
||||
:variant="variant"
|
||||
data-qa-selector="invite_members_button"
|
||||
:icon="icon"
|
||||
@click="openModal"
|
||||
>
|
||||
{{ displayText }}
|
||||
</gl-button>
|
||||
<gl-link v-else v-bind="componentAttributes" data-is-link="true" @click="openModal">
|
||||
{{ displayText }}
|
||||
</gl-link>
|
||||
</template>
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
import { mapState, mapActions } from 'vuex';
|
||||
import { __, s__ } from '~/locale';
|
||||
import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue';
|
||||
import UrlSync from '~/vue_shared/components/url_sync.vue';
|
||||
import getTableHeaders from '../utils';
|
||||
import PackageTypeToken from './tokens/package_type_token.vue';
|
||||
|
||||
|
@ -16,7 +17,7 @@ export default {
|
|||
operators: [{ value: '=', description: __('is'), default: 'true' }],
|
||||
},
|
||||
],
|
||||
components: { RegistrySearch },
|
||||
components: { RegistrySearch, UrlSync },
|
||||
computed: {
|
||||
...mapState({
|
||||
isGroupPage: (state) => state.config.isGroupPage,
|
||||
|
@ -38,13 +39,18 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<registry-search
|
||||
:filter="filter"
|
||||
:sorting="sorting"
|
||||
:tokens="$options.tokens"
|
||||
:sortable-fields="sortableFields"
|
||||
@sorting:changed="updateSorting"
|
||||
@filter:changed="setFilter"
|
||||
@filter:submit="$emit('update')"
|
||||
/>
|
||||
<url-sync>
|
||||
<template #default="{ updateQuery }">
|
||||
<registry-search
|
||||
:filter="filter"
|
||||
:sorting="sorting"
|
||||
:tokens="$options.tokens"
|
||||
:sortable-fields="sortableFields"
|
||||
@sorting:changed="updateSorting"
|
||||
@filter:changed="setFilter"
|
||||
@filter:submit="$emit('update')"
|
||||
@query:changed="updateQuery"
|
||||
/>
|
||||
</template>
|
||||
</url-sync>
|
||||
</template>
|
||||
|
|
|
@ -6,6 +6,7 @@ import { historyReplaceState } from '~/lib/utils/common_utils';
|
|||
import { s__ } from '~/locale';
|
||||
import { SHOW_DELETE_SUCCESS_ALERT } from '~/packages/shared/constants';
|
||||
import { FILTERED_SEARCH_TERM } from '~/packages_and_registries/shared/constants';
|
||||
import { getQueryParams, extractFilterAndSorting } from '~/packages_and_registries/shared/utils';
|
||||
import { DELETE_PACKAGE_SUCCESS_MESSAGE } from '../constants';
|
||||
import PackageSearch from './package_search.vue';
|
||||
import PackageTitle from './package_title.vue';
|
||||
|
@ -42,11 +43,21 @@ export default {
|
|||
},
|
||||
},
|
||||
mounted() {
|
||||
const queryParams = getQueryParams(window.document.location.search);
|
||||
const { sorting, filters } = extractFilterAndSorting(queryParams);
|
||||
this.setSorting(sorting);
|
||||
this.setFilter(filters);
|
||||
this.requestPackagesList();
|
||||
this.checkDeleteAlert();
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['requestPackagesList', 'requestDeletePackage', 'setSelectedType']),
|
||||
...mapActions([
|
||||
'requestPackagesList',
|
||||
'requestDeletePackage',
|
||||
'setSelectedType',
|
||||
'setSorting',
|
||||
'setFilter',
|
||||
]),
|
||||
onPageChanged(page) {
|
||||
return this.requestPackagesList({ page });
|
||||
},
|
||||
|
|
|
@ -7,3 +7,23 @@ export const keyValueToFilterToken = (type, data) => ({ type, value: { data } })
|
|||
|
||||
export const searchArrayToFilterTokens = (search) =>
|
||||
search.map((s) => keyValueToFilterToken(FILTERED_SEARCH_TERM, s));
|
||||
|
||||
export const extractFilterAndSorting = (queryObject) => {
|
||||
const { type, search, sort, orderBy } = queryObject;
|
||||
const filters = [];
|
||||
const sorting = {};
|
||||
|
||||
if (type) {
|
||||
filters.push(keyValueToFilterToken('type', type));
|
||||
}
|
||||
if (search) {
|
||||
filters.push(...searchArrayToFilterTokens(search));
|
||||
}
|
||||
if (sort) {
|
||||
sorting.sort = sort;
|
||||
}
|
||||
if (orderBy) {
|
||||
sorting.orderBy = orderBy;
|
||||
}
|
||||
return { filters, sorting };
|
||||
};
|
||||
|
|
|
@ -4,6 +4,7 @@ import initIssuableSidebar from '~/init_issuable_sidebar';
|
|||
import initInviteMemberModal from '~/invite_member/init_invite_member_modal';
|
||||
import initInviteMemberTrigger from '~/invite_member/init_invite_member_trigger';
|
||||
import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
|
||||
import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger';
|
||||
import { IssuableType } from '~/issuable_show/constants';
|
||||
import Issue from '~/issue';
|
||||
import '~/notes/index';
|
||||
|
@ -36,6 +37,7 @@ export default function initShowIssue() {
|
|||
initSentryErrorStackTraceApp();
|
||||
initRelatedMergeRequestsApp();
|
||||
initInviteMembersModal();
|
||||
initInviteMembersTrigger();
|
||||
|
||||
import(/* webpackChunkName: 'design_management' */ '~/design_management')
|
||||
.then((module) => module.default())
|
||||
|
|
|
@ -6,6 +6,7 @@ import initIssuableSidebar from '~/init_issuable_sidebar';
|
|||
import initInviteMemberModal from '~/invite_member/init_invite_member_modal';
|
||||
import initInviteMemberTrigger from '~/invite_member/init_invite_member_trigger';
|
||||
import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
|
||||
import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger';
|
||||
import { handleLocationHash } from '~/lib/utils/common_utils';
|
||||
import StatusBox from '~/merge_request/components/status_box.vue';
|
||||
import initSourcegraph from '~/sourcegraph';
|
||||
|
@ -22,6 +23,7 @@ export default function initMergeRequestShow() {
|
|||
initInviteMemberModal();
|
||||
initInviteMemberTrigger();
|
||||
initInviteMembersModal();
|
||||
initInviteMembersTrigger();
|
||||
|
||||
const el = document.querySelector('.js-mr-status-box');
|
||||
// eslint-disable-next-line no-new
|
||||
|
|
|
@ -12,6 +12,7 @@ import { get } from 'lodash';
|
|||
import getContainerRepositoriesQuery from 'shared_queries/container_registry/get_container_repositories.query.graphql';
|
||||
import createFlash from '~/flash';
|
||||
import { FILTERED_SEARCH_TERM } from '~/packages_and_registries/shared/constants';
|
||||
import { extractFilterAndSorting } from '~/packages_and_registries/shared/utils';
|
||||
import Tracking from '~/tracking';
|
||||
import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue';
|
||||
import DeleteImage from '../components/delete_image.vue';
|
||||
|
@ -82,6 +83,9 @@ export default {
|
|||
searchConfig: SORT_FIELDS,
|
||||
apollo: {
|
||||
baseImages: {
|
||||
skip() {
|
||||
return !this.fetchBaseQuery;
|
||||
},
|
||||
query: getContainerRepositoriesQuery,
|
||||
variables() {
|
||||
return this.queryVariables;
|
||||
|
@ -125,15 +129,19 @@ export default {
|
|||
sorting: { orderBy: 'UPDATED', sort: 'desc' },
|
||||
name: null,
|
||||
mutationLoading: false,
|
||||
fetchBaseQuery: false,
|
||||
fetchAdditionalDetails: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
images() {
|
||||
return this.baseImages.map((image, index) => ({
|
||||
...image,
|
||||
...get(this.additionalDetails, index, {}),
|
||||
}));
|
||||
if (this.baseImages) {
|
||||
return this.baseImages.map((image, index) => ({
|
||||
...image,
|
||||
...get(this.additionalDetails, index, {}),
|
||||
}));
|
||||
}
|
||||
return [];
|
||||
},
|
||||
graphqlResource() {
|
||||
return this.config.isGroupPage ? 'group' : 'project';
|
||||
|
@ -172,8 +180,15 @@ export default {
|
|||
},
|
||||
},
|
||||
mounted() {
|
||||
const { sorting, filters } = extractFilterAndSorting(this.$route.query);
|
||||
|
||||
this.filter = [...filters];
|
||||
this.name = filters[0]?.value.data;
|
||||
this.sorting = { ...this.sorting, ...sorting };
|
||||
|
||||
// If the two graphql calls - which are not batched - resolve togheter we will have a race
|
||||
// condition when apollo sets the cache, with this we give the 'base' call an headstart
|
||||
this.fetchBaseQuery = true;
|
||||
setTimeout(() => {
|
||||
this.fetchAdditionalDetails = true;
|
||||
}, 200);
|
||||
|
@ -245,6 +260,9 @@ export default {
|
|||
const search = this.filter.find((i) => i.type === FILTERED_SEARCH_TERM);
|
||||
this.name = search?.value?.data;
|
||||
},
|
||||
updateUrlQueryString(query) {
|
||||
this.$router.push({ query });
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -304,6 +322,7 @@ export default {
|
|||
@sorting:changed="updateSorting"
|
||||
@filter:changed="filter = $event"
|
||||
@filter:submit="doFilter"
|
||||
@query:changed="updateUrlQueryString"
|
||||
/>
|
||||
|
||||
<div v-if="isLoading" class="gl-mt-5">
|
||||
|
|
|
@ -53,10 +53,10 @@
|
|||
%ul.dropdown-footer-list
|
||||
%li
|
||||
- if directly_invite_members?
|
||||
= link_to invite_text,
|
||||
project_project_members_path(@project),
|
||||
title: invite_text,
|
||||
data: { 'is-link': true, 'track-event': 'click_invite_members', 'track-label': track_label }
|
||||
.js-invite-members-trigger{ data: { trigger_element: 'anchor',
|
||||
display_text: invite_text,
|
||||
event: 'click_invite_members',
|
||||
label: track_label } }
|
||||
- else
|
||||
.js-invite-member-trigger{ data: { display_text: invite_text, event: 'click_invite_members_version_b', label: track_label } }
|
||||
- else
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Change assignee dropdown invite to utilize invite modal
|
||||
merge_request: 57002
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Connect Registries searches to URL
|
||||
merge_request: 57251
|
||||
author:
|
||||
type: added
|
|
@ -6,8 +6,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
|
||||
# PostgreSQL replication and failover with Omnibus GitLab **(PREMIUM SELF)**
|
||||
|
||||
This document focuses on configuration supported with [GitLab Premium](https://about.gitlab.com/pricing/), using the Omnibus GitLab package.
|
||||
If you're a Community Edition or Starter user, consider using a cloud hosted solution.
|
||||
If you're a Free user of GitLab self-managed, consider using a cloud-hosted solution.
|
||||
This document doesn't cover installations from source.
|
||||
|
||||
If a setup with replication and failover isn't what you were looking for, see
|
||||
|
|
|
@ -215,7 +215,7 @@ Example response:
|
|||
|
||||
## Update an issue board
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/5954) in [GitLab Starter](https://about.gitlab.com/pricing/) 11.1.
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/5954) in GitLab 11.1.
|
||||
|
||||
Updates a project issue board.
|
||||
|
||||
|
@ -228,10 +228,10 @@ PUT /projects/:id/boards/:board_id
|
|||
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
|
||||
| `board_id` | integer | yes | The ID of a board |
|
||||
| `name` | string | no | The new name of the board |
|
||||
| `assignee_id` **(STARTER)** | integer | no | The assignee the board should be scoped to |
|
||||
| `milestone_id` **(STARTER)** | integer | no | The milestone the board should be scoped to |
|
||||
| `labels` **(STARTER)** | string | no | Comma-separated list of label names which the board should be scoped to |
|
||||
| `weight` **(STARTER)** | integer | no | The weight range from 0 to 9, to which the board should be scoped to |
|
||||
| `assignee_id` **(PREMIUM)** | integer | no | The assignee the board should be scoped to |
|
||||
| `milestone_id` **(PREMIUM)** | integer | no | The milestone the board should be scoped to |
|
||||
| `labels` **(PREMIUM)** | string | no | Comma-separated list of label names which the board should be scoped to |
|
||||
| `weight` **(PREMIUM)** | integer | no | The weight range from 0 to 9, to which the board should be scoped to |
|
||||
|
||||
```shell
|
||||
curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/boards/1?name=new_name&milestone_id=43&assignee_id=1&labels=Doing&weight=4"
|
||||
|
|
|
@ -183,20 +183,52 @@ file.
|
|||
|
||||
To add a redirect:
|
||||
|
||||
1. Create a merge request in one of the internal docs projects (`gitlab`,
|
||||
`gitlab-runner`, `omnibus-gitlab`, or `charts`), depending on the location of
|
||||
the file that's being moved, renamed, or removed.
|
||||
1. To move or rename the documentation file, create a new file with the new
|
||||
name or location, but don't delete the existing documentation file.
|
||||
1. In the original documentation file, add the redirect code for
|
||||
`/help`. Use the following template exactly, and change only the links and
|
||||
date. Use relative paths and `.md` for a redirect to another documentation
|
||||
page. Use the full URL (with `https://`) to redirect to a different project or
|
||||
site:
|
||||
1. In the repository (`gitlab`, `gitlab-runner`, `omnibus-gitlab`, or `charts`),
|
||||
create a new documentation file. Don't delete the old one. The easiest
|
||||
way is to copy it. For example:
|
||||
|
||||
```shell
|
||||
cp doc/user/search/old_file.md doc/api/new_file.md
|
||||
```
|
||||
|
||||
1. Add the redirect code to the old documentation file by running the
|
||||
following Rake task. The first argument is the path of the old file,
|
||||
and the second argument is the path of the new file:
|
||||
|
||||
- To redirect to a page in the same project, use relative paths and
|
||||
the `.md` extension. Both old and new paths start from the same location.
|
||||
In the following example, both paths are relative to `doc/`:
|
||||
|
||||
```shell
|
||||
bundle exec rake "gitlab:docs:redirect[doc/user/search/old_file.md, doc/api/new_file.md]"
|
||||
```
|
||||
|
||||
- To redirect to a page in a different project or site, use the full URL (with `https://`) :
|
||||
|
||||
```shell
|
||||
bundle exec rake "gitlab:docs:redirect[doc/user/search/old_file.md, https://example.com]"
|
||||
```
|
||||
|
||||
Alternatively, you can omit the arguments, and you'll be asked to enter
|
||||
their values:
|
||||
|
||||
```shell
|
||||
bundle exec rake gitlab:docs:redirect
|
||||
```
|
||||
|
||||
If you don't want to use the Rake task, you can use the following template.
|
||||
However, the file paths must be relative to the `doc` or `docs` directory.
|
||||
|
||||
Replace the value of `redirect_to` with the new file path and `YYYY-MM-DD`
|
||||
with the date the file should be removed.
|
||||
|
||||
Redirect files that link to docs in internal documentation projects
|
||||
are removed after three months. Redirect files that link to external sites are
|
||||
removed after one year:
|
||||
|
||||
```markdown
|
||||
---
|
||||
redirect_to: '../path/to/file/index.md'
|
||||
redirect_to: '../newpath/to/file/index.md'
|
||||
---
|
||||
|
||||
This document was moved to [another location](../path/to/file/index.md).
|
||||
|
@ -205,27 +237,24 @@ To add a redirect:
|
|||
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->
|
||||
```
|
||||
|
||||
Redirect files linking to docs in any of the internal documentations projects
|
||||
are removed after three months. Redirect files linking to external sites are
|
||||
removed after one year.
|
||||
|
||||
1. If the documentation page being moved has any Disqus comments, follow the steps
|
||||
described in [Redirections for pages with Disqus comments](#redirections-for-pages-with-disqus-comments).
|
||||
1. If a documentation page you're removing includes images that aren't used
|
||||
1. Open a merge request with your changes. If a documentation page
|
||||
you're removing includes images that aren't used
|
||||
with any other documentation pages, be sure to use your merge request to delete
|
||||
those images from the repository.
|
||||
1. Assign the merge request to a technical writer for review and merge.
|
||||
1. Search for links to the original documentation file. You must find and update all
|
||||
links that point to the original documentation file:
|
||||
1. Search for links to the old documentation file. You must find and update all
|
||||
links that point to the old documentation file:
|
||||
|
||||
- In <https://gitlab.com/gitlab-com/www-gitlab-com>, search for full URLs:
|
||||
`grep -r "docs.gitlab.com/ee/path/to/file.html" .`
|
||||
- In <https://gitlab.com/gitlab-org/gitlab-docs/-/tree/master/content/_data>,
|
||||
search the navigation bar configuration files for the path with `.html`:
|
||||
`grep -r "path/to/file.html" .`
|
||||
- In any of the four internal projects. This includes searching for links in the docs
|
||||
- In any of the four internal projects, search for links in the docs
|
||||
and codebase. Search for all variations, including full URL and just the path.
|
||||
In macOS for example, go to the root directory of the `gitlab` project and run:
|
||||
For example, go to the root directory of the `gitlab` project and run:
|
||||
|
||||
```shell
|
||||
grep -r "docs.gitlab.com/ee/path/to/file.html" .
|
||||
|
|
|
@ -302,7 +302,7 @@ end
|
|||
|
||||
Adding foreign key to `projects`:
|
||||
|
||||
We can use the `add_concurrenct_foreign_key` method in this case, as this helper method
|
||||
We can use the `add_concurrent_foreign_key` method in this case, as this helper method
|
||||
has the lock retries built into it.
|
||||
|
||||
```ruby
|
||||
|
|
|
@ -6,93 +6,93 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
|
||||
# Jira DVCS connector
|
||||
|
||||
If you're using GitLab.com and Jira Cloud, use the
|
||||
[GitLab for Jira app](connect-app.md) unless you have a specific need for the DVCS Connector.
|
||||
Use the Jira DVCS (distributed version control system) connector if you self-host
|
||||
either your Jira instance or your GitLab instance, and you want to sync information
|
||||
between them. If you use Jira Cloud and GitLab.com, you should use the
|
||||
[GitLab for Jira app](connect-app.md) unless you specifically need the DVCS connector.
|
||||
|
||||
When configuring Jira DVCS Connector:
|
||||
When you configure the Jira DVCS connector, make sure your GitLab and Jira instances
|
||||
are accessible.
|
||||
|
||||
- If you are using self-managed GitLab, make sure your GitLab instance is accessible by Jira.
|
||||
- If you're connecting to Jira Cloud, ensure your instance is accessible through the internet.
|
||||
- If you are using Jira Server, make sure your instance is accessible however your network is set up.
|
||||
- **Self-managed GitLab**: Your GitLab instance must be accessible by Jira.
|
||||
- **Jira Cloud**: Your instance must be accessible through the internet.
|
||||
- **Jira Server**: Your network must allow access to your instance.
|
||||
|
||||
## GitLab account configuration for DVCS
|
||||
## Configure a GitLab application for DVCS
|
||||
|
||||
NOTE:
|
||||
To ensure that regular user account maintenance doesn't impact your integration,
|
||||
create and use a single-purpose `jira` user in GitLab.
|
||||
We recommend you create and use a `jira` user in GitLab, and use the account only
|
||||
for integration work. A separate account ensures regular account maintenance does not affect
|
||||
your integration.
|
||||
|
||||
1. In GitLab, create a new application to allow Jira to connect with your GitLab account.
|
||||
1. Sign in to the GitLab account that you want Jira to use to connect to GitLab.
|
||||
1. In the top-right corner, select your avatar.
|
||||
1. Select **Edit profile**.
|
||||
1. In GitLab, [create a user](../../user/profile/account/create_accounts.md) for Jira to
|
||||
use to connect to GitLab. For Jira to access all projects,
|
||||
a user with [Administrator](../../user/permissions.md) permissions must
|
||||
create the user.
|
||||
1. In the top right corner, click the account's avatar, and select **Edit profile**.
|
||||
1. In the left sidebar, select **Applications**.
|
||||
1. In the **Name** field, enter a descriptive name for the integration, such as `Jira`.
|
||||
1. In the **Redirect URI** field, enter `https://<gitlab.example.com>/login/oauth/callback`,
|
||||
replacing `<gitlab.example.com>` with your GitLab instance domain. For example, if you are using GitLab.com,
|
||||
this would be `https://gitlab.com/login/oauth/callback`.
|
||||
1. In the **Redirect URI** field, enter the URI appropriate for your version of GitLab,
|
||||
replacing `<gitlab.example.com>` with your GitLab instance domain:
|
||||
- *For GitLab versions 11.3 and later,* use `https://<gitlab.example.com>/login/oauth/callback`.
|
||||
If you use GitLab.com, the URL is `https://gitlab.com/login/oauth/callback`.
|
||||
- *For GitLab versions 11.2 and earlier,* use
|
||||
`https://<gitlab.example.com>/-/jira/login/oauth/callback`.
|
||||
|
||||
NOTE:
|
||||
If using a GitLab version earlier than 11.3, the `Redirect URI` must be
|
||||
`https://<gitlab.example.com>/-/jira/login/oauth/callback`. If you want Jira
|
||||
to have access to all projects, GitLab recommends that an administrator create the
|
||||
application.
|
||||
1. For **Scopes**, select `api` and clear any other checkboxes.
|
||||
1. Select **Submit**.
|
||||
1. GitLab displays the generated **Application ID**
|
||||
and **Secret** values. Copy these values, as you need them to configure Jira.
|
||||
|
||||
![GitLab application setup](img/jira_dev_panel_gl_setup_1.png)
|
||||
## Configure Jira for DVCS
|
||||
|
||||
1. Check **API** in the **Scopes** section, and clear any other checkboxes.
|
||||
1. Click **Save application**. GitLab displays the generated **Application ID**
|
||||
and **Secret** values. Copy these values, which you use in Jira.
|
||||
If you use Jira Cloud and GitLab.com, use the [GitLab for Jira app](connect-app.md)
|
||||
unless you specifically need the DVCS Connector.
|
||||
|
||||
## Jira DVCS Connector setup
|
||||
|
||||
If you're using GitLab.com and Jira Cloud, use the
|
||||
[GitLab for Jira app](connect-app.md) unless you have a specific need for the DVCS Connector.
|
||||
|
||||
1. Ensure you have completed the [GitLab configuration](#gitlab-account-configuration-for-dvcs).
|
||||
1. If you're using Jira Server, go to **Settings (gear) > Applications > DVCS accounts**.
|
||||
If you're using Jira Cloud, go to **Settings (gear) > Products > DVCS accounts**.
|
||||
1. Click **Link GitHub Enterprise account** to start creating a new integration.
|
||||
(We're pretending to be GitHub in this integration, until there's additional platform support in Jira.)
|
||||
1. Complete the form:
|
||||
|
||||
1. Select **GitHub Enterprise** for the **Host** field.
|
||||
|
||||
1. In the **Team or User Account** field, enter either:
|
||||
Configure this connection when you want to import all GitLab commits and branches,
|
||||
for the groups you specify, into Jira. This import takes a few minutes and, after
|
||||
it completes, refreshes every 60 minutes:
|
||||
|
||||
1. Ensure you have completed the [GitLab configuration](#configure-a-gitlab-application-for-dvcs).
|
||||
1. Go to your DVCS account:
|
||||
- *For Jira Server,* go to **Settings (gear) > Applications > DVCS accounts**.
|
||||
- *For Jira Cloud,* go to **Settings (gear) > Products > DVCS accounts**.
|
||||
1. To create a new integration, select the appropriate value for **Host**:
|
||||
- *For Jira versions 8.14 and later:* Select **GitLab** or
|
||||
<!-- vale gitlab.Substitutions = NO -->
|
||||
**GitLab Self-Hosted**.
|
||||
<!-- vale gitlab.Substitutions = YES -->
|
||||
- *For Jira versions 8.13 and earlier:* Select **GitHub Enterprise**.
|
||||
1. For **Team or User Account**, enter either:
|
||||
- The relative path of a top-level GitLab group that you have access to.
|
||||
- The relative path of your personal namespace.
|
||||
|
||||
![Creation of Jira DVCS integration](img/jira_dev_panel_jira_setup_2.png)
|
||||
|
||||
1. In the **Host URL** field, enter `https://<gitlab.example.com>/`,
|
||||
replacing `<gitlab.example.com>` with your GitLab instance domain. For example, if you are using GitLab.com,
|
||||
this would be `https://gitlab.com/`.
|
||||
|
||||
NOTE:
|
||||
If using a GitLab version earlier than 11.3 the **Host URL** value should be `https://<gitlab.example.com>/-/jira`
|
||||
|
||||
1. For the **Client ID** field, use the **Application ID** value from the previous section.
|
||||
|
||||
1. For the **Client Secret** field, use the **Secret** value from the previous section.
|
||||
1. In the **Host URL** field, enter the URI appropriate for your version of GitLab,
|
||||
replacing `<gitlab.example.com>` with your GitLab instance domain:
|
||||
- *For GitLab versions 11.3 and later,* use `https://<gitlab.example.com>/`.
|
||||
- *For GitLab versions 11.2 and earlier,* use
|
||||
`https://<gitlab.example.com>/-/jira`.
|
||||
|
||||
1. For **Client ID**, use the **Application ID** value from the previous section.
|
||||
1. For **Client Secret**, use the **Secret** value from the previous section.
|
||||
1. Ensure that the rest of the checkboxes are checked.
|
||||
1. Select **Add** to complete and create the integration.
|
||||
|
||||
1. Click **Add** to complete and create the integration.
|
||||
To connect additional GitLab projects from other GitLab top-level groups, or
|
||||
personal namespaces, repeat the previous steps with additional Jira DVCS accounts.
|
||||
|
||||
Jira takes up to a few minutes to know about (import behind the scenes) all the commits and branches
|
||||
for all the projects in the GitLab group you specified in the previous step. These are refreshed
|
||||
every 60 minutes.
|
||||
After you configure the integration, read more about [how to test and use it](index.md#usage).
|
||||
|
||||
In the future, we plan on implementing real-time integration. If you need
|
||||
to refresh the data manually, you can do this from the `Applications -> DVCS
|
||||
accounts` screen where you initially set up the integration:
|
||||
## Refresh data imported to Jira
|
||||
|
||||
![Refresh GitLab information in Jira](img/jira_dev_panel_manual_refresh.png)
|
||||
Jira imports the commits and branches every 60 minutes for your projects. You
|
||||
can refresh the data manually from the Jira interface:
|
||||
|
||||
To connect additional GitLab projects from other GitLab top-level groups (or personal namespaces), repeat the previous
|
||||
steps with additional Jira DVCS accounts.
|
||||
|
||||
Now that the integration is configured, read more about how to test and use it in [Usage](index.md#usage).
|
||||
1. Sign in to your Jira instance as the user you configured the integration with.
|
||||
1. Go to **Settings (gear) > Applications**.
|
||||
1. Select **DVCS accounts**.
|
||||
1. In the table, for the repository you want to refresh, in the **Last Activity**
|
||||
column, select the icon:
|
||||
![Refresh GitLab information in Jira](img/jira_dev_panel_manual_refresh.png)
|
||||
|
||||
## Troubleshooting your DVCS connection
|
||||
|
||||
|
@ -100,39 +100,46 @@ Refer to the items in this section if you're having problems with your DVCS conn
|
|||
|
||||
### Jira cannot access GitLab server
|
||||
|
||||
If you complete the **Add New Account** form, authorize access, and you receive
|
||||
this error, Jira and GitLab cannot connect. No other error messages
|
||||
appear in any logs:
|
||||
|
||||
```plaintext
|
||||
Error obtaining access token. Cannot access https://gitlab.example.com from Jira.
|
||||
```
|
||||
|
||||
This error message is generated in Jira, after completing the **Add New Account**
|
||||
form and authorizing access. It indicates a connectivity issue from Jira to
|
||||
GitLab. No other error messages appear in any logs.
|
||||
### SSL and TLS problems
|
||||
|
||||
If there was an issue with SSL/TLS, this error message is generated.
|
||||
Problems with SSL and TLS can cause this error message:
|
||||
|
||||
- The [GitLab Jira integration](../../user/project/integrations/jira.md) requires GitLab to connect to Jira. Any
|
||||
TLS issues that arise from a private certificate authority or self-signed
|
||||
certificate [are resolved on the GitLab server](https://docs.gitlab.com/omnibus/settings/ssl.html#other-certificate-authorities),
|
||||
```plaintext
|
||||
Error obtaining access token. Cannot access https://gitlab.example.com from Jira.
|
||||
```
|
||||
|
||||
- The [GitLab Jira integration](../../user/project/integrations/jira.md) requires
|
||||
GitLab to connect to Jira. Any TLS issues that arise from a private certificate
|
||||
authority or self-signed certificate are resolved
|
||||
[on the GitLab server](https://docs.gitlab.com/omnibus/settings/ssl.html#other-certificate-authorities),
|
||||
as GitLab is the TLS client.
|
||||
- The Jira Development panel integration requires Jira to connect to GitLab, which
|
||||
causes Jira to be the TLS client. If your GitLab server's certificate is not
|
||||
issued by a public certificate authority, the Java Truststore on Jira's server
|
||||
needs to have the appropriate certificate added to it (such as your organization's
|
||||
root certificate).
|
||||
must have the appropriate certificate (such as your organization's
|
||||
root certificate) added to it .
|
||||
|
||||
Refer to Atlassian's documentation and Atlassian Support for assistance setting up Jira correctly:
|
||||
|
||||
- [Adding a certificate to the trust store](https://confluence.atlassian.com/kb/how-to-import-a-public-ssl-certificate-into-a-jvm-867025849.html).
|
||||
- Simplest approach is to use [`keytool`](https://docs.oracle.com/javase/8/docs/technotes/tools/unix/keytool.html).
|
||||
- [Add a certificate](https://confluence.atlassian.com/kb/how-to-import-a-public-ssl-certificate-into-a-jvm-867025849.html)
|
||||
to the trust store.
|
||||
- The simplest approach is [`keytool`](https://docs.oracle.com/javase/8/docs/technotes/tools/unix/keytool.html).
|
||||
- Add additional roots to Java's default Truststore (`cacerts`) to allow Jira to
|
||||
also trust public certificate authorities.
|
||||
- If the integration stops working after upgrading Jira's Java runtime, this
|
||||
might be because the `cacerts` Truststore got replaced.
|
||||
- If the integration stops working after upgrading Jira's Java runtime, the
|
||||
`cacerts` Truststore may have been replaced during the upgrade.
|
||||
|
||||
- [Troubleshooting connectivity up to and including TLS handshaking](https://confluence.atlassian.com/kb/unable-to-connect-to-ssl-services-due-to-pkix-path-building-failed-error-779355358.html),
|
||||
- Troubleshooting connectivity [up to and including TLS handshaking](https://confluence.atlassian.com/kb/unable-to-connect-to-ssl-services-due-to-pkix-path-building-failed-error-779355358.html),
|
||||
using the a java class called `SSLPoke`.
|
||||
|
||||
- Download the class from Atlassian's knowledge base to Jira's server, for example to `/tmp`.
|
||||
- Download the class from Atlassian's knowledge base to a directory on Jira's server, such as `/tmp`.
|
||||
- Use the same Java runtime as Jira.
|
||||
- Pass all networking-related parameters that Jira is called with, such as proxy
|
||||
settings or an alternative root Truststore (`-Djavax.net.ssl.trustStore`):
|
||||
|
@ -154,38 +161,42 @@ The requested scope is invalid, unknown, or malformed.
|
|||
|
||||
Potential resolutions:
|
||||
|
||||
- Verify the URL shown in the browser after being redirected from Jira in step 5 of [Jira DVCS Connector Setup](#jira-dvcs-connector-setup) includes `scope=api` in the query string.
|
||||
- If `scope=api` is missing from the URL, return to [GitLab account configuration](#gitlab-account-configuration-for-dvcs) and ensure the application you created in step 1 has the `api` box checked under scopes.
|
||||
1. Verify that the URL shown in the browser after being redirected from Jira in the
|
||||
[Jira DVCS connector setup](#configure-jira-for-dvcs) includes `scope=api` in
|
||||
the query string.
|
||||
1. If `scope=api` is missing from the URL, edit the
|
||||
[GitLab account configuration](#configure-a-gitlab-application-for-dvcs). Review
|
||||
the **Scopes** field and ensure the `api` check box is selected.
|
||||
|
||||
### Jira error adding account and no repositories listed
|
||||
|
||||
```plaintext
|
||||
Error!
|
||||
Failed adding the account: [Error retrieving list of repositories]
|
||||
```
|
||||
After you complete the **Add New Account** form in Jira and authorize access, you might
|
||||
encounter these issues:
|
||||
|
||||
This error message is generated in Jira after completing the **Add New Account**
|
||||
form in Jira and authorizing access. Attempting to click **Try Again** returns
|
||||
`Account is already integrated with JIRA.` The account is set up in the DVCS
|
||||
accounts view, but no repositories are listed.
|
||||
- An `Error! Failed adding the account: [Error retrieving list of repositories]` error.
|
||||
- An `Account is already integrated with JIRA` error when you click **Try Again**.
|
||||
- An account is visible in the DVCS accounts view, but no repositories are listed.
|
||||
|
||||
Potential resolutions:
|
||||
To resolve this issue:
|
||||
|
||||
- If you're using GitLab versions 11.10-12.7, upgrade to GitLab 12.8.10 or later
|
||||
to resolve an identified [issue](https://gitlab.com/gitlab-org/gitlab/-/issues/37012).
|
||||
- If you're using GitLab Free, be sure you're using GitLab 13.4 or later.
|
||||
- If you're using GitLab Free or GitLab Starter, be sure you're using
|
||||
GitLab 13.4 or later.
|
||||
- *If you're using GitLab versions 11.10-12.7,* upgrade to GitLab 12.8.10 or later
|
||||
to resolve [an identified issue](https://gitlab.com/gitlab-org/gitlab/-/issues/37012).
|
||||
|
||||
[Contact GitLab Support](https://about.gitlab.com/support/) if none of these reasons apply.
|
||||
|
||||
### Fixing synchronization issues
|
||||
### Fix synchronization issues
|
||||
|
||||
If Jira displays incorrect information (such as deleted branches), you may need to
|
||||
If Jira displays incorrect information, such as deleted branches, you may need to
|
||||
resynchronize the information. To do so:
|
||||
|
||||
1. In Jira, go to **Jira Administration > Applications > DVCS accounts**.
|
||||
1. At the account (group or subgroup) level, Jira displays an option to
|
||||
**Refresh repositories** in the `...` (ellipsis) menu.
|
||||
**Refresh repositories** in the **{ellipsis_h}** (ellipsis) menu.
|
||||
1. For each project, there's a sync button displayed next to the **last activity** date.
|
||||
To perform a *soft resync*, click the button, or complete a *full sync* by shift clicking
|
||||
the button. For more information, see
|
||||
[Atlassian's documentation](https://support.atlassian.com/jira-cloud-administration/docs/synchronize-jira-cloud-to-bitbucket/).
|
||||
- To perform a *soft resync*, click the button.
|
||||
- To complete a *full sync*, shift-click the button.
|
||||
|
||||
For more information, read
|
||||
[Atlassian's documentation](https://support.atlassian.com/jira-cloud-administration/docs/synchronize-jira-cloud-to-bitbucket/).
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 35 KiB |
Binary file not shown.
Before Width: | Height: | Size: 17 KiB |
|
@ -19,8 +19,8 @@ After pushing your changes to a new branch, you can:
|
|||
- [Discuss](../../../discussions/index.md) your implementation with your team
|
||||
- Preview changes submitted to a new branch with [Review Apps](../../../../ci/review_apps/index.md).
|
||||
|
||||
With [GitLab Starter](https://about.gitlab.com/pricing/), you can also request
|
||||
[approval](../../merge_requests/merge_request_approvals.md) from your managers.
|
||||
You can also request [approval](../../merge_requests/merge_request_approvals.md)
|
||||
from your managers.
|
||||
|
||||
For more information on managing branches using the GitLab UI, see:
|
||||
|
||||
|
|
57
lib/tasks/gitlab/docs/redirect.rake
Normal file
57
lib/tasks/gitlab/docs/redirect.rake
Normal file
|
@ -0,0 +1,57 @@
|
|||
# frozen_string_literal: true
|
||||
require 'date'
|
||||
require 'pathname'
|
||||
|
||||
# https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page
|
||||
namespace :gitlab do
|
||||
namespace :docs do
|
||||
desc 'GitLab | Docs | Create a doc redirect'
|
||||
task :redirect, [:old_path, :new_path] do |_, args|
|
||||
if args.old_path
|
||||
old_path = args.old_path
|
||||
else
|
||||
puts '=> Enter the path of the OLD file:'
|
||||
old_path = STDIN.gets.chomp
|
||||
end
|
||||
|
||||
if args.new_path
|
||||
new_path = args.new_path
|
||||
else
|
||||
puts '=> Enter the path of the NEW file:'
|
||||
new_path = STDIN.gets.chomp
|
||||
end
|
||||
|
||||
#
|
||||
# If the new path is a relative URL, find the relative path between
|
||||
# the old and new paths.
|
||||
# The returned path is one level deeper, so remove the leading '../'.
|
||||
#
|
||||
unless new_path.start_with?('http')
|
||||
old_pathname = Pathname.new(old_path)
|
||||
new_pathname = Pathname.new(new_path)
|
||||
relative_path = new_pathname.relative_path_from(old_pathname).to_s
|
||||
(_, *last) = relative_path.split('/')
|
||||
new_path = last.join('/')
|
||||
end
|
||||
|
||||
#
|
||||
# - If this is an external URL, move the date 1 year later.
|
||||
# - If this is a relative URL, move the date 3 months later.
|
||||
#
|
||||
date = Time.now.utc.strftime('%Y-%m-%d')
|
||||
date = new_path.start_with?('http') ? Date.parse(date) >> 12 : Date.parse(date) >> 3
|
||||
|
||||
puts "=> Creating new redirect from #{old_path} to #{new_path}"
|
||||
File.open(old_path, 'w') do |post|
|
||||
post.puts '---'
|
||||
post.puts "redirect_to: '#{new_path}'"
|
||||
post.puts '---'
|
||||
post.puts
|
||||
post.puts "This file was moved to [another location](#{new_path})."
|
||||
post.puts
|
||||
post.puts "<!-- This redirect file can be deleted after <#{date}>. -->"
|
||||
post.puts "<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,4 +1,4 @@
|
|||
import { GlButton } from '@gitlab/ui';
|
||||
import { GlButton, GlLink } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import ExperimentTracking from '~/experimentation/experiment_tracking';
|
||||
import InviteMembersTrigger from '~/invite_members/components/invite_members_trigger.vue';
|
||||
|
@ -8,22 +8,29 @@ jest.mock('~/experimentation/experiment_tracking');
|
|||
|
||||
const displayText = 'Invite team members';
|
||||
let wrapper;
|
||||
let triggerProps;
|
||||
let findButton;
|
||||
const triggerComponent = {
|
||||
button: GlButton,
|
||||
anchor: GlLink,
|
||||
};
|
||||
|
||||
const createComponent = (props = {}) => {
|
||||
wrapper = shallowMount(InviteMembersTrigger, {
|
||||
propsData: {
|
||||
displayText,
|
||||
...triggerProps,
|
||||
...props,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
describe('InviteMembersTrigger', () => {
|
||||
const findButton = () => wrapper.findComponent(GlButton);
|
||||
describe.each(['button', 'anchor'])('with triggerElement as %s', (triggerElement) => {
|
||||
triggerProps = { triggerElement };
|
||||
findButton = () => wrapper.findComponent(triggerComponent[triggerElement]);
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
wrapper = null;
|
||||
});
|
||||
|
||||
describe('displayText', () => {
|
||||
|
@ -74,5 +81,19 @@ describe('InviteMembersTrigger', () => {
|
|||
|
||||
expect(ExperimentTracking).not.toHaveBeenCalledWith('_track_experiment_');
|
||||
});
|
||||
|
||||
it('does not add tracking attributes', () => {
|
||||
createComponent();
|
||||
|
||||
expect(findButton().attributes('data-track-event')).toBeUndefined();
|
||||
expect(findButton().attributes('data-track-label')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('adds tracking attributes', () => {
|
||||
createComponent({ label: '_label_', event: '_event_' });
|
||||
|
||||
expect(findButton().attributes('data-track-event')).toBe('_event_');
|
||||
expect(findButton().attributes('data-track-label')).toBe('_label_');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,6 +7,8 @@ import PackageSearch from '~/packages/list/components/package_search.vue';
|
|||
import PackageListApp from '~/packages/list/components/packages_list_app.vue';
|
||||
import { DELETE_PACKAGE_SUCCESS_MESSAGE } from '~/packages/list/constants';
|
||||
import { SHOW_DELETE_SUCCESS_ALERT } from '~/packages/shared/constants';
|
||||
import { FILTERED_SEARCH_TERM } from '~/packages_and_registries/shared/constants';
|
||||
import * as packageUtils from '~/packages_and_registries/shared/utils';
|
||||
|
||||
jest.mock('~/lib/utils/common_utils');
|
||||
jest.mock('~/flash');
|
||||
|
@ -61,6 +63,7 @@ describe('packages_list_app', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
createStore();
|
||||
jest.spyOn(packageUtils, 'getQueryParams').mockReturnValue({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -72,25 +75,6 @@ describe('packages_list_app', () => {
|
|||
expect(wrapper.element).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe('empty state', () => {
|
||||
it('generate the correct empty list link', () => {
|
||||
mountComponent();
|
||||
|
||||
const link = findListComponent().find(GlLink);
|
||||
|
||||
expect(link.attributes('href')).toBe(emptyListHelpUrl);
|
||||
expect(link.text()).toBe('publish and share your packages');
|
||||
});
|
||||
|
||||
it('includes the right content on the default tab', () => {
|
||||
mountComponent();
|
||||
|
||||
const heading = findEmptyState().find('h1');
|
||||
|
||||
expect(heading.text()).toBe('There are no packages yet');
|
||||
});
|
||||
});
|
||||
|
||||
it('call requestPackagesList on page:changed', () => {
|
||||
mountComponent();
|
||||
store.dispatch.mockClear();
|
||||
|
@ -108,10 +92,75 @@ describe('packages_list_app', () => {
|
|||
expect(store.dispatch).toHaveBeenCalledWith('requestDeletePackage', 'foo');
|
||||
});
|
||||
|
||||
it('does not call requestPackagesList two times on render', () => {
|
||||
it('does call requestPackagesList only one time on render', () => {
|
||||
mountComponent();
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledTimes(1);
|
||||
expect(store.dispatch).toHaveBeenCalledTimes(3);
|
||||
expect(store.dispatch).toHaveBeenNthCalledWith(1, 'setSorting', expect.any(Object));
|
||||
expect(store.dispatch).toHaveBeenNthCalledWith(2, 'setFilter', expect.any(Array));
|
||||
expect(store.dispatch).toHaveBeenNthCalledWith(3, 'requestPackagesList');
|
||||
});
|
||||
|
||||
describe('url query string handling', () => {
|
||||
const defaultQueryParamsMock = {
|
||||
search: [1, 2],
|
||||
type: 'npm',
|
||||
sort: 'asc',
|
||||
orderBy: 'created',
|
||||
};
|
||||
|
||||
it('calls setSorting with the query string based sorting', () => {
|
||||
jest.spyOn(packageUtils, 'getQueryParams').mockReturnValue(defaultQueryParamsMock);
|
||||
|
||||
mountComponent();
|
||||
|
||||
expect(store.dispatch).toHaveBeenNthCalledWith(1, 'setSorting', {
|
||||
orderBy: defaultQueryParamsMock.orderBy,
|
||||
sort: defaultQueryParamsMock.sort,
|
||||
});
|
||||
});
|
||||
|
||||
it('calls setFilter with the query string based filters', () => {
|
||||
jest.spyOn(packageUtils, 'getQueryParams').mockReturnValue(defaultQueryParamsMock);
|
||||
|
||||
mountComponent();
|
||||
|
||||
expect(store.dispatch).toHaveBeenNthCalledWith(2, 'setFilter', [
|
||||
{ type: 'type', value: { data: defaultQueryParamsMock.type } },
|
||||
{ type: FILTERED_SEARCH_TERM, value: { data: defaultQueryParamsMock.search[0] } },
|
||||
{ type: FILTERED_SEARCH_TERM, value: { data: defaultQueryParamsMock.search[1] } },
|
||||
]);
|
||||
});
|
||||
|
||||
it('calls setSorting and setFilters with the results of extractFilterAndSorting', () => {
|
||||
jest
|
||||
.spyOn(packageUtils, 'extractFilterAndSorting')
|
||||
.mockReturnValue({ filters: ['foo'], sorting: { sort: 'desc' } });
|
||||
|
||||
mountComponent();
|
||||
|
||||
expect(store.dispatch).toHaveBeenNthCalledWith(1, 'setSorting', { sort: 'desc' });
|
||||
expect(store.dispatch).toHaveBeenNthCalledWith(2, 'setFilter', ['foo']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('empty state', () => {
|
||||
it('generate the correct empty list link', () => {
|
||||
mountComponent();
|
||||
|
||||
const link = findListComponent().find(GlLink);
|
||||
|
||||
expect(link.attributes('href')).toBe(emptyListHelpUrl);
|
||||
expect(link.text()).toBe('publish and share your packages');
|
||||
});
|
||||
|
||||
it('includes the right content on the default tab', () => {
|
||||
mountComponent();
|
||||
|
||||
const heading = findEmptyState().find('h1');
|
||||
|
||||
expect(heading.text()).toBe('There are no packages yet');
|
||||
});
|
||||
});
|
||||
|
||||
describe('filter without results', () => {
|
||||
|
|
|
@ -4,6 +4,7 @@ import component from '~/packages/list/components/package_search.vue';
|
|||
import PackageTypeToken from '~/packages/list/components/tokens/package_type_token.vue';
|
||||
import getTableHeaders from '~/packages/list/utils';
|
||||
import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue';
|
||||
import UrlSync from '~/vue_shared/components/url_sync.vue';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(Vuex);
|
||||
|
@ -12,7 +13,8 @@ describe('Package Search', () => {
|
|||
let wrapper;
|
||||
let store;
|
||||
|
||||
const findRegistrySearch = () => wrapper.find(RegistrySearch);
|
||||
const findRegistrySearch = () => wrapper.findComponent(RegistrySearch);
|
||||
const findUrlSync = () => wrapper.findComponent(UrlSync);
|
||||
|
||||
const createStore = (isGroupPage) => {
|
||||
const state = {
|
||||
|
@ -37,6 +39,9 @@ describe('Package Search', () => {
|
|||
wrapper = shallowMount(component, {
|
||||
localVue,
|
||||
store,
|
||||
stubs: {
|
||||
UrlSync,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -104,4 +109,20 @@ describe('Package Search', () => {
|
|||
|
||||
expect(wrapper.emitted('update')).toEqual([[]]);
|
||||
});
|
||||
|
||||
it('has a UrlSync component', () => {
|
||||
mountComponent();
|
||||
|
||||
expect(findUrlSync().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('on query:changed calls updateQuery from UrlSync', () => {
|
||||
jest.spyOn(UrlSync.methods, 'updateQuery').mockImplementation(() => {});
|
||||
|
||||
mountComponent();
|
||||
|
||||
findRegistrySearch().vm.$emit('query:changed');
|
||||
|
||||
expect(UrlSync.methods.updateQuery).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import { FILTERED_SEARCH_TERM } from '~/packages_and_registries/shared/constants';
|
||||
import {
|
||||
getQueryParams,
|
||||
keyValueToFilterToken,
|
||||
searchArrayToFilterTokens,
|
||||
extractFilterAndSorting,
|
||||
} from '~/packages_and_registries/shared/utils';
|
||||
|
||||
describe('Packages And Registries shared utils', () => {
|
||||
|
@ -27,9 +29,31 @@ describe('Packages And Registries shared utils', () => {
|
|||
const search = ['one', 'two'];
|
||||
|
||||
expect(searchArrayToFilterTokens(search)).toStrictEqual([
|
||||
{ type: 'filtered-search-term', value: { data: 'one' } },
|
||||
{ type: 'filtered-search-term', value: { data: 'two' } },
|
||||
{ type: FILTERED_SEARCH_TERM, value: { data: 'one' } },
|
||||
{ type: FILTERED_SEARCH_TERM, value: { data: 'two' } },
|
||||
]);
|
||||
});
|
||||
});
|
||||
describe('extractFilterAndSorting', () => {
|
||||
it.each`
|
||||
search | type | sort | orderBy | result
|
||||
${['one']} | ${'myType'} | ${'asc'} | ${'foo'} | ${{ sorting: { sort: 'asc', orderBy: 'foo' }, filters: [{ type: 'type', value: { data: 'myType' } }, { type: FILTERED_SEARCH_TERM, value: { data: 'one' } }] }}
|
||||
${['one']} | ${null} | ${'asc'} | ${'foo'} | ${{ sorting: { sort: 'asc', orderBy: 'foo' }, filters: [{ type: FILTERED_SEARCH_TERM, value: { data: 'one' } }] }}
|
||||
${[]} | ${null} | ${'asc'} | ${'foo'} | ${{ sorting: { sort: 'asc', orderBy: 'foo' }, filters: [] }}
|
||||
${null} | ${null} | ${'asc'} | ${'foo'} | ${{ sorting: { sort: 'asc', orderBy: 'foo' }, filters: [] }}
|
||||
${null} | ${null} | ${null} | ${'foo'} | ${{ sorting: { orderBy: 'foo' }, filters: [] }}
|
||||
${null} | ${null} | ${null} | ${null} | ${{ sorting: {}, filters: [] }}
|
||||
`(
|
||||
'returns sorting and filters objects in the correct form',
|
||||
({ search, type, sort, orderBy, result }) => {
|
||||
const queryObject = {
|
||||
search,
|
||||
type,
|
||||
sort,
|
||||
orderBy,
|
||||
};
|
||||
expect(extractFilterAndSorting(queryObject)).toStrictEqual(result);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { GlSkeletonLoader, GlSprintf, GlAlert } from '@gitlab/ui';
|
||||
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||
import { nextTick } from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
|
@ -61,7 +62,7 @@ describe('List Page', () => {
|
|||
const waitForApolloRequestRender = async () => {
|
||||
jest.runOnlyPendingTimers();
|
||||
await waitForPromises();
|
||||
await wrapper.vm.$nextTick();
|
||||
await nextTick();
|
||||
};
|
||||
|
||||
const mountComponent = ({
|
||||
|
@ -70,6 +71,7 @@ describe('List Page', () => {
|
|||
detailsResolver = jest.fn().mockResolvedValue(graphQLProjectImageRepositoriesDetailsMock),
|
||||
mutationResolver = jest.fn().mockResolvedValue(graphQLImageDeleteMock),
|
||||
config = { isGroupPage: false },
|
||||
query = {},
|
||||
} = {}) => {
|
||||
localVue.use(VueApollo);
|
||||
|
||||
|
@ -96,6 +98,7 @@ describe('List Page', () => {
|
|||
$toast,
|
||||
$route: {
|
||||
name: 'foo',
|
||||
query,
|
||||
},
|
||||
...mocks,
|
||||
},
|
||||
|
@ -159,9 +162,11 @@ describe('List Page', () => {
|
|||
});
|
||||
|
||||
describe('isLoading is true', () => {
|
||||
it('shows the skeleton loader', () => {
|
||||
it('shows the skeleton loader', async () => {
|
||||
mountComponent();
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(findSkeletonLoader().exists()).toBe(true);
|
||||
});
|
||||
|
||||
|
@ -177,9 +182,11 @@ describe('List Page', () => {
|
|||
expect(findCliCommands().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('title has the metadataLoading props set to true', () => {
|
||||
it('title has the metadataLoading props set to true', async () => {
|
||||
mountComponent();
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(findRegistryHeader().props('metadataLoading')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
@ -312,7 +319,7 @@ describe('List Page', () => {
|
|||
await selectImageForDeletion();
|
||||
|
||||
findDeleteImage().vm.$emit('success');
|
||||
await wrapper.vm.$nextTick();
|
||||
await nextTick();
|
||||
|
||||
const alert = findDeleteAlert();
|
||||
expect(alert.exists()).toBe(true);
|
||||
|
@ -328,7 +335,7 @@ describe('List Page', () => {
|
|||
await selectImageForDeletion();
|
||||
|
||||
findDeleteImage().vm.$emit('error');
|
||||
await wrapper.vm.$nextTick();
|
||||
await nextTick();
|
||||
|
||||
const alert = findDeleteAlert();
|
||||
expect(alert.exists()).toBe(true);
|
||||
|
@ -349,7 +356,7 @@ describe('List Page', () => {
|
|||
|
||||
findRegistrySearch().vm.$emit('filter:submit');
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
await nextTick();
|
||||
};
|
||||
|
||||
it('has a search box element', async () => {
|
||||
|
@ -374,7 +381,7 @@ describe('List Page', () => {
|
|||
await waitForApolloRequestRender();
|
||||
|
||||
findRegistrySearch().vm.$emit('sorting:changed', { sort: 'asc' });
|
||||
await wrapper.vm.$nextTick();
|
||||
await nextTick();
|
||||
|
||||
expect(resolver).toHaveBeenCalledWith(expect.objectContaining({ sort: 'UPDATED_DESC' }));
|
||||
});
|
||||
|
@ -417,7 +424,7 @@ describe('List Page', () => {
|
|||
await waitForApolloRequestRender();
|
||||
|
||||
findImageList().vm.$emit('prev-page');
|
||||
await wrapper.vm.$nextTick();
|
||||
await nextTick();
|
||||
|
||||
expect(resolver).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ before: pageInfo.startCursor }),
|
||||
|
@ -437,7 +444,7 @@ describe('List Page', () => {
|
|||
await waitForApolloRequestRender();
|
||||
|
||||
findImageList().vm.$emit('next-page');
|
||||
await wrapper.vm.$nextTick();
|
||||
await nextTick();
|
||||
|
||||
expect(resolver).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ after: pageInfo.endCursor }),
|
||||
|
@ -458,11 +465,10 @@ describe('List Page', () => {
|
|||
expect(findDeleteModal().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('contains a description with the path of the item to delete', () => {
|
||||
it('contains a description with the path of the item to delete', async () => {
|
||||
findImageList().vm.$emit('delete', { path: 'foo' });
|
||||
return wrapper.vm.$nextTick().then(() => {
|
||||
expect(findDeleteModal().html()).toContain('foo');
|
||||
});
|
||||
await nextTick();
|
||||
expect(findDeleteModal().html()).toContain('foo');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -498,4 +504,60 @@ describe('List Page', () => {
|
|||
testTrackingCall('confirm_delete');
|
||||
});
|
||||
});
|
||||
|
||||
describe('url query string handling', () => {
|
||||
const defaultQueryParams = {
|
||||
search: [1, 2],
|
||||
sort: 'asc',
|
||||
orderBy: 'CREATED',
|
||||
};
|
||||
const queryChangePayload = 'foo';
|
||||
|
||||
it('query:updated event pushes the new query to the router', async () => {
|
||||
const push = jest.fn();
|
||||
mountComponent({ mocks: { $router: { push } } });
|
||||
|
||||
await nextTick();
|
||||
|
||||
findRegistrySearch().vm.$emit('query:changed', queryChangePayload);
|
||||
|
||||
expect(push).toHaveBeenCalledWith({ query: queryChangePayload });
|
||||
});
|
||||
|
||||
it('graphql API call has the variables set from the URL', async () => {
|
||||
const resolver = jest.fn().mockResolvedValue(graphQLImageListMock);
|
||||
mountComponent({ query: defaultQueryParams, resolver });
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(resolver).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
name: 1,
|
||||
sort: 'CREATED_ASC',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it.each`
|
||||
sort | orderBy | search | payload
|
||||
${'ASC'} | ${undefined} | ${undefined} | ${{ sort: 'UPDATED_ASC' }}
|
||||
${undefined} | ${'bar'} | ${undefined} | ${{ sort: 'BAR_DESC' }}
|
||||
${'ASC'} | ${'bar'} | ${undefined} | ${{ sort: 'BAR_ASC' }}
|
||||
${undefined} | ${undefined} | ${undefined} | ${{}}
|
||||
${undefined} | ${undefined} | ${['one']} | ${{ name: 'one' }}
|
||||
${undefined} | ${undefined} | ${['one', 'two']} | ${{ name: 'one' }}
|
||||
${undefined} | ${'UPDATED'} | ${['one', 'two']} | ${{ name: 'one', sort: 'UPDATED_DESC' }}
|
||||
${'ASC'} | ${'UPDATED'} | ${['one', 'two']} | ${{ name: 'one', sort: 'UPDATED_ASC' }}
|
||||
`(
|
||||
'with sort equal to $sort, orderBy equal to $orderBy, search set to $search API call has the variables set as $payload',
|
||||
async ({ sort, orderBy, search, payload }) => {
|
||||
const resolver = jest.fn().mockResolvedValue({ sort, orderBy });
|
||||
mountComponent({ query: { sort, orderBy, search }, resolver });
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(resolver).toHaveBeenCalledWith(expect.objectContaining(payload));
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
RSpec.shared_examples 'issuable invite members experiments' do
|
||||
context 'when a privileged user can invite' do
|
||||
it 'shows a link for inviting members and follows through to the members page' do
|
||||
it 'shows a link for inviting members and launches invite modal' do
|
||||
project.add_maintainer(user)
|
||||
visit issuable_path
|
||||
|
||||
|
@ -11,14 +11,14 @@ RSpec.shared_examples 'issuable invite members experiments' do
|
|||
wait_for_requests
|
||||
|
||||
page.within '.dropdown-menu-user' do
|
||||
expect(page).to have_link('Invite Members', href: project_project_members_path(project))
|
||||
expect(page).to have_link('Invite Members')
|
||||
expect(page).to have_selector('[data-track-event="click_invite_members"]')
|
||||
expect(page).to have_selector('[data-track-label="edit_assignee"]')
|
||||
end
|
||||
|
||||
click_link 'Invite Members'
|
||||
|
||||
expect(current_path).to eq project_project_members_path(project)
|
||||
expect(page).to have_content("You're inviting members to the")
|
||||
end
|
||||
end
|
||||
|
||||
|
|
0
vendor/gitignore/C++.gitignore
vendored
Executable file → Normal file
0
vendor/gitignore/C++.gitignore
vendored
Executable file → Normal file
0
vendor/gitignore/Java.gitignore
vendored
Executable file → Normal file
0
vendor/gitignore/Java.gitignore
vendored
Executable file → Normal file
Loading…
Reference in a new issue