diff options
57 files changed, 1053 insertions, 722 deletions
diff --git a/.gitlab/issue_templates/Feature Flag Roll Out.md b/.gitlab/issue_templates/Feature Flag Roll Out.md index 39f4de94553..449569df554 100644 --- a/.gitlab/issue_templates/Feature Flag Roll Out.md +++ b/.gitlab/issue_templates/Feature Flag Roll Out.md @@ -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 - -<!-- Please check which steps are needed and remove those which don't apply --> - -**Rollout Steps** - -*Preparation Phase* -- [ ] Enable on staging (`/chatops run feature set feature_name true --staging`) +## Rollout Steps -- [ ] Test on staging +### Rollout on non-production environments -- [ ] Ensure that documentation has been updated ([More info](https://docs.gitlab.com/ee/development/documentation/feature_flags.html#features-that-became-enabled-by-default)) +- [ ] 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. -- [ ] 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. - -- [ ] 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. - -*Partial Rollout Phase* - -- [ ] Enable on GitLab.com for individual groups/projects listed above and verify behaviour (`/chatops run feature set --project=gitlab-org/gitlab feature_name true`) - -- [ ] Verify behaviour (See Beta Groups) and add details with screenshots as a comment on this issue - -- [ ] 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` - -*Full Rollout Phase* -- [ ] Make the feature flag enabled by default i.e. Change `default_enabled` to `true` - -- [ ] 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 - -- [ ] Announce on the issue that the flag has been enabled - -- [ ] Create a cleanup issue using the "Feature Flag Removal" template + [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. + +### Global rollout on production + +- [ ] [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). + +### (Optional) Release the feature with the feature flag + +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: + +- [ ] 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. + +**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. + +### Release the feature + +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. + +<!-- 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" @@ -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' diff --git a/Gemfile.lock b/Gemfile.lock index 70d4e89976a..a703d29bc4e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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) diff --git a/app/assets/javascripts/behaviors/shortcuts/keybindings.js b/app/assets/javascripts/behaviors/shortcuts/keybindings.js index 6abbd7f3243..c63dba05f10 100644 --- a/app/assets/javascripts/behaviors/shortcuts/keybindings.js +++ b/app/assets/javascripts/behaviors/shortcuts/keybindings.js @@ -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, }; diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcut.vue b/app/assets/javascripts/behaviors/shortcuts/shortcut.vue new file mode 100644 index 00000000000..e5992779a99 --- /dev/null +++ b/app/assets/javascripts/behaviors/shortcuts/shortcut.vue @@ -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> diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_help.vue b/app/assets/javascripts/behaviors/shortcuts/shortcuts_help.vue index 49216cc4aa0..cb7c6f9f6bc 100644 --- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_help.vue +++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_help.vue @@ -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> diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_toggle.vue b/app/assets/javascripts/behaviors/shortcuts/shortcuts_toggle.vue index 6cbe443062a..8f1518a1c9c 100644 --- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_toggle.vue +++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_toggle.vue @@ -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> diff --git a/app/assets/javascripts/editor/extensions/editor_lite_extension_base.js b/app/assets/javascripts/editor/extensions/editor_lite_extension_base.js index 3d4f08131c1..447d8e9b8c6 100644 --- a/app/assets/javascripts/editor/extensions/editor_lite_extension_base.js +++ b/app/assets/javascripts/editor/extensions/editor_lite_extension_base.js @@ -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) => { diff --git a/app/assets/stylesheets/bootstrap_migration.scss b/app/assets/stylesheets/bootstrap_migration.scss index a5cfc8d12b0..c4f292dd05d 100644 --- a/app/assets/stylesheets/bootstrap_migration.scss +++ b/app/assets/stylesheets/bootstrap_migration.scss @@ -105,10 +105,6 @@ hr { } } -kbd { - display: inline-block; -} - code { padding: 2px 4px; color: $code-color; diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index 87e4bb50984..cde5ad24fa5 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -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'; diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index aedb54c6d11..a3e3cbd3e38 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -266,15 +266,6 @@ } } - .shortcut-mappings { - display: none; - } - - &.shortcuts .shortcut-mappings { - display: inline-block; - margin-right: 5px; - } - ul { margin: 0; padding: 0; diff --git a/app/assets/stylesheets/framework/kbd.scss b/app/assets/stylesheets/framework/kbd.scss new file mode 100644 index 00000000000..05991bc16fd --- /dev/null +++ b/app/assets/stylesheets/framework/kbd.scss @@ -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; +} diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index 648ae29e212..603b05efe10 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -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; diff --git a/app/assets/stylesheets/pages/help.scss b/app/assets/stylesheets/pages/help.scss index c05216ac6e6..9182292ffd3 100644 --- a/app/assets/stylesheets/pages/help.scss +++ b/app/assets/stylesheets/pages/help.scss @@ -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; + + &-title { + margin-left: 40%; + } - .shortcut { - padding-right: 10px; - color: $gray-300; - text-align: right; - white-space: nowrap; + kbd { + margin: 0.1rem 0; + line-height: unset; + font-size: unset; + } } } diff --git a/app/finders/repositories/branch_names_finder.rb b/app/finders/repositories/branch_names_finder.rb index 5bb67425aa5..8c8c7405407 100644 --- a/app/finders/repositories/branch_names_finder.rb +++ b/app/finders/repositories/branch_names_finder.rb @@ -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 diff --git a/app/graphql/resolvers/repository_branch_names_resolver.rb b/app/graphql/resolvers/repository_branch_names_resolver.rb index 45cfe229b2f..c0a5ea0366f 100644 --- a/app/graphql/resolvers/repository_branch_names_resolver.rb +++ b/app/graphql/resolvers/repository_branch_names_resolver.rb @@ -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 diff --git a/app/models/concerns/optimized_issuable_label_filter.rb b/app/models/concerns/optimized_issuable_label_filter.rb index c7af841e450..19d2ac620f3 100644 --- a/app/models/concerns/optimized_issuable_label_filter.rb +++ b/app/models/concerns/optimized_issuable_label_filter.rb @@ -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 diff --git a/app/services/users/upsert_credit_card_validation_service.rb b/app/services/users/upsert_credit_card_validation_service.rb new file mode 100644 index 00000000000..70a96b3ec6b --- /dev/null +++ b/app/services/users/upsert_credit_card_validation_service.rb @@ -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 diff --git a/changelogs/unreleased/321364-update-post-eoa-subscriptions.yml b/changelogs/unreleased/321364-update-post-eoa-subscriptions.yml new file mode 100644 index 00000000000..0741345174e --- /dev/null +++ b/changelogs/unreleased/321364-update-post-eoa-subscriptions.yml @@ -0,0 +1,5 @@ +--- +title: Add migration to update plans on new post-EoA subscriptions +merge_request: 55625 +author: +type: changed diff --git a/changelogs/unreleased/329141-add-api-set-user-credit-card-validation.yml b/changelogs/unreleased/329141-add-api-set-user-credit-card-validation.yml new file mode 100644 index 00000000000..92cc00e92e8 --- /dev/null +++ b/changelogs/unreleased/329141-add-api-set-user-credit-card-validation.yml @@ -0,0 +1,5 @@ +--- +title: Add API to set credit card validation timestamp for user +merge_request: 60828 +author: +type: added diff --git a/changelogs/unreleased/fix-incorrect-issuable-counts.yml b/changelogs/unreleased/fix-incorrect-issuable-counts.yml new file mode 100644 index 00000000000..412b291b2b3 --- /dev/null +++ b/changelogs/unreleased/fix-incorrect-issuable-counts.yml @@ -0,0 +1,5 @@ +--- +title: Fix incorrect issue and merge requests counts with filters +merge_request: 61230 +author: +type: fixed diff --git a/changelogs/unreleased/leipert-dynamic-kbd-help.yml b/changelogs/unreleased/leipert-dynamic-kbd-help.yml new file mode 100644 index 00000000000..0b7fe113820 --- /dev/null +++ b/changelogs/unreleased/leipert-dynamic-kbd-help.yml @@ -0,0 +1,5 @@ +--- +title: "Update Keyboard shortcut help: adding search, update styling" +merge_request: 56400 +author: +type: changed diff --git a/changelogs/unreleased/mc-feature-add-limit-offset-branch-names-graphql.yml b/changelogs/unreleased/mc-feature-add-limit-offset-branch-names-graphql.yml new file mode 100644 index 00000000000..0426430a41f --- /dev/null +++ b/changelogs/unreleased/mc-feature-add-limit-offset-branch-names-graphql.yml @@ -0,0 +1,5 @@ +--- +title: Add offset and limit to branch names resolver. +merge_request: 61061 +author: +type: changed diff --git a/config/metrics/counts_all/20210216181252_boards.yml b/config/metrics/counts_all/20210216181252_boards.yml index 7552c7289c7..45844a54aa8 100644 --- a/config/metrics/counts_all/20210216181252_boards.yml +++ b/config/metrics/counts_all/20210216181252_boards.yml @@ -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 diff --git a/db/post_migrate/20210303121224_update_gitlab_subscriptions_start_at_post_eoa.rb b/db/post_migrate/20210303121224_update_gitlab_subscriptions_start_at_post_eoa.rb new file mode 100644 index 00000000000..69d99704469 --- /dev/null +++ b/db/post_migrate/20210303121224_update_gitlab_subscriptions_start_at_post_eoa.rb @@ -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 diff --git a/db/schema_migrations/20210303121224 b/db/schema_migrations/20210303121224 new file mode 100644 index 00000000000..0c0ba7c882c --- /dev/null +++ b/db/schema_migrations/20210303121224 @@ -0,0 +1 @@ +cef2421a6885cb8b28d34388af6c79c4be1564dfd5fae2efcb35622d511eb8c0
\ No newline at end of file diff --git a/doc/administration/monitoring/prometheus/gitlab_metrics.md b/doc/administration/monitoring/prometheus/gitlab_metrics.md index e8cf090b19e..45dabc387bd 100644 --- a/doc/administration/monitoring/prometheus/gitlab_metrics.md +++ b/doc/administration/monitoring/prometheus/gitlab_metrics.md @@ -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 | | diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index d9d6c6d5fe7..565f2da07eb 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -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` diff --git a/doc/ci/pipelines/job_artifacts.md b/doc/ci/pipelines/job_artifacts.md index fa5da1f6a11..76f05f5e1e7 100644 --- a/doc/ci/pipelines/job_artifacts.md +++ b/doc/ci/pipelines/job_artifacts.md @@ -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: diff --git a/doc/development/usage_ping/dictionary.md b/doc/development/usage_ping/dictionary.md index 0ec481511fa..8cee670f72a 100644 --- a/doc/development/usage_ping/dictionary.md +++ b/doc/development/usage_ping/dictionary.md @@ -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) diff --git a/doc/user/analytics/index.md b/doc/user/analytics/index.md index 5ab5fe9300a..2f83ddd7a0e 100644 --- a/doc/user/analytics/index.md +++ b/doc/user/analytics/index.md @@ -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) diff --git a/doc/user/analytics/merge_request_analytics.md b/doc/user/analytics/merge_request_analytics.md index 909eb7e585f..321e2f0fc24 100644 --- a/doc/user/analytics/merge_request_analytics.md +++ b/doc/user/analytics/merge_request_analytics.md @@ -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, diff --git a/doc/user/packages/index.md b/doc/user/packages/index.md index ca6df5e0f84..b871a08c133 100644 --- a/doc/user/packages/index.md +++ b/doc/user/packages/index.md @@ -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. diff --git a/lib/api/entities/user_credit_card_validations.rb b/lib/api/entities/user_credit_card_validations.rb new file mode 100644 index 00000000000..fcd42388b16 --- /dev/null +++ b/lib/api/entities/user_credit_card_validations.rb @@ -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 diff --git a/lib/api/users.rb b/lib/api/users.rb index 078ba7542a3..565a3544da2 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -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.' diff --git a/lib/gitlab/usage/metrics/instrumentations/count_boards_metric.rb b/lib/gitlab/usage/metrics/instrumentations/count_boards_metric.rb new file mode 100644 index 00000000000..4e1ba027bca --- /dev/null +++ b/lib/gitlab/usage/metrics/instrumentations/count_boards_metric.rb @@ -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 diff --git a/lib/sidebars/projects/menus/project_overview_menu.rb b/lib/sidebars/projects/menus/project_information_menu.rb index 32ea63e4917..3d6362cf187 100644 --- a/lib/sidebars/projects/menus/project_overview_menu.rb +++ b/lib/sidebars/projects/menus/project_information_menu.rb @@ -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), diff --git a/lib/sidebars/projects/panel.rb b/lib/sidebars/projects/panel.rb index 3be2a2b2b71..dbf2977df01 100644 --- a/lib/sidebars/projects/panel.rb +++ b/lib/sidebars/projects/panel.rb @@ -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)) diff --git a/locale/gitlab.pot b/locale/gitlab.pot index c6fa42e17eb..93235d47b2d 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -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,13 +33801,13 @@ msgstr "" msgid "Toggle project select" msgstr "" -msgid "Toggle sidebar" +msgid "Toggle shortcuts" msgstr "" -msgid "Toggle the Performance Bar" +msgid "Toggle sidebar" msgstr "" -msgid "Toggle this dialog" +msgid "Toggle the Performance Bar" msgstr "" msgid "Toggle thread" @@ -38934,6 +38973,9 @@ msgstr "" msgid "the wiki" msgstr "" +msgid "then" +msgstr "" + msgid "this document" msgstr "" diff --git a/qa/qa/page/project/show.rb b/qa/qa/page/project/show.rb index 6cc99b6cfc1..7453326573f 100644 --- a/qa/qa/page/project/show.rb +++ b/qa/qa/page/project/show.rb @@ -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? diff --git a/qa/qa/page/project/web_ide/edit.rb b/qa/qa/page/project/web_ide/edit.rb index e8e83a10a7f..c6cd572a680 100644 --- a/qa/qa/page/project/web_ide/edit.rb +++ b/qa/qa/page/project/web_ide/edit.rb @@ -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 diff --git a/qa/qa/specs/features/browser_ui/3_create/web_ide/link_to_line_in_web_ide_spec.rb b/qa/qa/specs/features/browser_ui/3_create/web_ide/link_to_line_in_web_ide_spec.rb new file mode 100644 index 00000000000..c7fc01303b7 --- /dev/null +++ b/qa/qa/specs/features/browser_ui/3_create/web_ide/link_to_line_in_web_ide_spec.rb @@ -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 diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb index 0fab5718aa6..bcccadf7710 100644 --- a/spec/features/groups_spec.rb +++ b/spec/features/groups_spec.rb @@ -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 diff --git a/spec/features/projects/active_tabs_spec.rb b/spec/features/projects/active_tabs_spec.rb index 9de43e7d18c..68695840808 100644 --- a/spec/features/projects/active_tabs_spec.rb +++ b/spec/features/projects/active_tabs_spec.rb @@ -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 diff --git a/spec/features/projects/navbar_spec.rb b/spec/features/projects/navbar_spec.rb index d01e4aa85ab..662bbd9b26f 100644 --- a/spec/features/projects/navbar_spec.rb +++ b/spec/features/projects/navbar_spec.rb @@ -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 }) diff --git a/spec/features/projects/user_uses_shortcuts_spec.rb b/spec/features/projects/user_uses_shortcuts_spec.rb index b73a1f6b9fa..cf97897d976 100644 --- a/spec/features/projects/user_uses_shortcuts_spec.rb +++ b/spec/features/projects/user_uses_shortcuts_spec.rb @@ -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 diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb index 597d22801ca..3b835d366db 100644 --- a/spec/finders/merge_requests_finder_spec.rb +++ b/spec/finders/merge_requests_finder_spec.rb @@ -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 diff --git a/spec/finders/repositories/branch_names_finder_spec.rb b/spec/finders/repositories/branch_names_finder_spec.rb index 4d8bfcc0f20..40f5d339832 100644 --- a/spec/finders/repositories/branch_names_finder_spec.rb +++ b/spec/finders/repositories/branch_names_finder_spec.rb @@ -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 diff --git a/spec/frontend/behaviors/shortcuts/shortcut_spec.js b/spec/frontend/behaviors/shortcuts/shortcut_spec.js new file mode 100644 index 00000000000..44bb74ce179 --- /dev/null +++ b/spec/frontend/behaviors/shortcuts/shortcut_spec.js @@ -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>`); + }); + }); +}); diff --git a/spec/graphql/resolvers/repository_branch_names_resolver_spec.rb b/spec/graphql/resolvers/repository_branch_names_resolver_spec.rb index 398dd7a2e2e..004e0411e51 100644 --- a/spec/graphql/resolvers/repository_branch_names_resolver_spec.rb +++ b/spec/graphql/resolvers/repository_branch_names_resolver_spec.rb @@ -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 diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/count_boards_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/count_boards_metric_spec.rb new file mode 100644 index 00000000000..52c1ccdcd47 --- /dev/null +++ b/spec/lib/gitlab/usage/metrics/instrumentations/count_boards_metric_spec.rb @@ -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 diff --git a/spec/lib/gitlab/usage_data_metrics_spec.rb b/spec/lib/gitlab/usage_data_metrics_spec.rb index 1cb43c2886c..e841f680a32 100644 --- a/spec/lib/gitlab/usage_data_metrics_spec.rb +++ b/spec/lib/gitlab/usage_data_metrics_spec.rb @@ -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 diff --git a/spec/lib/sidebars/projects/menus/project_overview_menu_spec.rb b/spec/lib/sidebars/projects/menus/project_information_menu_spec.rb index 91682a9e415..ddf9e779219 100644 --- a/spec/lib/sidebars/projects/menus/project_overview_menu_spec.rb +++ b/spec/lib/sidebars/projects/menus/project_information_menu_spec.rb @@ -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 } diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index 01a24be9f20..71fdd986f20 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -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') } diff --git a/spec/services/users/upsert_credit_card_validation_service_spec.rb b/spec/services/users/upsert_credit_card_validation_service_spec.rb new file mode 100644 index 00000000000..148638fe5e7 --- /dev/null +++ b/spec/services/users/upsert_credit_card_validation_service_spec.rb @@ -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 diff --git a/spec/support/shared_contexts/navbar_structure_context.rb b/spec/support/shared_contexts/navbar_structure_context.rb index 37f18e52449..52c8f2b5986 100644 --- a/spec/support/shared_contexts/navbar_structure_context.rb +++ b/spec/support/shared_contexts/navbar_structure_context.rb @@ -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: [ diff --git a/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb b/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb index 9087b694d63..11bb81bcd40 100644 --- a/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb +++ b/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb @@ -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 |