Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
05b83be3ee
commit
1572e2a376
|
@ -1,11 +1,11 @@
|
|||
<!-- Title suggestion: [Feature flag] Enable description of feature -->
|
||||
|
||||
## Feature
|
||||
## Summary
|
||||
|
||||
This feature uses the `:feature_name` feature flag!
|
||||
This issue is to rollout [the feature](ISSUE LINK) on production,
|
||||
that is currently behind the `<feature-flag-name>` feature flag.
|
||||
|
||||
<!-- Short description of what the feature is about and link to relevant other issues. -->
|
||||
- [Issue Name](ISSUE LINK)
|
||||
|
||||
## Owners
|
||||
|
||||
|
@ -26,14 +26,15 @@ Are there any other stages or teams involved that need to be kept in the loop?
|
|||
|
||||
## The Rollout Plan
|
||||
|
||||
- Partial Rollout on GitLab.com with beta groups
|
||||
- Partial Rollout on GitLab.com with testing groups
|
||||
- Rollout on GitLab.com for a certain period (How long)
|
||||
- Percentage Rollout on GitLab.com
|
||||
- Rollout Feature for everyone as soon as it's ready
|
||||
|
||||
<!-- Which dashboards from https://dashboards.gitlab.net are most relevant? Sentry errors reports can also be useful to review -->
|
||||
|
||||
**Beta Groups/Projects:**
|
||||
## Testing Groups/Projects/Users
|
||||
|
||||
<!-- If applicable, any groups/projects that are happy to have this feature turned on early. Some organizations may wish to test big changes they are interested in with a small subset of users ahead of time for example. -->
|
||||
|
||||
- `gitlab-org/gitlab` project
|
||||
|
@ -55,54 +56,97 @@ Are there any other stages or teams involved that need to be kept in the loop?
|
|||
|
||||
<!-- Which dashboards from https://dashboards.gitlab.net are most relevant? -->
|
||||
|
||||
## Rollout Timeline
|
||||
## Rollout Steps
|
||||
|
||||
<!-- Please check which steps are needed and remove those which don't apply -->
|
||||
### Rollout on non-production environments
|
||||
|
||||
**Rollout Steps**
|
||||
- [ ] Ensure that the feature MRs have been deployed to non-production environments.
|
||||
- [ ] `/chatops run auto_deploy status <merge-commit-of-your-feature>`
|
||||
- [ ] Enable the feature globally on non-production environments.
|
||||
- [ ] `/chatops run feature set <feature-flag-name> true --dev`
|
||||
- [ ] `/chatops run feature set <feature-flag-name> true --staging`
|
||||
- [ ] Verify that the feature works as expected. Posting the QA result in this issue is preferable.
|
||||
|
||||
*Preparation Phase*
|
||||
- [ ] Enable on staging (`/chatops run feature set feature_name true --staging`)
|
||||
|
||||
- [ ] Test on staging
|
||||
|
||||
- [ ] Ensure that documentation has been updated ([More info](https://docs.gitlab.com/ee/development/documentation/feature_flags.html#features-that-became-enabled-by-default))
|
||||
|
||||
- [ ] Announce on the issue an estimated time this will be enabled on GitLab.com
|
||||
### Preparation before production rollout
|
||||
|
||||
- [ ] Ensure that the feature MRs have been deployed to both production and canary.
|
||||
- [ ] `/chatops run auto_deploy status <merge-commit-of-your-feature>`
|
||||
- [ ] Check if the feature flag change needs to be accompanied with a
|
||||
[change management
|
||||
issue](https://about.gitlab.com/handbook/engineering/infrastructure/change-management/#feature-flags-and-the-change-management-process). Cross
|
||||
link the issue here if it does.
|
||||
[change management issue](https://about.gitlab.com/handbook/engineering/infrastructure/change-management/#feature-flags-and-the-change-management-process).
|
||||
Cross link the issue here if it does.
|
||||
- [ ] Ensure that you or a representative in development can be available for at least 2 hours after feature flag updates in production.
|
||||
If a different developer will be covering, or an exception is needed, please inform the oncall SRE by using the `@sre-oncall` Slack alias.
|
||||
- [ ] Ensure that documentation has been updated ([More info](https://docs.gitlab.com/ee/development/documentation/feature_flags.html#features-that-became-enabled-by-default)).
|
||||
- [ ] Announce on [the feature issue](ISSUE LINK) an estimated time this will be enabled on GitLab.com.
|
||||
- [ ] If the feature flag in code has [an actor](https://docs.gitlab.com/ee/development/feature_flags/#feature-actors), enable it on GitLab.com for [testing groups/projects](#testing-groupsprojectsusers).
|
||||
- [ ] `/chatops run feature set --<actor-type>=<actor> <feature-flag-name> true`
|
||||
- [ ] Verify that the feature works as expected. Posting the QA result in this issue is preferable.
|
||||
|
||||
- [ ] Ensure that you or a representative in development can be available for at least 2 hours after feature flag updates in production. If a different developer will be covering, or an exception is needed, please inform the oncall SRE by using the `@sre-oncall` Slack alias.
|
||||
### Global rollout on production
|
||||
|
||||
*Partial Rollout Phase*
|
||||
- [ ] [Incrementally roll out](https://docs.gitlab.com/ee/development/feature_flags/controls.html#process) the feature. If there is no risk that the feature affects usability or server loads, skip to the global rollout.
|
||||
- If the feature flag in code has [an actor](https://docs.gitlab.com/ee/development/feature_flags/#feature-actors), perform **actor-based** rollout.
|
||||
- [ ] `/chatops run feature set <feature-flag-name> <rollout-percentage> --actors`
|
||||
- If the feature flag in code does **NOT** have [an actor](https://docs.gitlab.com/ee/development/feature_flags/#feature-actors), perform time-based rollout (**random** rollout).
|
||||
- [ ] `/chatops run feature set <feature-flag-name> <rollout-percentage>`
|
||||
- [ ] Enable the feature globally on production environment.
|
||||
- [ ] `/chatops run feature set <feature-flag-name> true`
|
||||
- [ ] Announce on [the feature issue](ISSUE LINK) that the feature has been globally enabled.
|
||||
- [ ] Cross-post chatops slack command to `#support_gitlab-com`.
|
||||
([more guidance when this is necessary in the dev docs](https://docs.gitlab.com/ee/development/feature_flags/controls.html#communicate-the-change)) and in your team channel
|
||||
- [ ] Wait for [at least one day for the verification term](https://about.gitlab.com/handbook/product-development-flow/feature-flag-lifecycle/#including-a-feature-behind-feature-flag-in-the-final-release).
|
||||
|
||||
- [ ] Enable on GitLab.com for individual groups/projects listed above and verify behaviour (`/chatops run feature set --project=gitlab-org/gitlab feature_name true`)
|
||||
### (Optional) Release the feature with the feature flag
|
||||
|
||||
- [ ] Verify behaviour (See Beta Groups) and add details with screenshots as a comment on this issue
|
||||
If you're still unsure whether the feature is [deemed stable](https://about.gitlab.com/handbook/product-development-flow/feature-flag-lifecycle/#including-a-feature-behind-feature-flag-in-the-final-release)
|
||||
but want to release it in the current milestone, you can change the default state of the feature flag to be enabled.
|
||||
To do so, follow these steps:
|
||||
|
||||
- [ ] If it is possible to perform an incremental rollout, this should be preferred. Proposed increments are: `10%`, `50%`, `100%`. Proposed minimum time between increments is 15 minutes.
|
||||
- When setting percentages, make sure that the feature works correctly between feature checks. See https://gitlab.com/gitlab-org/gitlab/-/issues/327117 for more information
|
||||
- For actor-based rollout: `/chatops run feature set feature_name 10 --actors`
|
||||
- For time-based rollout: `/chatops run feature set feature_name 10`
|
||||
- [ ] Create a merge request with the following changes. Ask for review and merge it.
|
||||
- [ ] Set the `default_enabled` attribute in [the feature flag definition](https://docs.gitlab.com/ee/development/feature_flags/#feature-flag-definition-and-validation) to `true`.
|
||||
- [ ] Create [a changelog entry](https://docs.gitlab.com/ee/development/feature_flags/#changelog).
|
||||
- [ ] Ensure that the above MR has been deployed to both production and canary.
|
||||
If the merge request was deployed before [the code cutoff](https://about.gitlab.com/handbook/engineering/releases/#self-managed-releases-1),
|
||||
the feature can be officially announced in a release blog post.
|
||||
- [ ] `/chatops run auto_deploy status <merge-commit>`
|
||||
- [ ] Close [the feature issue](ISSUE LINK) to indicate the feature has been released.
|
||||
|
||||
*Full Rollout Phase*
|
||||
- [ ] Make the feature flag enabled by default i.e. Change `default_enabled` to `true`
|
||||
**WARNING:** This approach has the downside that it makes it difficult for us to
|
||||
[clean up](https://docs.gitlab.com/ee/development/feature_flags/controls.html#cleaning-up) the flag.
|
||||
For example, on-premise users could disable the feature on their GitLab instance. But when you
|
||||
remove the flag at some point, they suddenly see the feature as enabled and they can't roll it back
|
||||
to the previous behavior. To avoid this potential breaking change, use this approach only for urgent
|
||||
matters.
|
||||
|
||||
- [ ] Cross post chatops slack command to `#support_gitlab-com` ([more guidance when this is necessary in the dev docs](https://docs.gitlab.com/ee/development/feature_flags/controls.html#where-to-run-commands)) and in your team channel
|
||||
### Release the feature
|
||||
|
||||
- [ ] Announce on the issue that the flag has been enabled
|
||||
After the feature has been [deemed stable](https://about.gitlab.com/handbook/product-development-flow/feature-flag-lifecycle/#including-a-feature-behind-feature-flag-in-the-final-release),
|
||||
the [clean up](https://docs.gitlab.com/ee/development/feature_flags/controls.html#cleaning-up)
|
||||
should be done as soon as possible to permanently enable the feature and reduce complexity in the
|
||||
codebase.
|
||||
|
||||
- [ ] Create a cleanup issue using the "Feature Flag Removal" template
|
||||
<!-- The checklist here is to help stakeholders keep track of the feature flag status -->
|
||||
- [ ] Create a merge request to remove `<feature-flag-name>` feature flag. Ask for review and merge it.
|
||||
- [ ] Remove all references to the feature flag from the codebase.
|
||||
- [ ] Remove the YAML definitions for the feature from the repository.
|
||||
- [ ] Create [a changelog entry](https://docs.gitlab.com/ee/development/feature_flags/#changelog).
|
||||
- [ ] Ensure that the above MR has been deployed to both production and canary.
|
||||
If the merge request was deployed before [the code cutoff](https://about.gitlab.com/handbook/engineering/releases/#self-managed-releases-1),
|
||||
the feature can be officially announced in a release blog post.
|
||||
- [ ] `/chatops run auto_deploy status <merge-commit>`
|
||||
- [ ] Close [the feature issue](ISSUE LINK) to indicate the feature has been released.
|
||||
- [ ] Clean up the feature flag from all environments by running these chatops command in `#production` channel:
|
||||
- [ ] `/chatops run feature delete <feature-flag-name> --dev`
|
||||
- [ ] `/chatops run feature delete <feature-flag-name> --staging`
|
||||
- [ ] `/chatops run feature delete <feature-flag-name>`
|
||||
- [ ] Close this rollout issue.
|
||||
|
||||
## Rollback Steps
|
||||
|
||||
- [ ] This feature can be disabled by running the following Chatops command:
|
||||
|
||||
```
|
||||
/chatops run feature set --project=gitlab-org/gitlab feature_name false
|
||||
/chatops run feature set <feature-flag-name> false
|
||||
```
|
||||
|
||||
/label ~"feature flag"
|
||||
|
|
2
Gemfile
2
Gemfile
|
@ -300,7 +300,7 @@ gem 'gon', '~> 6.4.0'
|
|||
gem 'request_store', '~> 1.5'
|
||||
gem 'base32', '~> 0.3.0'
|
||||
|
||||
gem "gitlab-license", "~> 1.4"
|
||||
gem 'gitlab-license', '~> 1.5'
|
||||
|
||||
# Protect against bruteforcing
|
||||
gem 'rack-attack', '~> 6.3.0'
|
||||
|
|
|
@ -475,7 +475,7 @@ GEM
|
|||
opentracing (~> 0.4)
|
||||
pg_query (~> 1.3)
|
||||
redis (> 3.0.0, < 5.0.0)
|
||||
gitlab-license (1.4.0)
|
||||
gitlab-license (1.5.0)
|
||||
gitlab-mail_room (0.0.9)
|
||||
gitlab-markup (1.7.1)
|
||||
gitlab-net-dns (0.9.1)
|
||||
|
@ -1455,7 +1455,7 @@ DEPENDENCIES
|
|||
gitlab-fog-azure-rm (~> 1.0.1)
|
||||
gitlab-fog-google (~> 1.13)
|
||||
gitlab-labkit (~> 0.16.2)
|
||||
gitlab-license (~> 1.4)
|
||||
gitlab-license (~> 1.5)
|
||||
gitlab-mail_room (~> 0.0.9)
|
||||
gitlab-markup (~> 1.7.1)
|
||||
gitlab-net-dns (~> 0.9.1)
|
||||
|
|
|
@ -375,7 +375,7 @@ export const MR_PREVIOUS_FILE_IN_DIFF = {
|
|||
export const MR_GO_TO_FILE = {
|
||||
id: 'mergeRequests.goToFile',
|
||||
description: __('Go to file'),
|
||||
defaultKeys: ['t', 'mod+p'],
|
||||
defaultKeys: ['mod+p', 't'],
|
||||
customizable: false,
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
<script>
|
||||
import { __, s__ } from '~/locale';
|
||||
|
||||
// Map some keys to their proper representation depending on the system
|
||||
// See also: https://craig.is/killing/mice#keys
|
||||
const getKeyMap = () => {
|
||||
const keyMap = {
|
||||
up: '↑',
|
||||
down: '↓',
|
||||
left: '←',
|
||||
right: '→',
|
||||
ctrl: s__('KeyboardKey|Ctrl'),
|
||||
shift: s__('KeyboardKey|Shift'),
|
||||
enter: s__('KeyboardKey|Enter'),
|
||||
esc: s__('KeyboardKey|Esc'),
|
||||
command: '⌘',
|
||||
option: window.gl?.client?.isMac ? '⌥' : s__('KeyboardKey|Alt'),
|
||||
};
|
||||
|
||||
// Meta and alt are aliases
|
||||
keyMap.meta = keyMap.command;
|
||||
keyMap.alt = keyMap.option;
|
||||
|
||||
// Mod is Command on Mac, and Ctrl on Windows/Linux
|
||||
keyMap.mod = window.gl?.client?.isMac ? keyMap.command : keyMap.ctrl;
|
||||
|
||||
return keyMap;
|
||||
};
|
||||
|
||||
export default {
|
||||
functional: true,
|
||||
props: {
|
||||
shortcuts: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
render(createElement, context) {
|
||||
const keyMap = getKeyMap();
|
||||
|
||||
const { staticClass } = context.data;
|
||||
|
||||
const shortcuts = context.props.shortcuts.reduce((acc, shortcut, i) => {
|
||||
if (
|
||||
!window.gl?.client?.isMac &&
|
||||
(shortcut.includes('command') || shortcut.includes('meta'))
|
||||
) {
|
||||
return acc;
|
||||
}
|
||||
const keys = shortcut.split(/([ +])/);
|
||||
|
||||
if (i !== 0 && acc.length) {
|
||||
acc.push(` ${__('or')} `);
|
||||
// If there are multiple alternative shortcuts,
|
||||
// we keep them on the same line if they are single-key, e.g. `]` or `j`
|
||||
// but if they consist of multiple keys, we insert a line break, e.g.:
|
||||
// `shift` + `]` <br> or `shift` + `j`
|
||||
if (keys.length > 1) {
|
||||
acc.push(createElement('br'));
|
||||
}
|
||||
}
|
||||
|
||||
keys.forEach((key) => {
|
||||
if (key === '+') {
|
||||
acc.push(' + ');
|
||||
} else if (key === ' ') {
|
||||
acc.push(` ${__('then')} `);
|
||||
} else {
|
||||
acc.push(createElement('kbd', {}, [keyMap[key] ?? key]));
|
||||
}
|
||||
});
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
return createElement('div', { staticClass }, shortcuts);
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -1,525 +1,99 @@
|
|||
<script>
|
||||
/* eslint-disable @gitlab/vue-require-i18n-strings */
|
||||
import { GlIcon, GlModal } from '@gitlab/ui';
|
||||
import { GlModal, GlSearchBoxByType } from '@gitlab/ui';
|
||||
import { s__, __ } from '~/locale';
|
||||
import { keybindingGroups } from './keybindings';
|
||||
import Shortcut from './shortcut.vue';
|
||||
import ShortcutsToggle from './shortcuts_toggle.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlIcon,
|
||||
GlModal,
|
||||
GlSearchBoxByType,
|
||||
ShortcutsToggle,
|
||||
Shortcut,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
searchTerm: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
ctrlCharacter() {
|
||||
return window.gl.client.isMac ? '⌘' : 'ctrl';
|
||||
},
|
||||
onDotCom() {
|
||||
return window.gon.dot_com;
|
||||
filteredKeybindings() {
|
||||
if (!this.searchTerm) {
|
||||
return keybindingGroups;
|
||||
}
|
||||
|
||||
const search = this.searchTerm.toLocaleLowerCase();
|
||||
|
||||
const mapped = keybindingGroups.map((group) => {
|
||||
if (group.name.toLocaleLowerCase().includes(search)) {
|
||||
return group;
|
||||
}
|
||||
return {
|
||||
...group,
|
||||
keybindings: group.keybindings.filter((binding) =>
|
||||
binding.description.toLocaleLowerCase().includes(search),
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
return mapped.filter((group) => group.keybindings.length);
|
||||
},
|
||||
},
|
||||
i18n: {
|
||||
title: __(`Keyboard shortcuts`),
|
||||
search: s__(`KeyboardShortcuts|Search keyboard shortcuts`),
|
||||
noMatch: s__(`KeyboardShortcuts|No shortcuts matched your search`),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<gl-modal
|
||||
modal-id="keyboard-shortcut-modal"
|
||||
size="lg"
|
||||
:title="$options.i18n.title"
|
||||
data-testid="modal-shortcuts"
|
||||
body-class="shortcut-help-body gl-p-0!"
|
||||
:visible="true"
|
||||
:hide-footer="true"
|
||||
@hidden="$emit('hidden')"
|
||||
>
|
||||
<template #modal-title>
|
||||
<shortcuts-toggle />
|
||||
</template>
|
||||
<div class="row">
|
||||
<div class="col-lg-4">
|
||||
<table class="shortcut-mappings text-2">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>{{ __('Global Shortcuts') }}</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="shortcut">
|
||||
<kbd>?</kbd>
|
||||
</td>
|
||||
<td>{{ __('Toggle this dialog') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="shortcut">
|
||||
<kbd>shift p</kbd>
|
||||
</td>
|
||||
<td>{{ __('Go to your projects') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="shortcut">
|
||||
<kbd>shift g</kbd>
|
||||
</td>
|
||||
<td>{{ __('Go to your groups') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="shortcut">
|
||||
<kbd>shift a</kbd>
|
||||
</td>
|
||||
<td>{{ __('Go to the activity feed') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="shortcut">
|
||||
<kbd>shift l</kbd>
|
||||
</td>
|
||||
<td>{{ __('Go to the milestone list') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="shortcut">
|
||||
<kbd>shift s</kbd>
|
||||
</td>
|
||||
<td>{{ __('Go to your snippets') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="shortcut">
|
||||
<kbd>s</kbd>
|
||||
/
|
||||
<kbd>/</kbd>
|
||||
</td>
|
||||
<td>{{ __('Start search') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="shortcut">
|
||||
<kbd>shift i</kbd>
|
||||
</td>
|
||||
<td>{{ __('Go to your issues') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="shortcut">
|
||||
<kbd>shift m</kbd>
|
||||
</td>
|
||||
<td>{{ __('Go to your merge requests') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="shortcut">
|
||||
<kbd>shift t</kbd>
|
||||
</td>
|
||||
<td>{{ __('Go to your To-Do list') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="shortcut">
|
||||
<kbd>p</kbd>
|
||||
<kbd>b</kbd>
|
||||
</td>
|
||||
<td>{{ __('Toggle the Performance Bar') }}</td>
|
||||
</tr>
|
||||
<tr v-if="onDotCom">
|
||||
<td class="shortcut">
|
||||
<kbd>g</kbd>
|
||||
<kbd>x</kbd>
|
||||
</td>
|
||||
<td>{{ __('Toggle GitLab Next') }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>{{ __('Editing') }}</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="shortcut">
|
||||
<kbd>{{ ctrlCharacter }} shift p</kbd>
|
||||
</td>
|
||||
<td>{{ __('Toggle Markdown preview') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="shortcut">
|
||||
<kbd>
|
||||
<gl-icon name="arrow-up" />
|
||||
</kbd>
|
||||
</td>
|
||||
<td>
|
||||
{{ __('Edit your most recent comment in a thread (from an empty textarea)') }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>{{ __('Wiki') }}</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="shortcut">
|
||||
<kbd>e</kbd>
|
||||
</td>
|
||||
<td>{{ __('Edit wiki page') }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>{{ __('Repository Graph') }}</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="shortcut">
|
||||
<kbd>
|
||||
<gl-icon name="arrow-left" />
|
||||
</kbd>
|
||||
/
|
||||
<kbd>h</kbd>
|
||||
</td>
|
||||
<td>{{ __('Scroll left') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="shortcut">
|
||||
<kbd>
|
||||
<gl-icon name="arrow-right" />
|
||||
</kbd>
|
||||
/
|
||||
<kbd>l</kbd>
|
||||
</td>
|
||||
<td>{{ __('Scroll right') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="shortcut">
|
||||
<kbd>
|
||||
<gl-icon name="arrow-up" />
|
||||
</kbd>
|
||||
/
|
||||
<kbd>k</kbd>
|
||||
</td>
|
||||
<td>{{ __('Scroll up') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="shortcut">
|
||||
<kbd>
|
||||
<gl-icon name="arrow-down" />
|
||||
</kbd>
|
||||
/
|
||||
<kbd>j</kbd>
|
||||
</td>
|
||||
<td>{{ __('Scroll down') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="shortcut">
|
||||
<kbd>
|
||||
shift
|
||||
<gl-icon name="arrow-up" />
|
||||
/ k
|
||||
</kbd>
|
||||
</td>
|
||||
<td>{{ __('Scroll to top') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="shortcut">
|
||||
<kbd>
|
||||
shift
|
||||
<gl-icon name="arrow-down" />
|
||||
/ j
|
||||
</kbd>
|
||||
</td>
|
||||
<td>{{ __('Scroll to bottom') }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<table class="shortcut-mappings text-2">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>{{ __('Project') }}</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="shortcut">
|
||||
<kbd>g</kbd>
|
||||
<kbd>p</kbd>
|
||||
</td>
|
||||
<td>{{ __("Go to the project's overview page") }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="shortcut">
|
||||
<kbd>g</kbd>
|
||||
<kbd>v</kbd>
|
||||
</td>
|
||||
<td>{{ __("Go to the project's activity feed") }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="shortcut">
|
||||
<kbd>g</kbd>
|
||||
<kbd>r</kbd>
|
||||
</td>
|
||||
<td>{{ __('Go to releases') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="shortcut">
|
||||
<kbd>g</kbd>
|
||||
<kbd>f</kbd>
|
||||
</td>
|
||||
<td>{{ __('Go to files') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="shortcut">
|
||||
<kbd>t</kbd>
|
||||
</td>
|
||||
<td>{{ __('Go to find file') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="shortcut">
|
||||
<kbd>g</kbd>
|
||||
<kbd>c</kbd>
|
||||
</td>
|
||||
<td>{{ __('Go to commits') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="shortcut">
|
||||
<kbd>g</kbd>
|
||||
<kbd>n</kbd>
|
||||
</td>
|
||||
<td>{{ __('Go to repository graph') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="shortcut">
|
||||
<kbd>g</kbd>
|
||||
<kbd>d</kbd>
|
||||
</td>
|
||||
<td>{{ __('Go to repository charts') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="shortcut">
|
||||
<kbd>g</kbd>
|
||||
<kbd>i</kbd>
|
||||
</td>
|
||||
<td>{{ __('Go to issues') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="shortcut">
|
||||
<kbd>i</kbd>
|
||||
</td>
|
||||
<td>{{ __('New issue') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="shortcut">
|
||||
<kbd>g</kbd>
|
||||
<kbd>b</kbd>
|
||||
</td>
|
||||
<td>{{ __('Go to issue boards') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="shortcut">
|
||||
<kbd>g</kbd>
|
||||
<kbd>m</kbd>
|
||||
</td>
|
||||
<td>{{ __('Go to merge requests') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="shortcut">
|
||||
<kbd>g</kbd>
|
||||
<kbd>j</kbd>
|
||||
</td>
|
||||
<td>{{ __('Go to jobs') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="shortcut">
|
||||
<kbd>g</kbd>
|
||||
<kbd>l</kbd>
|
||||
</td>
|
||||
<td>{{ __('Go to metrics') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="shortcut">
|
||||
<kbd>g</kbd>
|
||||
<kbd>e</kbd>
|
||||
</td>
|
||||
<td>{{ __('Go to environments') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="shortcut">
|
||||
<kbd>g</kbd>
|
||||
<kbd>k</kbd>
|
||||
</td>
|
||||
<td>{{ __('Go to kubernetes') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="shortcut">
|
||||
<kbd>g</kbd>
|
||||
<kbd>s</kbd>
|
||||
</td>
|
||||
<td>{{ __('Go to snippets') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="shortcut">
|
||||
<kbd>g</kbd>
|
||||
<kbd>w</kbd>
|
||||
</td>
|
||||
<td>{{ __('Go to wiki') }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>{{ __('Project Files') }}</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="shortcut">
|
||||
<kbd>
|
||||
<gl-icon name="arrow-up" />
|
||||
</kbd>
|
||||
</td>
|
||||
<td>{{ __('Move selection up') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="shortcut">
|
||||
<kbd>
|
||||
<gl-icon name="arrow-down" />
|
||||
</kbd>
|
||||
</td>
|
||||
<td>{{ __('Move selection down') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="shortcut">
|
||||
<kbd>enter</kbd>
|
||||
</td>
|
||||
<td>{{ __('Open Selection') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="shortcut">
|
||||
<kbd>esc</kbd>
|
||||
</td>
|
||||
<td>{{ __('Go back (while searching for files)') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="shortcut">
|
||||
<kbd>y</kbd>
|
||||
</td>
|
||||
<td>{{ __('Go to file permalink (while viewing a file)') }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<table class="shortcut-mappings text-2">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>{{ __('Epics, issues, and merge requests') }}</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="shortcut">
|
||||
<kbd>r</kbd>
|
||||
</td>
|
||||
<td>{{ __('Comment/Reply (quoting selected text)') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="shortcut">
|
||||
<kbd>e</kbd>
|
||||
</td>
|
||||
<td>{{ __('Edit description') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="shortcut">
|
||||
<kbd>l</kbd>
|
||||
</td>
|
||||
<td>{{ __('Change label') }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>{{ __('Issues and merge requests') }}</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="shortcut">
|
||||
<kbd>a</kbd>
|
||||
</td>
|
||||
<td>{{ __('Change assignee') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="shortcut">
|
||||
<kbd>m</kbd>
|
||||
</td>
|
||||
<td>{{ __('Change milestone') }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>{{ __('Merge requests') }}</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="shortcut">
|
||||
<kbd>]</kbd>
|
||||
/
|
||||
<kbd>j</kbd>
|
||||
</td>
|
||||
<td>{{ __('Next file in diff') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="shortcut">
|
||||
<kbd>[</kbd>
|
||||
/
|
||||
<kbd>k</kbd>
|
||||
</td>
|
||||
<td>{{ __('Previous file in diff') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="shortcut">
|
||||
<kbd>{{ ctrlCharacter }} p</kbd>
|
||||
</td>
|
||||
<td>{{ __('Go to file') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="shortcut">
|
||||
<kbd>n</kbd>
|
||||
</td>
|
||||
<td>{{ __('Next unresolved discussion') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="shortcut">
|
||||
<kbd>p</kbd>
|
||||
</td>
|
||||
<td>{{ __('Previous unresolved discussion') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="shortcut">
|
||||
<kbd>b</kbd>
|
||||
</td>
|
||||
<td>{{ __('Copy source branch name') }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>{{ __('Merge request commits') }}</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="shortcut">
|
||||
<kbd>c</kbd>
|
||||
</td>
|
||||
<td>{{ __('Next commit') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="shortcut">
|
||||
<kbd>x</kbd>
|
||||
</td>
|
||||
<td>{{ __('Previous commit') }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>{{ __('Web IDE') }}</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="shortcut">
|
||||
<kbd>{{ ctrlCharacter }} p</kbd>
|
||||
</td>
|
||||
<td>{{ __('Go to file') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="shortcut">
|
||||
<kbd>{{ ctrlCharacter }} enter</kbd>
|
||||
</td>
|
||||
<td>{{ __('Commit (when editing commit message)') }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div
|
||||
class="gl-sticky gl-top-0 gl-py-5 gl-px-5 gl-display-flex gl-align-items-center gl-bg-white"
|
||||
>
|
||||
<gl-search-box-by-type
|
||||
v-model.trim="searchTerm"
|
||||
:aria-label="$options.i18n.search"
|
||||
class="gl-w-half gl-mr-3"
|
||||
/>
|
||||
<shortcuts-toggle class="gl-w-half gl-ml-3" />
|
||||
</div>
|
||||
<div v-if="filteredKeybindings.length === 0" class="gl-px-5">
|
||||
{{ $options.i18n.noMatch }}
|
||||
</div>
|
||||
<div v-else class="shortcut-help-container gl-mt-8 gl-px-5 gl-pb-5">
|
||||
<section
|
||||
v-for="group in filteredKeybindings"
|
||||
:key="group.id"
|
||||
class="shortcut-help-mapping gl-mb-4"
|
||||
>
|
||||
<strong class="shortcut-help-mapping-title gl-w-half gl-display-inline-block">
|
||||
{{ group.name }}
|
||||
</strong>
|
||||
<div
|
||||
v-for="keybinding in group.keybindings"
|
||||
:key="keybinding.id"
|
||||
class="gl-display-flex gl-align-items-center"
|
||||
>
|
||||
<shortcut
|
||||
class="gl-w-40p gl-flex-shrink-0 gl-text-right gl-pr-4"
|
||||
:shortcuts="keybinding.defaultKeys"
|
||||
/>
|
||||
<div class="gl-w-half gl-flex-shrink-0 gl-flex-grow-1">
|
||||
{{ keybinding.description }}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</gl-modal>
|
||||
</template>
|
||||
|
|
|
@ -6,7 +6,7 @@ import { disableShortcuts, enableShortcuts, shouldDisableShortcuts } from './sho
|
|||
|
||||
export default {
|
||||
i18n: {
|
||||
toggleLabel: __('Keyboard shortcuts'),
|
||||
toggleLabel: __('Toggle shortcuts'),
|
||||
},
|
||||
components: {
|
||||
GlToggle,
|
||||
|
@ -31,14 +31,12 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="localStorageUsable" class="d-inline-flex align-items-center js-toggle-shortcuts">
|
||||
<div v-if="localStorageUsable" class="js-toggle-shortcuts">
|
||||
<gl-toggle
|
||||
v-model="shortcutsEnabled"
|
||||
aria-describedby="shortcutsToggle"
|
||||
:label="$options.i18n.toggleLabel"
|
||||
label-position="left"
|
||||
@change="onChange"
|
||||
/>
|
||||
<div id="shortcutsToggle" class="sr-only">{{ __('Enable or disable keyboard shortcuts') }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -7,6 +7,8 @@ const createAnchor = (href) => {
|
|||
const fragment = new DocumentFragment();
|
||||
const el = document.createElement('a');
|
||||
el.classList.add('link-anchor');
|
||||
el.setAttribute('data-qa-selector', 'line_link');
|
||||
el.setAttribute('data-qa-number', href);
|
||||
el.href = href;
|
||||
fragment.appendChild(el);
|
||||
el.addEventListener('contextmenu', (e) => {
|
||||
|
|
|
@ -105,10 +105,6 @@ hr {
|
|||
}
|
||||
}
|
||||
|
||||
kbd {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
code {
|
||||
padding: 2px 4px;
|
||||
color: $code-color;
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
@import 'framework/flash';
|
||||
@import 'framework/forms';
|
||||
@import 'framework/gfm';
|
||||
@import 'framework/kbd';
|
||||
@import 'framework/header';
|
||||
@import 'framework/highlight';
|
||||
@import 'framework/issue_box';
|
||||
|
|
|
@ -266,15 +266,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.shortcut-mappings {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.shortcuts .shortcut-mappings {
|
||||
display: inline-block;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
kbd {
|
||||
display: inline-block;
|
||||
padding: 3px 5px;
|
||||
font-size: $gl-font-size-monospace-sm;
|
||||
line-height: 10px;
|
||||
color: var(--gray-700, $gray-700);
|
||||
vertical-align: middle;
|
||||
background-color: var(--gray-10, $gray-10);
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: var(--gray-100, $gray-100) var(--gray-100, $gray-100) var(--gray-200, $gray-200);
|
||||
border-image: none;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 -1px 0 var(--gray-200, $gray-200) inset;
|
||||
}
|
|
@ -81,22 +81,6 @@
|
|||
word-break: keep-all;
|
||||
}
|
||||
|
||||
kbd {
|
||||
display: inline-block;
|
||||
padding: 3px 5px;
|
||||
font-size: 11px;
|
||||
line-height: 10px;
|
||||
color: $gray-700;
|
||||
vertical-align: middle;
|
||||
background-color: $gray-10;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: $gray-100 $gray-100 $gray-200;
|
||||
border-image: none;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 -1px 0 $gray-200 inset;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.75em;
|
||||
font-weight: $gl-font-weight-bold;
|
||||
|
|
|
@ -1,30 +1,30 @@
|
|||
.shortcut-mappings {
|
||||
font-size: 12px;
|
||||
color: $gray-700;
|
||||
|
||||
tbody:first-child tr:first-child {
|
||||
padding-top: 0;
|
||||
.shortcut-help {
|
||||
&-body {
|
||||
height: 80vh;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
th {
|
||||
padding-top: 15px;
|
||||
line-height: 1.5;
|
||||
color: $help-shortcut-header-color;
|
||||
text-align: left;
|
||||
&-container {
|
||||
column-count: 1;
|
||||
@include media-breakpoint-up(md) {
|
||||
column-count: 2;
|
||||
}
|
||||
column-gap: 1rem;
|
||||
}
|
||||
|
||||
td {
|
||||
padding-top: 3px;
|
||||
padding-bottom: 3px;
|
||||
vertical-align: top;
|
||||
line-height: 20px;
|
||||
}
|
||||
&-mapping {
|
||||
overflow: hidden;
|
||||
break-inside: avoid;
|
||||
|
||||
.shortcut {
|
||||
padding-right: 10px;
|
||||
color: $gray-300;
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
&-title {
|
||||
margin-left: 40%;
|
||||
}
|
||||
|
||||
kbd {
|
||||
margin: 0.1rem 0;
|
||||
line-height: unset;
|
||||
font-size: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -10,9 +10,9 @@ module Repositories
|
|||
end
|
||||
|
||||
def execute
|
||||
return unless search
|
||||
return unless search && offset && limit
|
||||
|
||||
repository.search_branch_names(search)
|
||||
repository.search_branch_names(search).lazy.drop(offset).take(limit) # rubocop:disable CodeReuse/ActiveRecord
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -20,5 +20,13 @@ module Repositories
|
|||
def search
|
||||
@params[:search].presence
|
||||
end
|
||||
|
||||
def offset
|
||||
@params[:offset]
|
||||
end
|
||||
|
||||
def limit
|
||||
@params[:limit]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -10,8 +10,16 @@ module Resolvers
|
|||
required: true,
|
||||
description: 'The pattern to search for branch names by.'
|
||||
|
||||
def resolve(search_pattern:)
|
||||
Repositories::BranchNamesFinder.new(object, search: search_pattern).execute
|
||||
argument :offset, GraphQL::INT_TYPE,
|
||||
required: true,
|
||||
description: 'The number of branch names to skip.'
|
||||
|
||||
argument :limit, GraphQL::INT_TYPE,
|
||||
required: true,
|
||||
description: 'The number of branch names to return.'
|
||||
|
||||
def resolve(search_pattern:, offset:, limit:)
|
||||
Repositories::BranchNamesFinder.new(object, offset: offset, limit: limit, search: search_pattern).execute
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -28,7 +28,6 @@ module OptimizedIssuableLabelFilter
|
|||
|
||||
# Taken from IssuableFinder
|
||||
def count_by_state
|
||||
return super if root_namespace.nil?
|
||||
return super if Feature.disabled?(:optimized_issuable_label_filter, default_enabled: :yaml)
|
||||
|
||||
count_params = params.merge(state: nil, sort: nil, force_cte: true)
|
||||
|
@ -40,7 +39,11 @@ module OptimizedIssuableLabelFilter
|
|||
.group(:state_id)
|
||||
.count
|
||||
|
||||
counts = state_counts.transform_keys { |key| count_key(key) }
|
||||
counts = Hash.new(0)
|
||||
|
||||
state_counts.each do |key, value|
|
||||
counts[count_key(key)] += value
|
||||
end
|
||||
|
||||
counts[:all] = counts.values.sum
|
||||
counts.with_indifferent_access
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Users
|
||||
class UpsertCreditCardValidationService < BaseService
|
||||
def initialize(params)
|
||||
@params = params.to_h.with_indifferent_access
|
||||
end
|
||||
|
||||
def execute
|
||||
::Users::CreditCardValidation.upsert(@params)
|
||||
|
||||
ServiceResponse.success(message: 'CreditCardValidation was set')
|
||||
rescue ActiveRecord::InvalidForeignKey, ActiveRecord::NotNullViolation => e
|
||||
ServiceResponse.error(message: "Could not set CreditCardValidation: #{e.message}")
|
||||
rescue StandardError => e
|
||||
Gitlab::ErrorTracking.track_exception(e, params: @params, class: self.class.to_s)
|
||||
ServiceResponse.error(message: "Could not set CreditCardValidation: #{e.message}")
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add migration to update plans on new post-EoA subscriptions
|
||||
merge_request: 55625
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add API to set credit card validation timestamp for user
|
||||
merge_request: 60828
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix incorrect issue and merge requests counts with filters
|
||||
merge_request: 61230
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: "Update Keyboard shortcut help: adding search, update styling"
|
||||
merge_request: 56400
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add offset and limit to branch names resolver.
|
||||
merge_request: 61061
|
||||
author:
|
||||
type: changed
|
|
@ -1,18 +1,19 @@
|
|||
---
|
||||
key_path: counts.boards
|
||||
description: Count of total Boards created
|
||||
description: Count of Boards created
|
||||
product_section: dev
|
||||
product_stage: plan
|
||||
product_group: group::project management
|
||||
product_category: boards
|
||||
product_category: boards
|
||||
value_type: number
|
||||
status: data_available
|
||||
time_frame: all
|
||||
data_source: database
|
||||
instrumentation_class: 'Gitlab::Usage::Metrics::Instrumentations::CountBoardsMetric'
|
||||
distribution:
|
||||
- ce
|
||||
- ee
|
||||
- ee
|
||||
tier:
|
||||
- free
|
||||
- premium
|
||||
- ultimate
|
||||
- premium
|
||||
- ultimate
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class UpdateGitlabSubscriptionsStartAtPostEoa < ActiveRecord::Migration[6.0]
|
||||
UPDATE_BATCH_SIZE = 100
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
class Plan < ActiveRecord::Base
|
||||
self.table_name = 'plans'
|
||||
self.inheritance_column = :_type_disabled
|
||||
end
|
||||
|
||||
class GitlabSubscription < ActiveRecord::Base
|
||||
include EachBatch
|
||||
|
||||
self.table_name = 'gitlab_subscriptions'
|
||||
self.inheritance_column = :_type_disabled
|
||||
|
||||
EOA_ROLLOUT_DATE = '2021-01-26'
|
||||
|
||||
scope :with_plan, -> (from_plan) do
|
||||
where("start_date >= ? AND hosted_plan_id = ?", EOA_ROLLOUT_DATE, from_plan.id)
|
||||
end
|
||||
end
|
||||
|
||||
def up
|
||||
return unless Gitlab.com?
|
||||
|
||||
silver_plan = Plan.find_by(name: 'silver')
|
||||
gold_plan = Plan.find_by(name: 'gold')
|
||||
premium_plan = Plan.find_by(name: 'premium')
|
||||
ultimate_plan = Plan.find_by(name: 'ultimate')
|
||||
|
||||
# Silver to Premium
|
||||
update_hosted_plan_for_subscription(from_plan: silver_plan, to_plan: premium_plan)
|
||||
|
||||
# Gold to Ultimate
|
||||
update_hosted_plan_for_subscription(from_plan: gold_plan, to_plan: ultimate_plan)
|
||||
end
|
||||
|
||||
def down
|
||||
# no-op
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def update_hosted_plan_for_subscription(from_plan:, to_plan:)
|
||||
return unless from_plan && to_plan
|
||||
|
||||
GitlabSubscription.with_plan(from_plan).each_batch(of: UPDATE_BATCH_SIZE) do |batch|
|
||||
batch.update_all(hosted_plan_id: to_plan.id)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1 @@
|
|||
cef2421a6885cb8b28d34388af6c79c4be1564dfd5fae2efcb35622d511eb8c0
|
|
@ -74,8 +74,6 @@ The following metrics are available:
|
|||
| `gitlab_transaction_event_import_repository_total` | Counter | 9.4 | Counter for repository imports (RepositoryImportWorker) | |
|
||||
| `gitlab_transaction_event_patch_hard_limit_bytes_hit_total` | Counter | 13.9 | Counter for diff patch size limit hits | |
|
||||
| `gitlab_transaction_event_push_branch_total` | Counter | 9.4 | Counter for all branch pushes | |
|
||||
| `gitlab_transaction_event_push_commit_total` | Counter | 9.4 | Counter for commits | `branch` |
|
||||
| `gitlab_transaction_event_push_tag_total` | Counter | 9.4 | Counter for tag pushes | |
|
||||
| `gitlab_transaction_event_rails_exception_total` | Counter | 9.4 | Counter for number of rails exceptions | |
|
||||
| `gitlab_transaction_event_receive_email_total` | Counter | 9.4 | Counter for received emails | `handler` |
|
||||
| `gitlab_transaction_event_remote_mirrors_failed_total` | Counter | 10.8 | Counter for failed remote mirrors | |
|
||||
|
|
|
@ -11760,6 +11760,8 @@ Returns [`[String!]`](#string).
|
|||
|
||||
| Name | Type | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| <a id="repositorybranchnameslimit"></a>`limit` | [`Int!`](#int) | The number of branch names to return. |
|
||||
| <a id="repositorybranchnamesoffset"></a>`offset` | [`Int!`](#int) | The number of branch names to skip. |
|
||||
| <a id="repositorybranchnamessearchpattern"></a>`searchPattern` | [`String!`](#string) | The pattern to search for branch names by. |
|
||||
|
||||
##### `Repository.tree`
|
||||
|
|
|
@ -118,7 +118,7 @@ job with the same name, the job artifact from the parent pipeline is returned.
|
|||
|
||||
## Access the latest job artifacts by URL
|
||||
|
||||
You can download the latest job artifacts by using a URL.
|
||||
You can download job artifacts from the latest successful pipeline by using a URL.
|
||||
|
||||
To download the whole artifacts archive:
|
||||
|
||||
|
|
|
@ -468,7 +468,7 @@ Tiers: `free`
|
|||
|
||||
### `counts.boards`
|
||||
|
||||
Count of total Boards created
|
||||
Count of Boards created
|
||||
|
||||
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210216181252_boards.yml)
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ group: Optimize
|
|||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
# Analytics
|
||||
# Analytics **(FREE)**
|
||||
|
||||
## Definitions
|
||||
|
||||
|
@ -70,38 +70,38 @@ in one place.
|
|||
|
||||
[Learn more about instance-level analytics](../admin_area/analytics/index.md).
|
||||
|
||||
## Group-level analytics
|
||||
## Group-level analytics **(PREMIUM)**
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/195979) in GitLab 12.8.
|
||||
> - Moved to [GitLab Premium](https://about.gitlab.com/pricing/) due to Starter/Bronze being [discontinued](https://about.gitlab.com/blog/2021/01/26/new-gitlab-product-subscription-model/) in 13.9.
|
||||
> - Moved to GitLab Premium in 13.9.
|
||||
|
||||
The following analytics features are available at the group level:
|
||||
|
||||
- [Application Security](../application_security/security_dashboard/#group-security-dashboard). **(ULTIMATE)**
|
||||
- [Contribution](../group/contribution_analytics/index.md). **(PREMIUM)**
|
||||
- [DevOps Adoption](../group/devops_adoption/index.md). **(ULTIMATE)**
|
||||
- [Insights](../group/insights/index.md). **(ULTIMATE)**
|
||||
- [Issue](../group/issues_analytics/index.md). **(PREMIUM)**
|
||||
- [Productivity](productivity_analytics.md). **(PREMIUM)**
|
||||
- [Repositories](../group/repositories_analytics/index.md). **(PREMIUM)**
|
||||
- [Value Stream](../group/value_stream_analytics/index.md). **(PREMIUM)**
|
||||
- [Application Security](../application_security/security_dashboard/#group-security-dashboard)
|
||||
- [Contribution](../group/contribution_analytics/index.md)
|
||||
- [DevOps Adoption](../group/devops_adoption/index.md)
|
||||
- [Insights](../group/insights/index.md)
|
||||
- [Issue](../group/issues_analytics/index.md)
|
||||
- [Productivity](productivity_analytics.md)
|
||||
- [Repositories](../group/repositories_analytics/index.md)
|
||||
- [Value Stream](../group/value_stream_analytics/index.md)
|
||||
|
||||
## Project-level analytics
|
||||
|
||||
The following analytics features are available at the project level:
|
||||
|
||||
- [Application Security](../application_security/security_dashboard/#project-security-dashboard). **(ULTIMATE)**
|
||||
- [CI/CD](ci_cd_analytics.md). **(FREE)**
|
||||
- [Code Review](code_review_analytics.md). **(PREMIUM)**
|
||||
- [Insights](../project/insights/index.md). **(ULTIMATE)**
|
||||
- [Issue](../group/issues_analytics/index.md). **(PREMIUM)**
|
||||
- [Application Security](../application_security/security_dashboard/#project-security-dashboard)
|
||||
- [CI/CD](ci_cd_analytics.md)
|
||||
- [Code Review](code_review_analytics.md)
|
||||
- [Insights](../project/insights/index.md)
|
||||
- [Issue](../group/issues_analytics/index.md)
|
||||
- [Merge Request](merge_request_analytics.md), enabled with the `project_merge_request_analytics`
|
||||
[feature flag](../../development/feature_flags/index.md#enabling-a-feature-flag-locally-in-development). **(PREMIUM)**
|
||||
- [Repository](repository_analytics.md). **(FREE)**
|
||||
- [Value Stream](value_stream_analytics.md). **(FREE)**
|
||||
[feature flag](../../development/feature_flags/index.md#enabling-a-feature-flag-locally-in-development)
|
||||
- [Repository](repository_analytics.md)
|
||||
- [Value Stream](value_stream_analytics.md)
|
||||
|
||||
## User-configurable analytics
|
||||
## User-configurable analytics **(ULTIMATE)**
|
||||
|
||||
The following analytics features are available for users to create personalized views:
|
||||
|
||||
- [Application Security](../application_security/security_dashboard/#security-center). **(ULTIMATE)**
|
||||
- [Application Security](../application_security/security_dashboard/#security-center)
|
||||
|
|
|
@ -7,8 +7,8 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
|
||||
# Merge Request Analytics **(PREMIUM)**
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/229045) in [GitLab Starter](https://about.gitlab.com/pricing/) 13.3.
|
||||
> - Moved to [GitLab Premium](https://about.gitlab.com/pricing/) due to Starter/Bronze being [discontinued](https://about.gitlab.com/blog/2021/01/26/new-gitlab-product-subscription-model/) in 13.9.
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/229045) in GitLab 13.3.
|
||||
> - Moved to GitLab Premium in 13.9.
|
||||
|
||||
Merge Request Analytics helps you understand the efficiency of your code review process, and the productivity of your team.
|
||||
|
||||
|
@ -56,7 +56,7 @@ The throughput chart shows the number of merge requests merged per month.
|
|||
|
||||
### Throughput table
|
||||
|
||||
[Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/232651) in GitLab 13.3.
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/232651) in GitLab 13.3.
|
||||
|
||||
The Throughput table displays the most recent merge requests merged in the date range. The
|
||||
table displays up to 20 merge requests at a time. If there are more than 20 merge requests,
|
||||
|
|
|
@ -12,22 +12,17 @@ packages, which can be easily consumed as a dependency in downstream projects.
|
|||
|
||||
The Package Registry supports the following formats:
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-9">
|
||||
<table align="left" style="width:50%">
|
||||
<tr style="background:#dfdfdf"><th>Package type</th><th>GitLab version</th></tr>
|
||||
<tr><td><a href="https://docs.gitlab.com/ee/user/packages/composer_repository/index.html">Composer</a></td><td>13.2+</td></tr>
|
||||
<tr><td><a href="https://docs.gitlab.com/ee/user/packages/conan_repository/index.html">Conan</a></td><td>12.6+</td></tr>
|
||||
<tr><td><a href="https://docs.gitlab.com/ee/user/packages/go_proxy/index.html">Go</a></td><td>13.1+</td></tr>
|
||||
<tr><td><a href="https://docs.gitlab.com/ee/user/packages/maven_repository/index.html">Maven</a></td><td>11.3+</td></tr>
|
||||
<tr><td><a href="https://docs.gitlab.com/ee/user/packages/npm_registry/index.html">npm</a></td><td>11.7+</td></tr>
|
||||
<tr><td><a href="https://docs.gitlab.com/ee/user/packages/nuget_repository/index.html">NuGet</a></td><td>12.8+</td></tr>
|
||||
<tr><td><a href="https://docs.gitlab.com/ee/user/packages/pypi_repository/index.html">PyPI</a></td><td>12.10+</td></tr>
|
||||
<tr><td><a href="https://docs.gitlab.com/ee/user/packages/generic_packages/index.html">Generic packages</a></td><td>13.5+</td></tr>
|
||||
<tr><td><a href="https://docs.gitlab.com/ee/user/packages/rubygems_registry/index.html">Ruby gems</a></td><td>13.10+</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
| Package type | GitLab version |
|
||||
| ------------ | -------------- |
|
||||
| [Composer](composer_repository/index.md) | 13.2+ |
|
||||
| [Conan](conan_repository/index.md) | 12.6+ |
|
||||
| [Go](go_proxy/index.md) | 13.1+ |
|
||||
| [Maven](maven_repository/index.md) | 11.3+ |
|
||||
| [npm](npm_registry/index.md) | 11.7+ |
|
||||
| [NuGet](nuget_repository/index.md) | 12.8+ |
|
||||
| [PyPI](pypi_repository/index.md) | 12.10+ |
|
||||
| [Generic packages](generic_packages/index.md) | 13.5+ |
|
||||
| [Ruby gems](rubygems_registry/index.md) | 13.10+ |
|
||||
|
||||
You can also use the [API](../../api/packages.md) to administer the Package Registry.
|
||||
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module API
|
||||
module Entities
|
||||
class UserCreditCardValidations < Grape::Entity
|
||||
expose :user_id, :credit_card_validated_at
|
||||
end
|
||||
end
|
||||
end
|
|
@ -996,6 +996,30 @@ module API
|
|||
present paginate(current_user.emails), with: Entities::Email
|
||||
end
|
||||
|
||||
desc "Update a user's credit_card_validation" do
|
||||
success Entities::UserCreditCardValidations
|
||||
end
|
||||
params do
|
||||
requires :user_id, type: String, desc: 'The ID or username of the user'
|
||||
requires :credit_card_validated_at, type: DateTime, desc: 'The time when the user\'s credit card was validated'
|
||||
end
|
||||
put ":user_id/credit_card_validation", feature_category: :users do
|
||||
authenticated_as_admin!
|
||||
|
||||
user = find_user(params[:user_id])
|
||||
not_found!('User') unless user
|
||||
|
||||
attrs = declared_params(include_missing: false)
|
||||
|
||||
service = ::Users::UpsertCreditCardValidationService.new(attrs).execute
|
||||
|
||||
if service.success?
|
||||
present user.credit_card_validation, with: Entities::UserCreditCardValidations
|
||||
else
|
||||
render_api_error!('400 Bad Request', 400)
|
||||
end
|
||||
end
|
||||
|
||||
desc "Update the current user's preferences" do
|
||||
success Entities::UserPreferences
|
||||
detail 'This feature was introduced in GitLab 13.10.'
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Usage
|
||||
module Metrics
|
||||
module Instrumentations
|
||||
class CountBoardsMetric < DatabaseMetric
|
||||
operation :count
|
||||
|
||||
relation { Board }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -3,7 +3,7 @@
|
|||
module Sidebars
|
||||
module Projects
|
||||
module Menus
|
||||
class ProjectOverviewMenu < ::Sidebars::Menu
|
||||
class ProjectInformationMenu < ::Sidebars::Menu
|
||||
override :configure_menu_items
|
||||
def configure_menu_items
|
||||
add_item(details_menu_item)
|
||||
|
@ -32,17 +32,34 @@ module Sidebars
|
|||
|
||||
override :title
|
||||
def title
|
||||
_('Project overview')
|
||||
if Feature.enabled?(:sidebar_refactor, context.current_user)
|
||||
_('Project information')
|
||||
else
|
||||
_('Project overview')
|
||||
end
|
||||
end
|
||||
|
||||
override :sprite_icon
|
||||
def sprite_icon
|
||||
'home'
|
||||
if Feature.enabled?(:sidebar_refactor, context.current_user)
|
||||
'project'
|
||||
else
|
||||
'home'
|
||||
end
|
||||
end
|
||||
|
||||
override :active_routes
|
||||
def active_routes
|
||||
return {} if Feature.disabled?(:sidebar_refactor, context.current_user)
|
||||
|
||||
{ path: 'projects#show' }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def details_menu_item
|
||||
return if Feature.enabled?(:sidebar_refactor, context.current_user)
|
||||
|
||||
::Sidebars::MenuItem.new(
|
||||
title: _('Details'),
|
||||
link: project_path(context.project),
|
|
@ -7,7 +7,7 @@ module Sidebars
|
|||
def configure_menus
|
||||
set_scope_menu(Sidebars::Projects::Menus::ScopeMenu.new(context))
|
||||
|
||||
add_menu(Sidebars::Projects::Menus::ProjectOverviewMenu.new(context))
|
||||
add_menu(Sidebars::Projects::Menus::ProjectInformationMenu.new(context))
|
||||
add_menu(Sidebars::Projects::Menus::LearnGitlabMenu.new(context))
|
||||
add_menu(Sidebars::Projects::Menus::RepositoryMenu.new(context))
|
||||
add_menu(Sidebars::Projects::Menus::IssuesMenu.new(context))
|
||||
|
|
|
@ -3661,7 +3661,7 @@ msgstr ""
|
|||
msgid "An error occurred while reordering issues."
|
||||
msgstr ""
|
||||
|
||||
msgid "An error occurred while requesting data from the Jira service"
|
||||
msgid "An error occurred while requesting data from the Jira service."
|
||||
msgstr ""
|
||||
|
||||
msgid "An error occurred while retrieving calendar activity"
|
||||
|
@ -12185,9 +12185,6 @@ msgstr ""
|
|||
msgid "Enable or disable Seat Link."
|
||||
msgstr ""
|
||||
|
||||
msgid "Enable or disable keyboard shortcuts"
|
||||
msgstr ""
|
||||
|
||||
msgid "Enable or disable the Pseudonymizer data collection."
|
||||
msgstr ""
|
||||
|
||||
|
@ -14538,6 +14535,9 @@ msgstr ""
|
|||
msgid "Geo|%{component} synced"
|
||||
msgstr ""
|
||||
|
||||
msgid "Geo|%{eventId} (%{timeAgo})"
|
||||
msgstr ""
|
||||
|
||||
msgid "Geo|%{itemTitle} checksum progress"
|
||||
msgstr ""
|
||||
|
||||
|
@ -14616,6 +14616,9 @@ msgstr ""
|
|||
msgid "Geo|Data type"
|
||||
msgstr ""
|
||||
|
||||
msgid "Geo|Disabled"
|
||||
msgstr ""
|
||||
|
||||
msgid "Geo|Discover GitLab Geo"
|
||||
msgstr ""
|
||||
|
||||
|
@ -14643,6 +14646,9 @@ msgstr ""
|
|||
msgid "Geo|Go to the primary site"
|
||||
msgstr ""
|
||||
|
||||
msgid "Geo|Healthy"
|
||||
msgstr ""
|
||||
|
||||
msgid "Geo|If you want to make changes, you must visit the primary site."
|
||||
msgstr ""
|
||||
|
||||
|
@ -14706,6 +14712,9 @@ msgstr ""
|
|||
msgid "Geo|Number of %{title}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Geo|Offline"
|
||||
msgstr ""
|
||||
|
||||
msgid "Geo|Pending synchronization"
|
||||
msgstr ""
|
||||
|
||||
|
@ -14868,6 +14877,12 @@ msgstr ""
|
|||
msgid "Geo|Undefined"
|
||||
msgstr ""
|
||||
|
||||
msgid "Geo|Unhealthy"
|
||||
msgstr ""
|
||||
|
||||
msgid "Geo|Unknown"
|
||||
msgstr ""
|
||||
|
||||
msgid "Geo|Unknown state"
|
||||
msgstr ""
|
||||
|
||||
|
@ -18399,7 +18414,7 @@ msgstr ""
|
|||
msgid "Jira integration not configured."
|
||||
msgstr ""
|
||||
|
||||
msgid "Jira project key is not configured"
|
||||
msgid "Jira project key is not configured."
|
||||
msgstr ""
|
||||
|
||||
msgid "Jira project: %{importProject}"
|
||||
|
@ -18798,9 +18813,30 @@ msgstr ""
|
|||
msgid "Keyboard shortcuts"
|
||||
msgstr ""
|
||||
|
||||
msgid "KeyboardKey|Alt"
|
||||
msgstr ""
|
||||
|
||||
msgid "KeyboardKey|Ctrl"
|
||||
msgstr ""
|
||||
|
||||
msgid "KeyboardKey|Ctrl+"
|
||||
msgstr ""
|
||||
|
||||
msgid "KeyboardKey|Enter"
|
||||
msgstr ""
|
||||
|
||||
msgid "KeyboardKey|Esc"
|
||||
msgstr ""
|
||||
|
||||
msgid "KeyboardKey|Shift"
|
||||
msgstr ""
|
||||
|
||||
msgid "KeyboardShortcuts|No shortcuts matched your search"
|
||||
msgstr ""
|
||||
|
||||
msgid "KeyboardShortcuts|Search keyboard shortcuts"
|
||||
msgstr ""
|
||||
|
||||
msgid "Keys"
|
||||
msgstr ""
|
||||
|
||||
|
@ -25162,6 +25198,9 @@ msgstr ""
|
|||
msgid "Project info:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Project information"
|
||||
msgstr ""
|
||||
|
||||
msgid "Project is required when cluster_type is :project"
|
||||
msgstr ""
|
||||
|
||||
|
@ -33762,15 +33801,15 @@ msgstr ""
|
|||
msgid "Toggle project select"
|
||||
msgstr ""
|
||||
|
||||
msgid "Toggle shortcuts"
|
||||
msgstr ""
|
||||
|
||||
msgid "Toggle sidebar"
|
||||
msgstr ""
|
||||
|
||||
msgid "Toggle the Performance Bar"
|
||||
msgstr ""
|
||||
|
||||
msgid "Toggle this dialog"
|
||||
msgstr ""
|
||||
|
||||
msgid "Toggle thread"
|
||||
msgstr ""
|
||||
|
||||
|
@ -38934,6 +38973,9 @@ msgstr ""
|
|||
msgid "the wiki"
|
||||
msgstr ""
|
||||
|
||||
msgid "then"
|
||||
msgstr ""
|
||||
|
||||
msgid "this document"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -147,7 +147,7 @@ module QA
|
|||
end
|
||||
|
||||
def open_web_ide!
|
||||
click_element :web_ide_button
|
||||
click_element(:web_ide_button)
|
||||
end
|
||||
|
||||
def has_edit_fork_button?
|
||||
|
|
|
@ -18,8 +18,8 @@ module QA
|
|||
end
|
||||
|
||||
view 'app/assets/javascripts/ide/components/ide_tree.vue' do
|
||||
element :new_file_button
|
||||
element :new_directory_button
|
||||
element :new_file_button, required: true
|
||||
element :new_directory_button, required: true
|
||||
end
|
||||
|
||||
view 'app/assets/javascripts/ide/components/ide_tree_list.vue' do
|
||||
|
@ -108,6 +108,10 @@ module QA
|
|||
element :file_to_commit_content
|
||||
end
|
||||
|
||||
view 'app/assets/javascripts/editor/extensions/editor_lite_extension_base.js' do
|
||||
element :line_link
|
||||
end
|
||||
|
||||
def has_file?(file_name)
|
||||
within_element(:file_list) do
|
||||
has_element?(:file_name_content, file_name: file_name)
|
||||
|
@ -305,6 +309,22 @@ module QA
|
|||
def switch_to_commit_tab
|
||||
click_element(:commit_mode_tab)
|
||||
end
|
||||
|
||||
def select_file(file_name)
|
||||
# wait for the list of files to load
|
||||
wait_until(reload: true) do
|
||||
has_element?(:file_name_content, file_name: file_name)
|
||||
end
|
||||
click_element(:file_name_content, file_name: file_name)
|
||||
end
|
||||
|
||||
def link_line(line_number)
|
||||
wait_for_animated_element(:editor_container)
|
||||
within_element(:editor_container) do
|
||||
find('.line-numbers', text: line_number).hover
|
||||
find_element(:line_link, number: "#L#{line_number}")['href'].to_s
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module QA
|
||||
RSpec.describe 'Create' do
|
||||
describe 'Link to line in Web IDE' do
|
||||
let(:user) { Resource::User.fabricate_or_use(Runtime::Env.gitlab_qa_username_1, Runtime::Env.gitlab_qa_password_1) }
|
||||
let(:project) do
|
||||
Resource::Project.fabricate_via_api! do |project|
|
||||
project.template_name = 'express'
|
||||
end
|
||||
end
|
||||
|
||||
before do
|
||||
Flow::Login.sign_in
|
||||
end
|
||||
|
||||
after do
|
||||
project.remove_via_api!
|
||||
end
|
||||
|
||||
it 'can link to a specific line of code in Web IDE', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1102' do
|
||||
project.visit!
|
||||
|
||||
Page::Project::Show.perform(&:open_web_ide!)
|
||||
|
||||
Page::Project::WebIDE::Edit.perform do |ide|
|
||||
ide.select_file('app.js')
|
||||
@link = ide.link_line('26')
|
||||
end
|
||||
|
||||
Flow::Login.sign_in(as: user)
|
||||
|
||||
page.visit(@link)
|
||||
|
||||
Page::Project::WebIDE::Edit.perform do |ide|
|
||||
expect(ide).to have_file('app.js')
|
||||
end
|
||||
|
||||
expect(page.driver.current_url).to include('app.js/#L26')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -378,11 +378,11 @@ RSpec.describe 'Group' do
|
|||
expect(page).to have_link('Subgroup information')
|
||||
end
|
||||
|
||||
it 'renders project page with the text "Project overview"' do
|
||||
it 'renders project page with the text "Project information"' do
|
||||
visit project_path(project)
|
||||
wait_for_requests
|
||||
|
||||
expect(page).to have_link('Project overview')
|
||||
expect(page).to have_link('Project information')
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -3,11 +3,11 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'Project active tab' do
|
||||
let(:user) { create :user }
|
||||
let(:project) { create(:project, :repository) }
|
||||
let_it_be(:project) { create(:project, :repository) }
|
||||
|
||||
let(:user) { project.owner }
|
||||
|
||||
before do
|
||||
project.add_maintainer(user)
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
|
@ -18,15 +18,28 @@ RSpec.describe 'Project active tab' do
|
|||
end
|
||||
|
||||
context 'on project Home' do
|
||||
before do
|
||||
visit project_path(project)
|
||||
context 'when feature flag :sidebar_refactor is enabled' do
|
||||
before do
|
||||
visit project_path(project)
|
||||
end
|
||||
|
||||
it_behaves_like 'page has active tab', 'Project'
|
||||
end
|
||||
|
||||
it_behaves_like 'page has active tab', 'Project'
|
||||
it_behaves_like 'page has active sub tab', 'Details'
|
||||
context 'when feature flag :sidebar_refactor is disabled' do
|
||||
before do
|
||||
stub_feature_flags(sidebar_refactor: false)
|
||||
|
||||
visit project_path(project)
|
||||
end
|
||||
|
||||
it_behaves_like 'page has active tab', 'Project'
|
||||
it_behaves_like 'page has active sub tab', 'Details'
|
||||
end
|
||||
|
||||
context 'on project Home/Activity' do
|
||||
before do
|
||||
visit project_path(project)
|
||||
click_tab('Activity')
|
||||
end
|
||||
|
||||
|
|
|
@ -86,6 +86,16 @@ RSpec.describe 'Project navbar' do
|
|||
]
|
||||
end
|
||||
|
||||
let(:project_information_nav_item) do
|
||||
{
|
||||
nav_item: _('Project information'),
|
||||
nav_sub_items: [
|
||||
_('Activity'),
|
||||
_('Releases')
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
before do
|
||||
stub_feature_flags(sidebar_refactor: true)
|
||||
stub_config(registry: { enabled: true })
|
||||
|
|
|
@ -68,14 +68,27 @@ RSpec.describe 'User uses shortcuts', :js do
|
|||
end
|
||||
|
||||
context 'when navigating to the Project pages' do
|
||||
it 'redirects to the details page' do
|
||||
it 'redirects to the project page' do
|
||||
visit project_issues_path(project)
|
||||
|
||||
find('body').native.send_key('g')
|
||||
find('body').native.send_key('p')
|
||||
|
||||
expect(page).to have_active_navigation('Project')
|
||||
expect(page).to have_active_sub_navigation('Details')
|
||||
end
|
||||
|
||||
context 'when feature flag :sidebar_refactor is disabled' do
|
||||
it 'redirects to the details page' do
|
||||
stub_feature_flags(sidebar_refactor: false)
|
||||
|
||||
visit project_issues_path(project)
|
||||
|
||||
find('body').native.send_key('g')
|
||||
find('body').native.send_key('p')
|
||||
|
||||
expect(page).to have_active_navigation('Project')
|
||||
expect(page).to have_active_sub_navigation('Details')
|
||||
end
|
||||
end
|
||||
|
||||
it 'redirects to the activity page' do
|
||||
|
|
|
@ -866,5 +866,36 @@ RSpec.describe MergeRequestsFinder do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#count_by_state' do
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:project) { create(:project, :repository) }
|
||||
let_it_be(:labels) { create_list(:label, 2, project: project) }
|
||||
let_it_be(:merge_requests) { create_list(:merge_request, 4, :unique_branches, author: user, target_project: project, source_project: project, labels: labels) }
|
||||
|
||||
before do
|
||||
project.add_developer(user)
|
||||
end
|
||||
|
||||
context 'when filtering by multiple labels' do
|
||||
it 'returns the correnct counts' do
|
||||
counts = described_class.new(user, { label_name: labels.map(&:name) }).count_by_state
|
||||
|
||||
expect(counts[:all]).to eq(merge_requests.size)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when filtering by approved_by_usernames' do
|
||||
before do
|
||||
merge_requests.each { |mr| mr.approved_by_users << user }
|
||||
end
|
||||
|
||||
it 'returns the correnct counts' do
|
||||
counts = described_class.new(user, { approved_by_usernames: [user.username] }).count_by_state
|
||||
|
||||
expect(counts[:all]).to eq(merge_requests.size)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,21 +5,34 @@ require 'spec_helper'
|
|||
RSpec.describe Repositories::BranchNamesFinder do
|
||||
let(:project) { create(:project, :repository) }
|
||||
|
||||
let(:branch_names_finder) { described_class.new(project.repository, search: 'conflict-*') }
|
||||
|
||||
describe '#execute' do
|
||||
subject(:execute) { branch_names_finder.execute }
|
||||
|
||||
it 'filters branch names' do
|
||||
expect(execute).to contain_exactly(
|
||||
'conflict-binary-file',
|
||||
'conflict-resolvable',
|
||||
'conflict-contains-conflict-markers',
|
||||
'conflict-missing-side',
|
||||
'conflict-start',
|
||||
'conflict-non-utf8',
|
||||
'conflict-too-large'
|
||||
it 'returns all filtered branch names' do
|
||||
expect(create_branch_names_finder(0, 100).execute).to contain_exactly(
|
||||
'snippet/edit-file',
|
||||
'snippet/multiple-files',
|
||||
'snippet/no-files',
|
||||
'snippet/rename-and-edit-file',
|
||||
'snippet/single-file'
|
||||
)
|
||||
end
|
||||
|
||||
it 'returns a limited number of offset filtered branch names' do
|
||||
starting_names = create_branch_names_finder(0, 3).execute
|
||||
offset_names = create_branch_names_finder(3, 2).execute
|
||||
|
||||
expect(starting_names.count).to eq(3)
|
||||
expect(offset_names.count).to eq(2)
|
||||
|
||||
expect(offset_names).not_to include(*starting_names)
|
||||
|
||||
all_names = create_branch_names_finder(0, 100).execute
|
||||
expect(all_names).to contain_exactly(*starting_names, *offset_names)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def create_branch_names_finder(offset, limit)
|
||||
described_class.new(project.repository, search: 'snippet/*', offset: offset, limit: limit)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,96 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import Shortcut from '~/behaviors/shortcuts/shortcut.vue';
|
||||
|
||||
describe('Shortcut Vue Component', () => {
|
||||
const render = (shortcuts) => shallowMount(Shortcut, { propsData: { shortcuts } }).html();
|
||||
|
||||
afterEach(() => {
|
||||
delete window.gl.client;
|
||||
});
|
||||
|
||||
describe.each([true, false])('With browser env isMac: %p', (isMac) => {
|
||||
beforeEach(() => {
|
||||
window.gl = { client: { isMac } };
|
||||
});
|
||||
|
||||
it.each([
|
||||
['up', '<kbd>↑</kbd>'],
|
||||
['down', '<kbd>↓</kbd>'],
|
||||
['left', '<kbd>←</kbd>'],
|
||||
['right', '<kbd>→</kbd>'],
|
||||
['ctrl', '<kbd>Ctrl</kbd>'],
|
||||
['shift', '<kbd>Shift</kbd>'],
|
||||
['enter', '<kbd>Enter</kbd>'],
|
||||
['esc', '<kbd>Esc</kbd>'],
|
||||
// Some normal ascii letter
|
||||
['a', '<kbd>a</kbd>'],
|
||||
// An umlaut letter
|
||||
['ø', '<kbd>ø</kbd>'],
|
||||
// A number
|
||||
['5', '<kbd>5</kbd>'],
|
||||
])('renders platform agnostic key %p as: %p', (key, rendered) => {
|
||||
expect(render([key])).toEqual(`<div>${rendered}</div>`);
|
||||
});
|
||||
|
||||
it('renders keys combined with plus ("+") correctly', () => {
|
||||
expect(render(['shift+a+b+c'])).toEqual(
|
||||
`<div><kbd>Shift</kbd> + <kbd>a</kbd> + <kbd>b</kbd> + <kbd>c</kbd></div>`,
|
||||
);
|
||||
});
|
||||
|
||||
it('renders keys combined with space (" ") correctly', () => {
|
||||
expect(render(['shift a b c'])).toEqual(
|
||||
`<div><kbd>Shift</kbd> then <kbd>a</kbd> then <kbd>b</kbd> then <kbd>c</kbd></div>`,
|
||||
);
|
||||
});
|
||||
|
||||
it('renders multiple shortcuts correctly', () => {
|
||||
expect(render(['shift+[', 'shift+k'])).toEqual(
|
||||
`<div><kbd>Shift</kbd> + <kbd>[</kbd> or <br><kbd>Shift</kbd> + <kbd>k</kbd></div>`,
|
||||
);
|
||||
expect(render(['[', 'k'])).toEqual(`<div><kbd>[</kbd> or <kbd>k</kbd></div>`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('With browser env isMac: true', () => {
|
||||
beforeEach(() => {
|
||||
window.gl = { client: { isMac: true } };
|
||||
});
|
||||
|
||||
it.each([
|
||||
['mod', '<kbd>⌘</kbd>'],
|
||||
['command', '<kbd>⌘</kbd>'],
|
||||
['meta', '<kbd>⌘</kbd>'],
|
||||
['option', '<kbd>⌥</kbd>'],
|
||||
['alt', '<kbd>⌥</kbd>'],
|
||||
])('renders platform specific key %p as: %p', (key, rendered) => {
|
||||
expect(render([key])).toEqual(`<div>${rendered}</div>`);
|
||||
});
|
||||
|
||||
it('does render Mac specific shortcuts', () => {
|
||||
expect(render(['command+[', 'ctrl+k'])).toEqual(
|
||||
`<div><kbd>⌘</kbd> + <kbd>[</kbd> or <br><kbd>Ctrl</kbd> + <kbd>k</kbd></div>`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('With browser env isMac: false', () => {
|
||||
beforeEach(() => {
|
||||
window.gl = { client: { isMac: false } };
|
||||
});
|
||||
|
||||
it.each([
|
||||
['mod', '<kbd>Ctrl</kbd>'],
|
||||
['command', ''],
|
||||
['meta', ''],
|
||||
['option', '<kbd>Alt</kbd>'],
|
||||
['alt', '<kbd>Alt</kbd>'],
|
||||
])('renders platform specific key %p as: %p', (key, rendered) => {
|
||||
expect(render([key])).toEqual(`<div>${rendered}</div>`);
|
||||
});
|
||||
|
||||
it('does not render Mac specific shortcuts', () => {
|
||||
expect(render(['command+[', 'ctrl+k'])).toEqual(`<div><kbd>Ctrl</kbd> + <kbd>k</kbd></div>`);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -8,29 +8,50 @@ RSpec.describe Resolvers::RepositoryBranchNamesResolver do
|
|||
let(:project) { create(:project, :repository) }
|
||||
|
||||
describe '#resolve' do
|
||||
subject(:resolve_branch_names) do
|
||||
resolve(
|
||||
described_class,
|
||||
obj: project.repository,
|
||||
args: { search_pattern: pattern },
|
||||
ctx: { current_user: project.creator }
|
||||
)
|
||||
end
|
||||
|
||||
context 'with empty search pattern' do
|
||||
let(:pattern) { '' }
|
||||
|
||||
it 'returns nil' do
|
||||
expect(resolve_branch_names).to eq(nil)
|
||||
expect(resolve_branch_names(pattern, 0, 100)).to eq(nil)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a valid search pattern' do
|
||||
let(:pattern) { 'mas*' }
|
||||
let(:pattern) { 'snippet/*' }
|
||||
|
||||
it 'returns matching branches' do
|
||||
expect(resolve_branch_names).to match_array(['master'])
|
||||
expect(resolve_branch_names(pattern, 0, 100)).to contain_exactly(
|
||||
'snippet/edit-file',
|
||||
'snippet/multiple-files',
|
||||
'snippet/no-files',
|
||||
'snippet/rename-and-edit-file',
|
||||
'snippet/single-file'
|
||||
)
|
||||
end
|
||||
|
||||
it 'properly offsets and limits branch name results' do
|
||||
starting_names = resolve_branch_names(pattern, 0, 3)
|
||||
offset_names = resolve_branch_names(pattern, 3, 2)
|
||||
|
||||
expect(starting_names.count).to eq(3)
|
||||
expect(offset_names.count).to eq(2)
|
||||
|
||||
expect(offset_names).not_to include(*starting_names)
|
||||
|
||||
all_names = resolve_branch_names(pattern, 0, 100)
|
||||
expect(all_names).to contain_exactly(*starting_names, *offset_names)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def resolve_branch_names(pattern, offset, limit)
|
||||
resolve(
|
||||
described_class,
|
||||
obj: project.repository,
|
||||
args: { search_pattern: pattern, offset: offset, limit: limit },
|
||||
ctx: { current_user: project.creator }
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::Usage::Metrics::Instrumentations::CountBoardsMetric do
|
||||
let_it_be(:board) { create(:board) }
|
||||
|
||||
it_behaves_like 'a correct instrumented metric value', { time_frame: 'all', data_source: 'database' }, 1
|
||||
end
|
|
@ -20,6 +20,10 @@ RSpec.describe Gitlab::UsageDataMetrics do
|
|||
it 'includes top level keys' do
|
||||
expect(subject).to include(:uuid)
|
||||
end
|
||||
|
||||
it 'includes counts keys' do
|
||||
expect(subject[:counts]).to include(:boards)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Sidebars::Projects::Menus::ProjectOverviewMenu do
|
||||
RSpec.describe Sidebars::Projects::Menus::ProjectInformationMenu do
|
||||
let_it_be(:project) { create(:project, :repository) }
|
||||
|
||||
let(:user) { project.owner }
|
|
@ -1449,6 +1449,48 @@ RSpec.describe API::Users do
|
|||
end
|
||||
end
|
||||
|
||||
describe "PUT /user/:id/credit_card_validation" do
|
||||
let(:credit_card_validated_time) { Time.utc(2020, 1, 1) }
|
||||
|
||||
context 'when unauthenticated' do
|
||||
it 'returns authentication error' do
|
||||
put api("/user/#{user.id}/credit_card_validation"), params: { credit_card_validated_at: credit_card_validated_time }
|
||||
|
||||
expect(response).to have_gitlab_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when authenticated as non-admin' do
|
||||
it "does not allow updating user's credit card validation", :aggregate_failures do
|
||||
put api("/user/#{user.id}/credit_card_validation", user), params: { credit_card_validated_at: credit_card_validated_time }
|
||||
|
||||
expect(response).to have_gitlab_http_status(:forbidden)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when authenticated as admin' do
|
||||
it "updates user's credit card validation", :aggregate_failures do
|
||||
put api("/user/#{user.id}/credit_card_validation", admin), params: { credit_card_validated_at: credit_card_validated_time }
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(user.reload.credit_card_validated_at).to eq(credit_card_validated_time)
|
||||
end
|
||||
|
||||
it "returns 400 error if credit_card_validated_at is missing" do
|
||||
put api("/user/#{user.id}/credit_card_validation", admin), params: {}
|
||||
|
||||
expect(response).to have_gitlab_http_status(:bad_request)
|
||||
end
|
||||
|
||||
it 'returns 404 error if user not found' do
|
||||
put api("/user/#{non_existing_record_id}/credit_card_validation", admin), params: { credit_card_validated_at: credit_card_validated_time }
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
expect(json_response['message']).to eq('404 User Not Found')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "DELETE /users/:id/identities/:provider" do
|
||||
let(:test_user) { create(:omniauth_user, provider: 'ldapmain') }
|
||||
|
||||
|
|
|
@ -0,0 +1,84 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Users::UpsertCreditCardValidationService do
|
||||
let_it_be(:user) { create(:user) }
|
||||
|
||||
let(:user_id) { user.id }
|
||||
let(:credit_card_validated_time) { Time.utc(2020, 1, 1) }
|
||||
let(:params) { { user_id: user_id, credit_card_validated_at: credit_card_validated_time } }
|
||||
|
||||
describe '#execute' do
|
||||
subject(:service) { described_class.new(params) }
|
||||
|
||||
context 'successfully set credit card validation record for the user' do
|
||||
context 'when user does not have credit card validation record' do
|
||||
it 'creates the credit card validation and returns a success' do
|
||||
expect(user.credit_card_validated_at).to be nil
|
||||
|
||||
result = service.execute
|
||||
|
||||
expect(result.status).to eq(:success)
|
||||
expect(user.reload.credit_card_validated_at).to eq(credit_card_validated_time)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user has credit card validation record' do
|
||||
let(:old_time) { Time.utc(1999, 2, 2) }
|
||||
|
||||
before do
|
||||
create(:credit_card_validation, user: user, credit_card_validated_at: old_time)
|
||||
end
|
||||
|
||||
it 'updates the credit card validation and returns a success' do
|
||||
expect(user.credit_card_validated_at).to eq(old_time)
|
||||
|
||||
result = service.execute
|
||||
|
||||
expect(result.status).to eq(:success)
|
||||
expect(user.reload.credit_card_validated_at).to eq(credit_card_validated_time)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'returns an error without tracking the exception' do
|
||||
it do
|
||||
expect(Gitlab::ErrorTracking).not_to receive(:track_exception)
|
||||
|
||||
result = service.execute
|
||||
|
||||
expect(result.status).to eq(:error)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user id does not exist' do
|
||||
let(:user_id) { non_existing_record_id }
|
||||
|
||||
it_behaves_like 'returns an error without tracking the exception'
|
||||
end
|
||||
|
||||
context 'when missing credit_card_validated_at' do
|
||||
let(:params) { { user_id: user_id } }
|
||||
|
||||
it_behaves_like 'returns an error without tracking the exception'
|
||||
end
|
||||
|
||||
context 'when missing user id' do
|
||||
let(:params) { { credit_card_validated_at: credit_card_validated_time } }
|
||||
|
||||
it_behaves_like 'returns an error without tracking the exception'
|
||||
end
|
||||
|
||||
context 'when unexpected exception happen' do
|
||||
it 'tracks the exception and returns an error' do
|
||||
expect(::Users::CreditCardValidation).to receive(:upsert).and_raise(e = StandardError.new('My exception!'))
|
||||
expect(Gitlab::ErrorTracking).to receive(:track_exception).with(e, class: described_class.to_s, params: params)
|
||||
|
||||
result = service.execute
|
||||
|
||||
expect(result.status).to eq(:error)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -41,16 +41,20 @@ RSpec.shared_context 'project navbar structure' do
|
|||
]
|
||||
end
|
||||
|
||||
let(:project_information_nav_item) do
|
||||
{
|
||||
nav_item: _('Project overview'),
|
||||
nav_sub_items: [
|
||||
_('Details'),
|
||||
_('Activity'),
|
||||
_('Releases')
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
let(:structure) do
|
||||
[
|
||||
{
|
||||
nav_item: _('Project overview'),
|
||||
nav_sub_items: [
|
||||
_('Details'),
|
||||
_('Activity'),
|
||||
_('Releases')
|
||||
]
|
||||
},
|
||||
project_information_nav_item,
|
||||
{
|
||||
nav_item: _('Repository'),
|
||||
nav_sub_items: [
|
||||
|
|
|
@ -19,20 +19,41 @@ RSpec.describe 'layouts/nav/sidebar/_project' do
|
|||
|
||||
it_behaves_like 'has nav sidebar'
|
||||
|
||||
describe 'Project Overview' do
|
||||
describe 'Project information' do
|
||||
it 'has a link to the project path' do
|
||||
render
|
||||
|
||||
expect(rendered).to have_link('Project overview', href: project_path(project), class: %w(shortcuts-project rspec-project-link))
|
||||
expect(rendered).to have_selector('[aria-label="Project overview"]')
|
||||
expect(rendered).to have_link('Project information', href: project_path(project), class: %w(shortcuts-project rspec-project-link))
|
||||
expect(rendered).to have_selector('[aria-label="Project information"]')
|
||||
end
|
||||
|
||||
context 'when feature flag :sidebar_refactor is disabled' do
|
||||
it 'has a link to the project path' do
|
||||
stub_feature_flags(sidebar_refactor: false)
|
||||
|
||||
render
|
||||
|
||||
expect(rendered).to have_link('Project overview', href: project_path(project), class: %w(shortcuts-project rspec-project-link))
|
||||
expect(rendered).to have_selector('[aria-label="Project overview"]')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'Details' do
|
||||
it 'has a link to the projects path' do
|
||||
it 'does not have a link to the details menu' do
|
||||
render
|
||||
|
||||
expect(rendered).to have_link('Details', href: project_path(project), class: 'shortcuts-project')
|
||||
expect(rendered).to have_selector('[aria-label="Project details"]')
|
||||
expect(rendered).not_to have_link('Details', href: project_path(project))
|
||||
end
|
||||
|
||||
context 'when feature flag :sidebar_refactor is disabled' do
|
||||
it 'has a link to the projects path' do
|
||||
stub_feature_flags(sidebar_refactor: false)
|
||||
|
||||
render
|
||||
|
||||
expect(rendered).to have_link('Details', href: project_path(project), class: 'shortcuts-project')
|
||||
expect(rendered).to have_selector('[aria-label="Project details"]')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
Loading…
Reference in New Issue